* feat(cli): headless runaway-protection guardrails (#4103)
Adds two opt-in run-level budgets and a startup safety warning for
non-interactive / CI / SDK runs. All defaults preserve existing
behavior; the budgets only fire when the user explicitly sets a limit.
Phase 1 — surface unsafe configs and fix doc drift
- New `--yolo`-without-sandbox stderr warning at startup of every
non-interactive run, emitted by `getHeadlessYoloSafetyWarning` in
`packages/cli/src/utils/headlessSafetyWarnings.ts`. Suppressible
via `QWEN_CODE_SUPPRESS_YOLO_WARNING=1` (strict `1`/`true` match so
`=0` / `=false` don't silence it). Strict env match also applied to
the `SANDBOX` check so values like `SANDBOX=0` don't accidentally
bypass the warning.
- Gated on `!config.isInteractive()` at the gemini.tsx call site so
TUI users aren't nagged.
- `docs/users/configuration/settings.md`: corrected
`model.skipLoopDetection` default (`true`, not `false`) and reworded
the `--yolo`/sandbox section — `--yolo` does NOT auto-enable a
sandbox; sandboxing must still be opted into explicitly.
Phase 2 — run-level budgets with distinct exit code
- `--max-wall-time` / `model.maxWallTimeSeconds`: wall-clock duration
for the whole run. Flag accepts `90` (s), `30s`, `5m`, `1h`,
`500ms`. Settings is plain seconds.
- `--max-tool-calls` / `model.maxToolCalls`: cumulative tool
executions (success + failure). Ticked BEFORE each `executeToolCall`
so a budget of N caps the run at exactly N executions.
- New `FatalBudgetExceededError` (exit code 55), distinct from
`FatalTurnLimitedError` (53) and `FatalCancellationError` (130) so
CI scripts can branch on the reason. JSON output mirrors the
`handleMaxTurnsExceededError` / `handleCancellationError` envelope
convention.
- Enforced via `RunBudgetEnforcer` in
`packages/cli/src/utils/runBudget.ts`, wired to the same
`AbortController` as SIGINT so existing cancellation plumbing
carries the abort. A `routeAbort` helper distinguishes budget vs.
SIGINT at the abort-check sites and at the outer catch.
Critical correctness fixes (informed by the #4105 review pass)
- Drain-loop fall-through: the inner drain-item `for await` previously
exited via `finalizeAssistantMessage(); return;`, swallowing a
budget abort that fires during the last drain item and surfacing
exit code 0. Now routes through `routeAbort` so exit 55 is
preserved.
- Settings symmetry: `maxWallTimeSeconds: 0` in settings.json is now
rejected (same as `--max-wall-time 0`); the enforcer treats `<=0`
as "no timer" so silent disable would be a foot-gun.
`validateMaxWallTimeSetting` also rejects `Infinity` / `NaN`.
- `setTimeout` overflow: both parser paths reject durations above
`Math.floor((2^31 - 1) / 1000)s` (~24.8 days). Node clamps
oversized delays to 1ms and fires the timer almost immediately;
fail loud at startup instead.
- First-fence-wins + SIGINT race: `markExceeded` no-ops if the
controller was already aborted by a third party, so a budget tick
arriving after user SIGINT doesn't misattribute the abort to exit
code 55.
- Outer catch re-routes mid-stream `AbortError`s through the budget
handler so users see "Run aborted: …" instead of raw "AbortError".
Tests
- `runBudget.test.ts` (32 tests): parser happy / reject paths,
setting validator, post-increment off-by-one, `maxToolCalls=0`
meaning "disallowed", `-1` meaning unlimited, wall-clock under
fake timers, `stop()` cancels pending timer, idempotent `start()`,
first-fence-wins, SIGINT-race protection.
- `headlessSafetyWarnings.test.ts` (7 tests): YOLO + sandbox / env
matrix; strict-truthy `SANDBOX` check; suppression env.
- Pre-existing suites: `nonInteractiveCli.test.ts` (46),
`gemini.test.tsx` (23), `config/config.test.ts` (220),
`core/utils/errors.test.ts` (12), `core/config/config.test.ts`
(172) all green after picking up the new config getters / CliArgs
fields.
Backward compatibility
- All budgets default to `-1` (unlimited); existing CLI invocations
behave identically.
- New stderr warning only fires in the narrow YOLO-no-sandbox case,
with an explicit suppress env.
- New exit code 55 is purely additive; no existing exit codes change
meaning.
* fix(cli): address audit findings for headless guardrails (#4103, #4502)
Round-1 audit (3 angles × line-by-line + removed-behavior + cross-file)
plus an open-ended design pass surfaced eight correctness issues. This
commit lands all of them; the larger ACP / serve-mode structural items
are documented for follow-up.
Correctness fixes
- headlessSafetyWarnings: `SANDBOX` env check reverted to plain truthy.
The sandbox transport sets `SANDBOX` to `sandbox-exec` (macOS
seatbelt) or the container name (`qwen-code-sandbox`), neither of
which matches `isTruthyEnv`. The PR's strict-`1`/`true` check was
emitting the "no sandbox" warning INSIDE real sandboxes. Match the
rest of the codebase (sandboxConfig.ts, gemini.tsx, Footer.tsx,
prompts.ts, …) which all treat any non-empty value as "sandboxed".
- nonInteractiveCli main-loop abort: add `finalizeAssistantMessage()`
before `routeAbort()`. The drain-item loop already had it (PR #4502
Critical bug #1); the main loop was asymmetric — stream-json
consumers would see an unterminated `message_start` when a budget /
SIGINT abort landed mid-stream.
- nonInteractiveCli drain-loop `routeAbort`: also flush
`flushQueuedNotificationsToSdk(localQueue)` and
`finalizeOneShotMonitors()` before exiting. The old `return`-and-
fall-through path went through the outer holdback loop, which did
this flushing; switching to `routeAbort()` skipped it, so
`task_started` envelopes lost their paired `task_notification`.
- nonInteractiveCli catch handler: emit `adapter.emitResult({...})`
BEFORE `handleBudgetExceededError`, with the budget message as
`errorMessage` when budget tripped. Previously the budget handler
`process.exit(55)`ed before the adapter could emit a terminal
`result` envelope, so STREAM_JSON consumers never saw a stream
terminator on budget exits and hung waiting for one.
- runBudget: new `validateMaxToolCalls` mirrors
`validateMaxWallTimeSetting`. yargs coerces non-numeric flag values
(`--max-tool-calls abc`) to `NaN`, and the enforcer's `>= 0` gate
treats `NaN` and negatives as "no limit", silently disabling the
budget. Reject `NaN`, `Infinity`, fractional, and negative-other-
than-`-1` values at both flag and settings layers. `0` remains
legal (`first tick aborts`), unlike wall-time where 0 is fatal.
- runBudget: new `MIN_WALL_TIME_SECONDS = 1` floor. Previously
`--max-wall-time 500ms` parsed cleanly and aborted on the next
event-loop tick before any model round-trip — almost certainly a
typo (`5m`?) and not a useful guardrail at any rate.
- nonInteractiveCli `tickToolCall`: exempt `ToolNames.STRUCTURED_OUTPUT`.
Under `--json-schema` this is the terminal "I'm done" contract tool,
not real work. Without the exemption a budget-edge completion is
aborted as a false positive (model used N tools then emitted
structured_output as call N+1 → exit 55 instead of success).
- commands/serve.ts: emit the YOLO-no-sandbox warning at daemon
startup when settings.json statically configures
`tools.approvalMode: 'yolo'` with no `tools.sandbox` /
`SANDBOX` env. The daemon can't use `getHeadlessYoloSafetyWarning`
(no Config yet — sessions get their own) so we re-derive the
predicate from settings. Per-session ACP override is documented as
out of scope.
Documentation
- `docs/users/features/headless.md`: new "Scope" subsection under
Run-level budgets explaining (a) `--max-tool-calls` counts top-level
dispatches only — subagent / `agent` tool inner calls are not
counted, (b) `structured_output` is exempt, (c) stream-json input
mode resets budgets per user message, (d) `qwen serve` / ACP
sessions do not currently consult budgets from settings.json.
Tests
- `runBudget.test.ts` grows from 32 → 41 tests: `validateMaxToolCalls`
(NaN / Infinity / negatives / fractional), `parseDurationSeconds`
sub-second rejection, `validateMaxWallTimeSetting` sub-second
rejection.
- `headlessSafetyWarnings.test.ts`: replaced the "still warns when
SANDBOX is 0/false/no" case (which encoded the strict-check bug) with
positive coverage for the real sandbox-set values
(`sandbox-exec`, `qwen-code-sandbox`).
All previously-green suites still green: cli/nonInteractiveCli (46),
cli/gemini.test (23), cli/config/config.test (220), core/utils/errors
(12), core/config/config.test (172). 337 tests across the touched suites.
Won't-fix (out of scope, documented or pre-existing)
- Unpaired `tool_use` in stream-json when a tool is aborted mid-execution
— pre-existing structural gap (SIGINT mid-tool has the same outcome);
PR amplifies it but doesn't introduce it.
- Narrow SIGINT-vs-budget-timer race — already mitigated by
`markExceeded`'s `signal.aborted` check.
- `tickToolCall` increments past abort (cosmetic; only affects the
`observed` value in the error envelope for a pathological caller).
* fix(cli): round-2 audit fixes for headless guardrails (#4103, #4502)
Round-2 audit (after round-1 commit 40ae6dd0f) surfaced two NEW
correctness issues introduced by the round-1 catch-handler restructure,
plus a handful of polish items from a parallel design pass.
Correctness fixes (new bugs from R1)
- nonInteractiveCli catch handler: wrap `adapter.emitResult` in
try/catch. R1 moved the emit BEFORE `handleBudgetExceededError` so
STREAM_JSON consumers see a terminal envelope first. But emitResult
eventually hits `stdout.write`, which throws on EPIPE /
ERR_STREAM_WRITE_AFTER_END when a piped consumer closes early
(`qwen -p ... | head -n 1` is the common CI case). Letting that
throw bubble out skipped both `handleBudgetExceededError` and
`handleError`, dropping the documented exit-code-55 contract
precisely when stdout was in trouble. Best-effort emit and continue
to the exit handler.
- nonInteractiveCli `structured_output` exemption: also require
`config.getJsonSchema?.() !== undefined`. Without that guard, an
MCP server registering an unrelated tool literally named
`structured_output` would silently bypass `--max-tool-calls`. Also
documents (in `headless.md` "Scope") the related caveat that failed
Ajv-validation retries skip the tick too, so a malformed-output
retry loop is NOT bounded by `--max-tool-calls` — combine with
`--max-session-turns` or `--max-wall-time`.
Polish
- runBudget `validateMaxToolCalls` upper bound: cap at 1_000_000.
`1e10` (typo for `1e1`) would otherwise parse cleanly, pass the
`>= 0` gate forever, and silently disable the budget — the exact
foot-gun `MAX_WALL_TIME_SECONDS` was built to prevent. Symmetry.
- runBudget `parseDurationSeconds` sub-second hint: only append the
"did you mean Ns?" suggestion when the input actually contained
`ms`. Bare `0.5` would otherwise produce a useless "did you mean
0.5s?" suggestion.
- nonInteractiveCli `routeAbort`: the `throw 'unreachable'` is only
hit if `handleBudgetExceededError` / `handleCancellationError` ever
becomes resumable (e.g. mocked `process.exit` in a test). Carry
the original exceeded.message into the thrown Error so the outer
catch's `errorMessage` field stays actionable instead of degrading
to a literal "unreachable" string.
- commands/serve.ts: compare `approvalMode` against `ApprovalMode.YOLO`
enum instead of the string literal `'yolo'`. If the enum value is
ever renamed, the startup warning stays in sync with the helper at
`headlessSafetyWarnings.ts` instead of silently going dead.
Documentation
- `headless.md` "Scope": clarify the `structured_output` exemption is
unconditional (including failed validations); add explicit note
that `--max-session-turns` does NOT exempt `structured_output`, so
size to `N+1` for `N` real-work turns under `--json-schema`.
- `headless.md` flag table: add `1.5h` to the accepted-forms hint for
`--max-wall-time` (the parser already accepts fractional units).
Tests
- `runBudget.test.ts`: new coverage for the `validateMaxToolCalls`
ceiling. Total 42 tests across `runBudget.test.ts` (was 41), all
green. cli/nonInteractiveCli, gemini.test, config/config all
unchanged and still green.
Won't-fix (documented above or out of scope)
- ACP per-session approval-mode escalation (mid-session flip to YOLO)
doesn't print the warning — daemon-level wiring; out of scope for
this PR.
- 1s wall-time floor vs higher (5–10s) — debatable, keeping 1s with
loud sub-second rejection; can raise later without semver impact.
- Integration test for the full budget-trip → catch → emitResult →
exit 55 path — requires a process-exit-mocking harness; tracked as
follow-up.
* docs: align headless guardrails examples with R1 sub-second floor
Round-3 audit caught two stale doc surfaces that R1's 1-second wall-time
floor (and R2's `1.5h` fractional-unit addition) didn't update:
- `docs/users/features/headless.md` budget table: replace stale `500ms`
example with `1.5h`, add explicit "minimum 1s — sub-second values are
rejected as typos" note.
- `docs/users/configuration/settings.md` `model.maxWallTimeSeconds` row:
same fix. Also extend `model.maxToolCalls` row with the structured_output
exemption note, the `0` semantic, and the 1,000,000 ceiling that R2
added.
A user copying the documented `--max-wall-time 500ms` example from either
surface would hit a startup error after R1.
Known follow-up (not addressed in this commit)
- No test exercises the R2 `isStructuredOutputExempt` predicate end-to-end.
Adding one needs the same process-exit-mocking harness called out in the
R2 commit as a separate follow-up.
* docs: align JSDoc / schema / CLI help with R1+R2 validation rules
Round-4 final-pass audit caught four schema/help-text/JSDoc surfaces
that drifted from the validators introduced in R1 (1s wall-time floor,
24-day ceiling) and R2 (1M tool-call ceiling, structured_output
exemption, `0` sentinel).
- `runBudget.ts` `parseDurationSeconds` JSDoc: replace stale claim
that `500ms` is accepted and "sub-second precision is preserved"
with the actual contract — `[MIN_WALL_TIME_SECONDS, MAX_WALL_TIME_SECONDS]`,
ms suffix only legal when value resolves to >= 1s. Adds `1.5h` to
the accepted-forms list.
- `settingsSchema.ts` `model.maxWallTimeSeconds` description: now
documents the 1s minimum and ~24-day ceiling.
- `settingsSchema.ts` `model.maxToolCalls` description: documents the
structured_output exemption, the `0` sentinel ("no tool calls
allowed"), and the 1,000,000 ceiling.
- `vscode-ide-companion/schemas/settings.schema.json`: mirrors both
schema descriptions above so the VS Code settings UI auto-completion
matches.
- `config.ts` yargs `--max-wall-time` description: documents the 1s
floor and the ~24-day max.
- `config.ts` yargs `--max-tool-calls` description: documents the
structured_output exemption, the `0` sentinel, and the 1M ceiling.
`qwen --help` is the most-read surface for these flags; matches the
prose docs in headless.md and settings.md.
No code changes — pure doc/help-text alignment.
---------
Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
19 KiB
Headless Mode
Headless mode allows you to run Qwen Code programmatically from command line scripts and automation tools without any interactive UI. This is ideal for scripting, automation, CI/CD pipelines, and building AI-powered tools.
Overview
The headless mode provides a headless interface to Qwen Code that:
- Accepts prompts via command line arguments or stdin
- Returns structured output (text or JSON)
- Supports file redirection and piping
- Enables automation and scripting workflows
- Provides consistent exit codes for error handling
- Can resume previous sessions scoped to the current project for multi-step automation
Basic Usage
Direct Prompts
Use the --prompt (or -p) flag to run in headless mode:
qwen --prompt "What is machine learning?"
Stdin Input
Pipe input to Qwen Code from your terminal:
echo "Explain this code" | qwen
Combining with File Input
Read from files and process with Qwen Code:
cat README.md | qwen --prompt "Summarize this documentation"
Resume Previous Sessions (Headless)
Reuse conversation context from the current project in headless scripts:
# Continue the most recent session for this project and run a new prompt
qwen --continue -p "Run the tests again and summarize failures"
# Resume a specific session ID directly (no UI)
qwen --resume 123e4567-e89b-12d3-a456-426614174000 -p "Apply the follow-up refactor"
Note
- Session data is project-scoped JSONL under
~/.qwen/projects/<sanitized-cwd>/chats.- Restores conversation history, tool outputs, and chat-compression checkpoints before sending the new prompt.
Customize the Main Session Prompt
You can change the main session system prompt for a single CLI run without editing shared memory files.
Override the Built-in System Prompt
Use --system-prompt to replace Qwen Code's built-in main-session prompt for the current run:
qwen -p "Review this patch" --system-prompt "You are a terse release reviewer. Report only blocking issues."
Append Extra Instructions
Use --append-system-prompt to keep the built-in prompt and add extra instructions for this run:
qwen -p "Review this patch" --append-system-prompt "Be terse and focus on concrete findings."
You can combine both flags when you want a custom base prompt plus an extra run-specific instruction:
qwen -p "Summarize this repository" \
--system-prompt "You are a migration planner." \
--append-system-prompt "Return exactly three bullets."
Note
--system-promptapplies only to the current run's main session.- Loaded memory and context files such as
QWEN.mdare still appended after--system-prompt.--append-system-promptis applied after the built-in prompt and loaded memory, and can be used together with--system-prompt.
Output Formats
Qwen Code supports multiple output formats for different use cases:
Text Output (Default)
Standard human-readable output:
qwen -p "What is the capital of France?"
Response format:
The capital of France is Paris.
JSON Output
Returns structured data as a JSON array. All messages are buffered and output together when the session completes. This format is ideal for programmatic processing and automation scripts.
The JSON output is an array of message objects. The output includes multiple message types: system messages (session initialization), assistant messages (AI responses), and result messages (execution summary).
Example Usage
qwen -p "What is the capital of France?" --output-format json
Output (at end of execution):
[
{
"type": "system",
"subtype": "session_start",
"uuid": "...",
"session_id": "...",
"model": "qwen3-coder-plus",
...
},
{
"type": "assistant",
"uuid": "...",
"session_id": "...",
"message": {
"id": "...",
"type": "message",
"role": "assistant",
"model": "qwen3-coder-plus",
"content": [
{
"type": "text",
"text": "The capital of France is Paris."
}
],
"usage": {...}
},
"parent_tool_use_id": null
},
{
"type": "result",
"subtype": "success",
"uuid": "...",
"session_id": "...",
"is_error": false,
"duration_ms": 1234,
"result": "The capital of France is Paris.",
"usage": {...}
}
]
Stream-JSON Output
Stream-JSON format emits JSON messages immediately as they occur during execution, enabling real-time monitoring. This format uses line-delimited JSON where each message is a complete JSON object on a single line.
qwen -p "Explain TypeScript" --output-format stream-json
Output (streaming as events occur):
{"type":"system","subtype":"session_start","uuid":"...","session_id":"..."}
{"type":"assistant","uuid":"...","session_id":"...","message":{...}}
{"type":"result","subtype":"success","uuid":"...","session_id":"..."}
When combined with --include-partial-messages, additional stream events are emitted in real-time (message_start, content_block_delta, etc.) for real-time UI updates.
qwen -p "Write a Python script" --output-format stream-json --include-partial-messages
Input Format
The --input-format parameter controls how Qwen Code consumes input from standard input:
text(default): Standard text input from stdin or command-line argumentsstream-json: JSON message protocol via stdin for bidirectional communication
Note: Stream-json input mode is currently under construction and is intended for SDK integration. It requires
--output-format stream-jsonto be set.
File Redirection
Save output to files or pipe to other commands:
# Save to file
qwen -p "Explain Docker" > docker-explanation.txt
qwen -p "Explain Docker" --output-format json > docker-explanation.json
# Append to file
qwen -p "Add more details" >> docker-explanation.txt
# Pipe to other tools
qwen -p "What is Kubernetes?" --output-format json | jq '.response'
qwen -p "Explain microservices" | wc -w
qwen -p "List programming languages" | grep -i "python"
# Stream-JSON output for real-time processing
qwen -p "Explain Docker" --output-format stream-json | jq '.type'
qwen -p "Write code" --output-format stream-json --include-partial-messages | jq '.event.type'
Configuration Options
Key command-line options for headless usage:
| Option | Description | Example |
|---|---|---|
--prompt, -p |
Run in headless mode | qwen -p "query" |
--output-format, -o |
Specify output format (text, json, stream-json) | qwen -p "query" --output-format json |
--input-format |
Specify input format (text, stream-json) | qwen --input-format text --output-format stream-json |
--include-partial-messages |
Include partial messages in stream-json output | qwen -p "query" --output-format stream-json --include-partial-messages |
--system-prompt |
Override the main session system prompt for this run | qwen -p "query" --system-prompt "You are a terse reviewer." |
--append-system-prompt |
Append extra instructions to the main session system prompt for this run | qwen -p "query" --append-system-prompt "Focus on concrete findings." |
--debug, -d |
Enable debug mode | qwen -p "query" --debug |
--all-files, -a |
Include all files in context | qwen -p "query" --all-files |
--include-directories |
Include additional directories | qwen -p "query" --include-directories src,docs |
--yolo, -y |
Auto-approve all actions | qwen -p "query" --yolo |
--approval-mode |
Set approval mode | qwen -p "query" --approval-mode auto_edit |
--continue |
Resume the most recent session for this project | qwen --continue -p "Pick up where we left off" |
--resume [sessionId] |
Resume a specific session (or choose interactively) | qwen --resume 123e... -p "Finish the refactor" |
--max-session-turns |
Cap the number of user/model/tool turns in the run | qwen -p "..." --max-session-turns 30 |
--max-wall-time |
Wall-clock budget; accepts 90 (s), 30s, 5m, 1h, 1.5h |
qwen -p "..." --max-wall-time 10m |
--max-tool-calls |
Cumulative tool-call budget for the run | qwen -p "..." --max-tool-calls 50 |
For complete details on all available configuration options, settings files, and environment variables, see the Configuration Guide.
Safety in unattended runs
Headless / CI runs combined with --yolo (or --approval-mode=yolo) auto-approve every tool call, including shell, write, and edit. --yolo does not enable a sandbox — those tools run at the host process's privilege level. When Qwen Code detects this combination with no sandbox configured, it prints a one-line warning to stderr at startup. Suppress the warning with QWEN_CODE_SUPPRESS_YOLO_WARNING=1 once you've reviewed the trade-off.
Run-level budgets
Qwen Code can abort an unattended run when it crosses one of the following thresholds. Each is -1 (unlimited) by default; setting any one is enough to bound runaway behavior. They are enforced cooperatively against the same AbortController that already carries SIGINT, so a budget abort emits a structured FatalBudgetExceededError (exit code 55) — distinct from the turn-cap exit code 53 and SIGINT's 130 so CI scripts can branch on the reason.
| Flag | Settings key | What it bounds |
|---|---|---|
--max-wall-time |
model.maxWallTimeSeconds |
Wall-clock duration of the whole run. Flag accepts 90 (s), 30s, 5m, 1h, 1.5h (fractional units supported). Minimum 1s — sub-second values are rejected as typos. Settings is seconds. |
--max-tool-calls |
model.maxToolCalls |
Cumulative top-level tool calls dispatched by the main run loop (counts successes and failures — the model still consumes tokens on errors). See "Scope" below for subagent / structured-output exemptions. |
--max-session-turns |
model.maxSessionTurns |
Number of user/model/tool turns; pre-existing. Exits with code 53 on overrun (distinct from budget exit 55). |
Scope
--max-tool-callscounts top-level dispatches only. When the model calls theagenttool, the dispatch counts as 1; inner tool calls performed by the spawned subagent are not counted. A model that funnels work through subagents can do unbounded inner work under a small top-level budget. Combine with--exclude-tools agentif you need a tighter cap.structured_outputis exempt from--max-tool-calls. Under--json-schema, the model's terminalstructured_outputcall is the "I'm done" contract, not real work — it doesn't count against--max-tool-callsso a budget-edge completion isn't aborted as a false positive. The exemption is unconditional (including failed Ajv validations), so a model stuck in a malformed-output retry loop is NOT bounded by--max-tool-calls; combine with--max-session-turnsor--max-wall-timeto cap retries.structured_outputis NOT exempt from--max-session-turns. That counter is pre-existing and bumps for every turn including the terminal contract. Size--max-session-turnstoN+1if you want to allowNreal-work turns under--json-schema.- Single-shot vs
--input-format stream-json: in stream-json input mode the daemon resets the budget counters at the start of every user message; the budget is per-message, not per-process. qwen serve/ ACP sessions: the daemon ACP session path does NOT currently consult--max-wall-time/--max-tool-callsfrom settings.json. These budgets only apply to single-shotqwen -pruns and to--input-format stream-jsonsessions. (qwen servedoes emit the YOLO-no-sandbox warning at boot iftools.approvalMode: 'yolo'is set in settings.)
Recommended combinations
- Trusted, isolated environment (ephemeral CI runner, container):
qwen -p "..." --yolo --max-session-turns N --max-wall-time 10m --output-format json. Pin a turn budget and a wall-clock budget so a stuck agent can't burn through your CI minutes, and capture--output-format jsonfor post-run usage / tool-call auditing. - Local machine or shared infra: also pass
--sandbox(or setQWEN_SANDBOX=1) so shell / write / edit tools run inside the sandbox image. - Long-running CI with retry-on-rate-limit: combine
QWEN_CODE_UNATTENDED_RETRY=1with--max-wall-time. The retry env keeps the run alive past transient 429 / 529 responses; the wall-clock budget ensures a persistently-failing provider can't extend the job indefinitely. - Bounded auditing / exploration: for read-only tasks,
--max-tool-calls 25caps how aggressively the model can grep / read. Combine with--exclude-tools shell,write,editto make the bound meaningful.
Examples
Code review
cat src/auth.py | qwen -p "Review this authentication code for security issues" > security-review.txt
Generate commit messages
result=$(git diff --cached | qwen -p "Write a concise commit message for these changes" --output-format json)
echo "$result" | jq -r '.response'
API documentation
result=$(cat api/routes.js | qwen -p "Generate OpenAPI spec for these routes" --output-format json)
echo "$result" | jq -r '.response' > openapi.json
Batch code analysis
for file in src/*.py; do
echo "Analyzing $file..."
result=$(cat "$file" | qwen -p "Find potential bugs and suggest improvements" --output-format json)
echo "$result" | jq -r '.response' > "reports/$(basename "$file").analysis"
echo "Completed analysis for $(basename "$file")" >> reports/progress.log
done
PR code review
result=$(git diff origin/main...HEAD | qwen -p "Review these changes for bugs, security issues, and code quality" --output-format json)
echo "$result" | jq -r '.response' > pr-review.json
Log analysis
grep "ERROR" /var/log/app.log | tail -20 | qwen -p "Analyze these errors and suggest root cause and fixes" > error-analysis.txt
Release notes generation
result=$(git log --oneline v1.0.0..HEAD | qwen -p "Generate release notes from these commits" --output-format json)
response=$(echo "$result" | jq -r '.response')
echo "$response"
echo "$response" >> CHANGELOG.md
Model and tool usage tracking
result=$(qwen -p "Explain this database schema" --include-directories db --output-format json)
total_tokens=$(echo "$result" | jq -r '.stats.models // {} | to_entries | map(.value.tokens.total) | add // 0')
models_used=$(echo "$result" | jq -r '.stats.models // {} | keys | join(", ") | if . == "" then "none" else . end')
tool_calls=$(echo "$result" | jq -r '.stats.tools.totalCalls // 0')
tools_used=$(echo "$result" | jq -r '.stats.tools.byName // {} | keys | join(", ") | if . == "" then "none" else . end')
echo "$(date): $total_tokens tokens, $tool_calls tool calls ($tools_used) used with models: $models_used" >> usage.log
echo "$result" | jq -r '.response' > schema-docs.md
echo "Recent usage trends:"
tail -5 usage.log
Persistent Retry Mode
When Qwen Code runs in CI/CD pipelines or as a background daemon, a brief API outage (rate limiting or overload) should not kill a multi-hour task. Persistent retry mode makes Qwen Code retry transient API errors indefinitely until the service recovers.
How it works
- Transient errors only: HTTP 429 (Rate Limit) and 529 (Overloaded) are retried indefinitely. Other errors (400, 500, etc.) still fail normally.
- Exponential backoff with cap: Retry delays grow exponentially but are capped at 5 minutes per retry.
- Heartbeat keepalive: During long waits, a status line is printed to stderr every 30 seconds to prevent CI runners from killing the process due to inactivity.
- Graceful degradation: Non-transient errors and interactive mode are completely unaffected.
Activation
Set the QWEN_CODE_UNATTENDED_RETRY environment variable to true or 1 (strict match, case-sensitive):
export QWEN_CODE_UNATTENDED_RETRY=1
Important
Persistent retry requires an explicit opt-in.
CI=truealone does not activate it — silently turning a fast-fail CI job into an infinite-wait job would be dangerous. Always setQWEN_CODE_UNATTENDED_RETRYexplicitly in your pipeline configuration.
Examples
GitHub Actions
- name: Automated code review
env:
QWEN_CODE_UNATTENDED_RETRY: '1'
run: |
qwen -p "Review all files in src/ for security issues" \
--output-format json \
--yolo > review.json
Overnight batch processing
export QWEN_CODE_UNATTENDED_RETRY=1
qwen -p "Migrate all callback-style functions to async/await in src/" --yolo
Background daemon
QWEN_CODE_UNATTENDED_RETRY=1 nohup qwen -p "Audit all dependencies for known CVEs" \
--output-format json > audit.json 2> audit.log &
Monitoring
During persistent retry, heartbeat messages are printed to stderr:
[qwen-code] Waiting for API capacity... attempt 3, retry in 45s
[qwen-code] Waiting for API capacity... attempt 3, retry in 15s
These messages keep CI runners alive and let you monitor progress. They do not appear in stdout, so JSON output piped to other tools remains clean.
Resources
- CLI Configuration - Complete configuration guide
- Authentication - Setup authentication
- Commands - Interactive commands reference
- Tutorials - Step-by-step automation guides