Vibe Headless Mode Gotchas
Vibe Headless Mode Gotchas
Section titled “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).
| Format | First-byte latency | Use when |
|---|---|---|
text (default) | end-of-session | one final human-readable answer, no live monitoring |
streaming | immediate (per message) | parallel batches, progress visibility, kill-on-stall heuristics |
json | end-of-session | structured 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:
timeout 300 vibe -p "…" --agent auto-approve \ --max-turns 15 --output streaming > out.ndjson 2> out.errGotcha 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:
-pno longer auto-approves tool calls by default — pass--auto-approveto 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:
- Trying a different tool (which also gets skipped).
- Rewording its plan.
- 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.
Why the LiteLLM proxy is a red herring
Section titled “Why the LiteLLM proxy is a red herring”~/.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/healthis expected behavior — that endpoint is gated bygeneral_settings.master_keyin LiteLLM 1.x. The unauthenticated probes are/health/livelinessand/health/readiness(both return 200). - The proxy being down does not affect
vibe -p. - The proxy being up does not help
vibe -p. - When
vibeis stalled, do not investigate the LiteLLM proxy. Look at~/.vibe/logs/vibe.logand 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'.
Safe parallel-dispatch template
Section titled “Safe parallel-dispatch template”This is the pattern to copy. Each guard addresses one of the gotchas above.
#!/usr/bin/env bashset -euo pipefailVIBE_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 limitPER_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 dispatchexport VIBE_BIN WORKDIR OUTDIR PER_JOB_TIMEOUT
# One line per job: "<label>\t<prompt>"JOBS=$(cat <<'EOF'labelA write a README for repo XlabelB summarize the diff for branch YlabelC 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-mortemfor 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)…)"doneWhy each guard exists:
| Guard | Prevents |
|---|---|
--agent auto-approve | Silent tool skips → apology output, no artifact (Gotcha 3) |
--output streaming | False “stuck” verdicts from buffered text output (Gotcha 1) |
timeout $PER_JOB_TIMEOUT | Mistral SDK 5-minute retry budget × 3 outer retries → multi-hour hangs (Gotcha 2) |
xargs -P 3 | Mistral per-key rate limit → 429 storms → cascade of retry-budget hangs (Gotcha 4) |
| Empty-NDJSON probe | Distinguishes “actually stalled” from “completed but said nothing” |
Troubleshooting checklist
Section titled “Troubleshooting checklist”When a parallel Vibe batch goes wrong, walk this list before investigating the proxy, the network, or the API:
- 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. - Are you looking at
--output textand assuming 0 bytes means stuck? Switch to--output streamingand re-run. - Is the job in a multi-hour retry? Add
timeout 300(or whatever 2× normal completion time is) and re-run. Inspectstderrafter timeout for 429/5xx hints. - 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.
Related Resources
Section titled “Related Resources”- Mistral Vibe — Main Vibe CLI guide (configuration, MCP, skills, headless dispatch patterns)
- Claude Code Self-Hosted —
cc-vibesetup using the LiteLLM proxy this page warns against confusing with vibe itself - OpenCode — Alternative dispatch target when LSP-aware edits matter
External Links
Section titled “External Links”- Mistral Vibe GitHub — Source for the behaviors documented above
- Mistral API rate limits — Per-key/tier QPS limits relevant to Gotcha 4