Skip to content

Vibe Headless Mode Gotchas

When you dispatch Mistral Vibe in headless mode (vibe -p "…"), four behaviors are not obvious from the help text and have collectively burned days of debugging across the community. Read this before writing a script that fans out parallel Vibe jobs.

Gotcha 1 — --output text buffers until the session ends

Section titled “Gotcha 1 — --output text buffers until the session ends”

The default text formatter does not stream. It stores the assistant’s final response in memory and writes it to stdout exactly once, in finalize(), after the agent loop exits.

Consequence:

  • A 0-byte output file while the process is still alive tells you nothing about whether the model is making progress.
  • The “file still empty after N minutes → process must be stuck” monitoring heuristic is false for --output text. Don’t write supervision code against it.

For progress visibility (parallel batches, kill-on-stall checks), use --output streaming — newline-delimited JSON, one line per message, flushed immediately. The first byte lands within about a second on a healthy connection (verified empirically: ~28 KB of NDJSON for a trivial “reply pong” test, first line within ~1s).

FormatFirst-byte latencyUse when
text (default)end-of-sessionone final human-readable answer, no live monitoring
streamingimmediate (per message)parallel batches, progress visibility, kill-on-stall heuristics
jsonend-of-sessionstructured post-mortem of all turns

Gotcha 2 — Mistral SDK retry budget is 5 minutes per LLM call

Section titled “Gotcha 2 — Mistral SDK retry budget is 5 minutes per LLM call”

The Mistral backend in vibe is configured with a 5-minute wall-clock retry budget per API call (BackoffStrategy(max_elapsed_time=300_000ms)), wrapped by an outer @async_retry(tries=3) decorator. Worst case per LLM call: ~15 minutes of backoff before the call surfaces an error.

--max-turns N is a turn cap, not a wall-clock cap. A 30-turn session hammering rate limits can in theory hang for hours.

Rule: always wrap headless invocations with timeout. Choose the duration based on observed normal completion × 2, not from --max-turns × 15min:

Terminal window
timeout 300 vibe -p "" --agent auto-approve \
--max-turns 15 --output streaming > out.ndjson 2> out.err

Gotcha 3 — vibe -p no longer auto-approves tools

Section titled “Gotcha 3 — vibe -p no longer auto-approves tools”

This changed in a recent mistral-vibe release. Quote from vibe/whats_new.md:

Safer programmatic mode: -p no longer auto-approves tool calls by default — pass --auto-approve to restore the previous behavior.

The wording in the release notes is slightly imprecise — there is no top-level --auto-approve flag. The actual mechanism is the agent profile: pass --agent auto-approve (one of the builtin agents: default, plan, accept-edits, auto-approve).

What happens when a tool needs approval and no approval callback is wired (which is the case for plain -p with the default agent)? vibe does not hang on stdin. It silently skips the tool with the feedback "Tool execution not permitted." The model typically responds by:

  1. Trying a different tool (which also gets skipped).
  2. Rewording its plan.
  3. Apologizing and reporting it cannot complete the task.

From outside, this looks like:

  • Process runs for a few minutes, exits cleanly with code 0.
  • Output file is empty (the text formatter wrote None), or contains an apology instead of the requested artifact.
  • Stderr is clean.

Fix: any prompt that needs write_file, search_replace, or bash mutation must include --agent auto-approve. Pure read-only invocations (--enabled-tools "read_file" --enabled-tools "grep") don’t need it.

Gotcha 4 — No internal concurrency cap; Mistral rate-limits per key

Section titled “Gotcha 4 — No internal concurrency cap; Mistral rate-limits per key”

vibe does not throttle outbound calls. N parallel vibe -p invocations means N concurrent Mistral API clients sharing whatever API key is in ~/.vibe/.env. The Mistral API rate limit is per key.

Past about 3 concurrent calls (especially on the devstral-medium tier), expect 429s. Those 429s feed straight into the 5-minute SDK backoff budget from Gotcha 2, and now all N processes sit in retry, indistinguishable from “stuck.”

Recommended cap: 3 concurrent. Raise only if you’ve empirically verified more works for your prompt shape and Mistral tier. Cap with xargs -P 3 or GNU parallel -j 3 — see template below.

~/.vibe/litellm-config.yaml runs a local LiteLLM proxy at http://127.0.0.1:4000. That proxy exists to expose Mistral models in Anthropic Messages format for Claude Code running on Mistral (the cc-vibe alias — see Claude Code Self-Hosted).

The vibe CLI itself does not consume the proxy. It reads ~/.vibe/config.toml (which has [provider] api_key = "…") and uses the official Mistral SDK to hit api.mistral.ai directly.

Consequences when troubleshooting:

  • A 401 on http://127.0.0.1:4000/health is expected behavior — that endpoint is gated by general_settings.master_key in LiteLLM 1.x. The unauthenticated probes are /health/liveliness and /health/readiness (both return 200).
  • The proxy being down does not affect vibe -p.
  • The proxy being up does not help vibe -p.
  • When vibe is stalled, do not investigate the LiteLLM proxy. Look at ~/.vibe/logs/vibe.log and the per-session directory at ~/.vibe/logs/session/session_<timestamp>_<id>/ instead.

There is also a cosmetic trap: ~/.vibe/litellm.pid is written once by the launcher and never updated. Nothing reads it. To find the live LiteLLM proxy pid, use launchctl list | grep io.vibe.litellm or pgrep -f 'litellm --config.*\.vibe/litellm-config'.

This is the pattern to copy. Each guard addresses one of the gotchas above.

#!/usr/bin/env bash
set -euo pipefail
VIBE_BIN="$(command -v vibe 2>/dev/null || echo "$HOME/.local/bin/vibe")"
WORKDIR="$(pwd)"
OUTDIR=$(mktemp -d -t vibe-batch)
MAX_PARALLEL=3 # do not raise without verifying — Mistral per-key rate limit
PER_JOB_TIMEOUT=300 # wall-clock seconds; covers the 5-min Mistral SDK retry budget
dispatch() {
local label=$1 prompt=$2
timeout "$PER_JOB_TIMEOUT" "$VIBE_BIN" -p "$prompt" \
--workdir "$WORKDIR" --agent auto-approve \
--max-turns 20 --output streaming \
> "$OUTDIR/$label.ndjson" 2> "$OUTDIR/$label.err"
local rc=$?
echo "$label exit=$rc bytes=$(wc -c < "$OUTDIR/$label.ndjson")"
}
export -f dispatch
export VIBE_BIN WORKDIR OUTDIR PER_JOB_TIMEOUT
# One line per job: "<label>\t<prompt>"
JOBS=$(cat <<'EOF'
labelA write a README for repo X
labelB summarize the diff for branch Y
labelC list TODOs in src/
EOF
)
printf '%s\n' "$JOBS" | xargs -P "$MAX_PARALLEL" -I{} -d '\n' \
bash -c 'IFS=$'\''\t'\'' read -r l p <<< "{}"; dispatch "$l" "$p"'
# Post-mortem
for f in "$OUTDIR"/*.ndjson; do
label=$(basename "$f" .ndjson)
if [ ! -s "$f" ]; then
echo "$label: STALLED (empty output — timeout fired or never produced bytes)"
head -5 "$OUTDIR/$label.err" 2>/dev/null
continue
fi
final=$(grep '"role": "assistant"' "$f" | tail -1)
echo "$label: ok (final assistant message: $(echo "$final" | head -c 100)…)"
done

Why each guard exists:

GuardPrevents
--agent auto-approveSilent tool skips → apology output, no artifact (Gotcha 3)
--output streamingFalse “stuck” verdicts from buffered text output (Gotcha 1)
timeout $PER_JOB_TIMEOUTMistral SDK 5-minute retry budget × 3 outer retries → multi-hour hangs (Gotcha 2)
xargs -P 3Mistral per-key rate limit → 429 storms → cascade of retry-budget hangs (Gotcha 4)
Empty-NDJSON probeDistinguishes “actually stalled” from “completed but said nothing”

When a parallel Vibe batch goes wrong, walk this list before investigating the proxy, the network, or the API:

  1. Are tools getting skipped silently? Inspect the NDJSON output. Look for assistant messages containing “Tool execution not permitted” or apologies about being unable to write files. Fix: add --agent auto-approve.
  2. Are you looking at --output text and assuming 0 bytes means stuck? Switch to --output streaming and re-run.
  3. Is the job in a multi-hour retry? Add timeout 300 (or whatever 2× normal completion time is) and re-run. Inspect stderr after timeout for 429/5xx hints.
  4. Are you running more than ~3 in parallel? Reduce to 3 with xargs -P 3. If the jobs now complete, you were hitting the per-key rate limit.

If all four are addressed and the batch still misbehaves, then check the model status page and ~/.vibe/logs/. Do not begin troubleshooting at the LiteLLM proxy — see the red herring section above.

  • Mistral Vibe — Main Vibe CLI guide (configuration, MCP, skills, headless dispatch patterns)
  • Claude Code Self-Hostedcc-vibe setup using the LiteLLM proxy this page warns against confusing with vibe itself
  • OpenCode — Alternative dispatch target when LSP-aware edits matter