@qwen-code/qwen-code 0.16.0-preview.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/bundled/qc-helper/docs/configuration/settings.md +39 -37
  2. package/bundled/qc-helper/docs/features/_meta.ts +2 -0
  3. package/bundled/qc-helper/docs/features/approval-mode.md +119 -2
  4. package/bundled/qc-helper/docs/features/auto-mode.md +263 -0
  5. package/bundled/qc-helper/docs/features/commands.md +11 -10
  6. package/bundled/qc-helper/docs/features/skills.md +3 -0
  7. package/bundled/qc-helper/docs/features/structured-output.md +309 -0
  8. package/bundled/qc-helper/docs/features/sub-agents.md +47 -5
  9. package/bundled/qc-helper/docs/qwen-serve.md +119 -14
  10. package/bundled/review/SKILL.md +12 -3
  11. package/chunks/{agent-ZNQPH67I.js → agent-2JCG7FDJ.js} +9 -11
  12. package/chunks/{anthropicContentGenerator-ICBDZ6R2.js → anthropicContentGenerator-RQJNXJIY.js} +6 -3
  13. package/chunks/{askUserQuestion-WQILGUSQ.js → askUserQuestion-PQPMPNM3.js} +1 -1
  14. package/chunks/{ca-S3XJMT6P.js → ca-UZ7BANMN.js} +3 -3
  15. package/chunks/{chunk-XVHR7ATJ.js → chunk-4AOCVI6J.js} +1 -0
  16. package/chunks/chunk-7LMPOVYW.js +956 -0
  17. package/chunks/{chunk-MNPZ2WO6.js → chunk-BAZDG3QU.js} +9104 -3955
  18. package/chunks/{chunk-AEJ2DKLP.js → chunk-C6WMLUNB.js} +1 -1
  19. package/chunks/{chunk-PPHYLJSS.js → chunk-CAVZVZX6.js} +2 -2
  20. package/chunks/{chunk-VTPOO6GV.js → chunk-CSWBPY3P.js} +1 -1
  21. package/chunks/chunk-D6LBYOCX.js +19126 -0
  22. package/chunks/{chunk-NAID3ZWF.js → chunk-DMIMF3CG.js} +1 -1
  23. package/chunks/{chunk-2B7UBDY5.js → chunk-GGNTZ2NH.js} +90 -19
  24. package/chunks/chunk-HW5S7L73.js +379 -0
  25. package/chunks/{chunk-C3LHPHN2.js → chunk-JCR2WRXZ.js} +1227 -657
  26. package/chunks/{chunk-EDYSNFEM.js → chunk-L5E26RN6.js} +2 -2
  27. package/chunks/{chunk-CW44BRRA.js → chunk-MAY32HXD.js} +375 -0
  28. package/chunks/{chunk-7QXHXMC6.js → chunk-N6GSJHZ4.js} +96 -20
  29. package/chunks/{chunk-FZIUV27X.js → chunk-PVVL5Q3W.js} +31 -0
  30. package/chunks/{chunk-YHEAJFCI.js → chunk-USE2VQ5P.js} +3 -0
  31. package/chunks/{chunk-JYQUJ5DS.js → chunk-YJLGXDQJ.js} +1 -1
  32. package/chunks/{contextCommand-IGBCEXI4.js → contextCommand-XVRGKS3Q.js} +11 -13
  33. package/chunks/{cron-create-AVI3Q267.js → cron-create-IGYXQVG4.js} +27 -1
  34. package/chunks/{cron-delete-ZCEGDXXV.js → cron-delete-ETKIZCWT.js} +1 -1
  35. package/chunks/{cron-list-VN653OK5.js → cron-list-BVCUSWRU.js} +1 -1
  36. package/chunks/{de-MNR4SMAI.js → de-V4IE2OOZ.js} +3 -3
  37. package/chunks/{dist-RRYNPBOE.js → dist-4L54HRX2.js} +2 -2
  38. package/chunks/{dist-WP4AH3VK.js → dist-BXDUQ2QY.js} +1 -1
  39. package/chunks/{dist-M6GFCZ7S.js → dist-MN2PDDPR.js} +1 -1
  40. package/chunks/{edit-74Q4AFHQ.js → edit-3MLXHQPW.js} +22 -12
  41. package/chunks/{en-FIUWJSZR.js → en-HGJ2SPLM.js} +4 -3
  42. package/chunks/{enter-worktree-H72HXC7D.js → enter-worktree-OCA4SG6D.js} +35 -11
  43. package/chunks/{exit-worktree-FGIQO3S3.js → exit-worktree-6EDLXVEV.js} +35 -11
  44. package/chunks/{exitPlanMode-NBR2PK2D.js → exitPlanMode-H75KHRX4.js} +9 -11
  45. package/chunks/{fr-OFJFHLCR.js → fr-CJULI7ZX.js} +3 -3
  46. package/chunks/{geminiContentGenerator-33RP4WKD.js → geminiContentGenerator-E7Y6TCPU.js} +1 -1
  47. package/chunks/{glob-WEE3CJL6.js → glob-JFFSKARO.js} +9 -11
  48. package/chunks/{grep-DZKSBFZK.js → grep-7TAFR7MX.js} +9 -11
  49. package/chunks/{ja-V6OQ6VL7.js → ja-L7CHRQEW.js} +3 -3
  50. package/chunks/{ls-6F3VSP6S.js → ls-7HD6XG3V.js} +1 -1
  51. package/chunks/{lsp-67Y7DJN5.js → lsp-ZZSFCIWD.js} +1 -1
  52. package/chunks/{monitor-EDZWEZVS.js → monitor-YX2ABLXH.js} +21 -11
  53. package/chunks/notebook-edit-EEJEGFZR.js +756 -0
  54. package/chunks/{openaiContentGenerator-5NQG3W64.js → openaiContentGenerator-BSAWHGQJ.js} +9 -8
  55. package/chunks/{pt-ZLE6SA4A.js → pt-M6JULLEQ.js} +3 -3
  56. package/chunks/{qwenContentGenerator-4DPUUS6R.js → qwenContentGenerator-47XRHQXM.js} +11 -13
  57. package/chunks/{qwenOAuth2-JE7H47TE.js → qwenOAuth2-EEJGROP7.js} +7 -1
  58. package/chunks/{read-file-CQOF7BQ2.js → read-file-O53WD46Y.js} +4 -5
  59. package/chunks/{ripGrep-KR5LKGTI.js → ripGrep-OXNZ5Z3T.js} +9 -11
  60. package/chunks/{ru-A4OHIUNN.js → ru-QILM4HBC.js} +3 -3
  61. package/chunks/{send-message-GB4AQZNC.js → send-message-ULK4MQXJ.js} +22 -1
  62. package/chunks/{serve-GAD2PEST.js → serve-H2REZAYD.js} +13728 -3026
  63. package/chunks/{shell-E2HMCBGR.js → shell-DET66JID.js} +9 -11
  64. package/chunks/{skill-KDZH6UZ6.js → skill-ZIXPX3L3.js} +19 -6
  65. package/chunks/{src-LY4RU5AI.js → src-PN3XGQYP.js} +205 -13
  66. package/chunks/{syntheticOutput-HFL3DE7R.js → syntheticOutput-IS2X5OZ2.js} +2 -2
  67. package/chunks/{task-stop-ZQF26RXS.js → task-stop-7QSJGSSP.js} +1 -1
  68. package/chunks/{todoWrite-U4SC643O.js → todoWrite-7CVACFUX.js} +2 -2
  69. package/chunks/{tool-search-U4XQVLFU.js → tool-search-GTYLSGZ3.js} +4 -5
  70. package/chunks/{web-fetch-BRWZ4WSE.js → web-fetch-ENQ2I5JA.js} +5 -2
  71. package/chunks/{write-file-NBLRMNGB.js → write-file-NILNEZCR.js} +19 -12
  72. package/chunks/{zh-V32QONGV.js → zh-PWL2NKY3.js} +4 -3
  73. package/chunks/{zh-TW-552S24LR.js → zh-TW-S3YGWICZ.js} +4 -3
  74. package/cli.js +14832 -49049
  75. package/locales/ca.js +4 -5
  76. package/locales/de.js +4 -5
  77. package/locales/en.js +6 -5
  78. package/locales/fr.js +4 -5
  79. package/locales/ja.js +4 -5
  80. package/locales/pt.js +4 -5
  81. package/locales/ru.js +4 -5
  82. package/locales/zh-TW.js +5 -4
  83. package/locales/zh.js +5 -4
  84. package/package.json +2 -2
  85. package/chunks/chunk-3MBY4GKN.js +0 -350
  86. package/chunks/chunk-5P5XGNYH.js +0 -93
  87. package/chunks/chunk-JHMX4QTD.js +0 -2306
  88. package/chunks/chunk-SYCJMSIJ.js +0 -82
  89. package/chunks/chunk-Y6Z2O3WR.js +0 -33
@@ -0,0 +1,263 @@
1
+ # Auto Mode
2
+
3
+ Auto Mode uses an LLM classifier to evaluate each tool call and decide
4
+ whether to auto-approve it. It sits between Auto-Edit (which only
5
+ auto-approves file edits) and YOLO (which auto-approves everything).
6
+
7
+ This page is the reference for configuring and troubleshooting Auto Mode.
8
+ For an introduction, see the
9
+ [Approval Mode overview](./approval-mode.md#4-auto-mode---classifier-driven-approval).
10
+
11
+ ## How it works
12
+
13
+ When you're in Auto Mode and the agent tries to run a tool, Qwen Code
14
+ walks three layers in order:
15
+
16
+ 1. **acceptEdits fast-path** — Edit / Write whose target path is inside
17
+ the workspace is auto-approved without invoking the classifier.
18
+ 2. **Safe-tool allowlist** — Read-only and metadata-only built-in tools
19
+ (Read, Grep, Glob, LS, LSP, TodoWrite, AskUserQuestion, etc.) are
20
+ auto-approved without invoking the classifier.
21
+ 3. **LLM classifier** — Everything else (shell commands, web fetches,
22
+ sub-agent spawns, edits outside the workspace, MCP tools) is sent to
23
+ a two-stage classifier:
24
+ - **Stage 1 (fast)** — outputs `{ shouldBlock }` only. Around ~300ms.
25
+ If `shouldBlock` is `false`, the action is allowed and the call
26
+ proceeds.
27
+ - **Stage 2 (thinking)** — only runs when Stage 1 said block. Uses
28
+ chain-of-thought review to reduce Stage 1 false positives. Can
29
+ downgrade Stage 1's block to allow. Outputs the user-visible
30
+ `reason` on block.
31
+
32
+ The classifier uses your configured fast model
33
+ (`/model --fast`). If no fast model is configured, the main session
34
+ model is used instead.
35
+
36
+ ## Hard rules still win
37
+
38
+ Auto Mode does **not** replace hard permission rules. Before the classifier
39
+ runs:
40
+
41
+ - `permissions.deny` rules block the action with the rule's reason. The
42
+ classifier never sees it.
43
+ - `permissions.allow` rules with specific specifiers (e.g.
44
+ `Bash(git status)`, `Read(./docs/**)`) still auto-allow without the
45
+ classifier.
46
+ - `permissions.ask` rules force manual confirmation even in Auto Mode.
47
+
48
+ ## Over-broad allow rules are stripped while in Auto Mode
49
+
50
+ Rules like the following would let the agent execute arbitrary code
51
+ without classifier review:
52
+
53
+ - `Bash` / `Bash(*)` / `Bash()` — auto-allow every shell command
54
+ - `Bash(python:*)`, `Bash(node*)`, `Bash(bash*)` — interpreter wildcards
55
+ - `Agent` / `Agent(coder)` — any allow on the Agent tool
56
+ - `Skill` / `Skill(pdf)` — any allow on the Skill tool
57
+
58
+ When you enter Auto Mode, Qwen Code temporarily removes these rules from
59
+ the active permission set and prints a notice listing them. The rules
60
+ come back the moment you leave Auto Mode. `settings.json` is never
61
+ modified.
62
+
63
+ If you genuinely need these broad rules, use YOLO mode instead.
64
+
65
+ ## Configuring hints
66
+
67
+ Auto Mode reads `permissions.autoMode` from your `settings.json`. The
68
+ entries are natural-language descriptions, not rule patterns — they are
69
+ injected additively into the classifier's system prompt alongside the
70
+ built-in defaults.
71
+
72
+ ```json
73
+ {
74
+ "permissions": {
75
+ "autoMode": {
76
+ "hints": {
77
+ "allow": [
78
+ "Running poetry install and poetry update in this Python project",
79
+ "Cleaning build artifacts under ./dist or ./build",
80
+ "Reading any file under /Users/me/code/"
81
+ ],
82
+ "deny": [
83
+ "Any network call to intranet.example.com endpoints",
84
+ "Modifying anything under ~/.ssh or ~/.aws",
85
+ "Running migration scripts that touch the production DB"
86
+ ]
87
+ },
88
+ "environment": [
89
+ "This is a private monorepo with strict commit signing",
90
+ "Production credentials live in 1Password, never in plain files"
91
+ ]
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### Length and count limits
98
+
99
+ To keep the classifier system prompt small:
100
+
101
+ - Each entry is capped at 200 characters (longer entries are truncated
102
+ with a warning).
103
+ - `hints.allow` and `hints.deny` accept up to 50 entries each.
104
+ - `environment` accepts up to 20 entries.
105
+
106
+ ### Layering across settings files
107
+
108
+ `autoMode` is merged across system / user / workspace settings the same
109
+ way other permission settings are: arrays are concatenated and
110
+ de-duplicated.
111
+
112
+ ## Reading the decision
113
+
114
+ When the classifier blocks an action, the tool call fails with one of
115
+ the following error texts:
116
+
117
+ - **`Blocked by auto mode policy: <reason>`** — the classifier judged
118
+ the action unsafe. The reason comes from Stage 2 of the classifier.
119
+ - **`Auto mode classifier unavailable; action blocked for safety`** —
120
+ the classifier API was unreachable, timed out, or returned an
121
+ un-parseable response. This is fail-closed behavior: when in doubt,
122
+ block.
123
+
124
+ The main LLM sees the same message in the tool result and adjusts its
125
+ approach (asks you, switches tactic, gives up).
126
+
127
+ ### Classifier reason language
128
+
129
+ Classifier reasons are produced by the LLM and are not translated. If you
130
+ want non-English reasons, add a hint like
131
+ `Respond reasons in Chinese` to `permissions.autoMode.environment`.
132
+
133
+ ## Fallback to manual approval
134
+
135
+ Auto Mode protects you from getting stuck:
136
+
137
+ - After **3 consecutive policy blocks** the next tool call falls back to
138
+ the standard manual-approval prompt. This catches the case where the
139
+ agent keeps trying minor variants of a forbidden command.
140
+ - After **2 consecutive unavailable** results (classifier API failures)
141
+ the next tool call also falls back. This avoids waiting on a broken
142
+ classifier.
143
+
144
+ The session itself stays in Auto Mode — only the single fallback call
145
+ goes through manual approval. The counters reset when you approve the
146
+ fallback call or switch modes.
147
+
148
+ If you find yourself constantly hitting fallback, the most likely causes
149
+ are an outage on the classifier API or hints that need tuning. Switch to
150
+ Default Mode while you investigate.
151
+
152
+ ## Troubleshooting
153
+
154
+ **"Auto mode keeps blocking my commands"**
155
+
156
+ Look at the reason in the error message. If the classifier is being too
157
+ conservative for your context, add an entry to
158
+ `permissions.autoMode.hints.allow` describing the pattern in
159
+ natural-language. Examples:
160
+
161
+ - `"Building Docker images for this project (docker build ...)"`
162
+ - `"Running database migrations against the local test DB"`
163
+
164
+ **"Auto mode classifier unavailable"**
165
+
166
+ The classifier API didn't respond. Possible causes:
167
+
168
+ - Network issue between you and the model endpoint.
169
+ - The configured fast model is no longer available — check `/model --fast`.
170
+ - The transcript is too long and exceeds the fast-model context window.
171
+
172
+ While diagnosing, switch back to Default Mode: `/approval-mode default`.
173
+
174
+ **"Falling back to manual approval"**
175
+
176
+ You've hit either the 3-consecutive-block or 2-consecutive-unavailable
177
+ guard. Approve or reject the prompt as you normally would. After one
178
+ approved fallback the consecutive counter resets.
179
+
180
+ **The classifier sees sensitive data in my prompts**
181
+
182
+ Tool inputs are projected through each tool's `toAutoClassifierInput`
183
+ method before they reach the classifier. Long edit content, web fetch
184
+ prompts, and sub-agent prompts are truncated. Tool results (file
185
+ contents, web pages) are never sent to the classifier — only the user's
186
+ text and assistant tool-use calls go through.
187
+
188
+ If a specific tool is exposing fields you'd rather redact, file an issue
189
+ with the tool name; the projection is per-tool and is meant to be
190
+ tightened over time.
191
+
192
+ ## Limitations
193
+
194
+ - **Not offline-capable.** The classifier requires an LLM call.
195
+ - **Adds latency on the slow path.** Allowlist + acceptEdits cover most
196
+ calls without latency, but a `run_shell_command` typically adds
197
+ ~300ms (fast classifier path) or ~3-5s (slow path with thinking
198
+ review).
199
+ - **Not a substitute for `deny` rules.** The classifier is best-effort.
200
+ For commands you're sure should never run, put them in
201
+ `permissions.deny`.
202
+ - **MCP tools default to conservative blocking.** Third-party MCP tools
203
+ (`mcp__*`) opt-in to argument forwarding via the
204
+ `toAutoClassifierInput` override. Tools that have not opted in expose
205
+ only their name to the classifier — most such calls are
206
+ conservatively blocked unless you've written an explicit `allow`
207
+ rule. This is fail-closed by design (credentials and voluminous
208
+ content do not leak into the classifier LLM). If you trust a
209
+ specific MCP tool, add `permissions.allow: ["mcp__server__tool"]` so
210
+ it bypasses the classifier entirely.
211
+
212
+ ## FAQ
213
+
214
+ **Does Auto Mode send my code to a third party?**
215
+
216
+ Auto Mode reuses your existing model configuration — same endpoint as
217
+ the main agent. If you've configured Qwen Code to use a self-hosted
218
+ model, the classifier runs against that endpoint too.
219
+
220
+ **Will my secrets / `.env` contents reach the classifier?**
221
+
222
+ The classifier sees only what each tool's `toAutoClassifierInput`
223
+ projection exposes:
224
+
225
+ - `read_file` and other read-only tools: not invoked (they're on the
226
+ fast-path allowlist).
227
+ - `edit` / `write_file`: file_path plus the first 80 characters of
228
+ old/new content. Full content is not forwarded.
229
+ - `run_shell_command`: the full command (it has to — that's what the
230
+ classifier judges).
231
+ - `web_fetch`: the URL only. The prompt field is not forwarded.
232
+ - `agent`: subagent type plus the full prompt. The prompt is the
233
+ instruction the sub-agent will follow, so the classifier needs it
234
+ in full to detect attacks that would steer the sub-agent toward
235
+ destructive actions — same reason `run_shell_command` forwards the
236
+ full command.
237
+
238
+ Tool results (the actual content returned by tools) are stripped from
239
+ the classifier transcript entirely.
240
+
241
+ MCP tools (`mcp__*`) follow a stricter default: their parameters are
242
+ not forwarded unless the MCP tool author explicitly opted in via the
243
+ `toAutoClassifierInput` override. The classifier sees the tool name
244
+ but no arguments, so most MCP calls will be conservatively blocked
245
+ unless the user has written an explicit allow rule. This is fail-
246
+ closed by design — third-party tools should not leak credentials or
247
+ voluminous file content into the classifier LLM without intent.
248
+
249
+ **Can I disable the first-time information message?**
250
+
251
+ It only shows once per user-settings file. After dismissal,
252
+ `ui.autoModeAcknowledged: true` is set in your user settings.
253
+
254
+ **How is this different from Auto-Edit?**
255
+
256
+ Auto-Edit auto-approves file edits and nothing else — shell commands
257
+ still ask. Auto Mode uses a classifier to also auto-approve safe shell
258
+ commands and other tool calls while still blocking risky ones.
259
+
260
+ **How is this different from YOLO?**
261
+
262
+ YOLO auto-approves everything without any review. Auto Mode has the
263
+ classifier in the loop and blocks risky actions.
@@ -212,16 +212,17 @@ this setting.
212
212
 
213
213
  Commands for obtaining information and performing system settings.
214
214
 
215
- | Command | Description | Usage Examples |
216
- | ----------- | ----------------------------------------------- | -------------------------------- |
217
- | `/help` | Display help information for available commands | `/help` or `/?` |
218
- | `/about` | Display version information | `/about` |
219
- | `/stats` | Display detailed statistics for current session | `/stats` |
220
- | `/settings` | Open settings editor | `/settings` |
221
- | `/auth` | Change authentication method | `/auth` |
222
- | `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` |
223
- | `/copy` | Copy last output content to clipboard | `/copy` |
224
- | `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
215
+ | Command | Description | Usage Examples |
216
+ | --------------- | ----------------------------------------------- | -------------------------------- |
217
+ | `/help` | Display help information for available commands | `/help` or `/?` |
218
+ | `/status` | Display version information | `/status` or `/about` |
219
+ | `/status paths` | Display current session file and log paths | `/status paths` |
220
+ | `/stats` | Display detailed statistics for current session | `/stats` |
221
+ | `/settings` | Open settings editor | `/settings` |
222
+ | `/auth` | Change authentication method | `/auth` |
223
+ | `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` |
224
+ | `/copy` | Copy last output content to clipboard | `/copy` |
225
+ | `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
225
226
 
226
227
  ### 1.9 Common Shortcuts
227
228
 
@@ -74,6 +74,7 @@ Create a `SKILL.md` file with YAML frontmatter and Markdown content:
74
74
  ---
75
75
  name: your-skill-name
76
76
  description: Brief description of what this Skill does and when to use it
77
+ priority: 10
77
78
  ---
78
79
 
79
80
  # Your Skill Name
@@ -91,11 +92,13 @@ Qwen Code currently validates that:
91
92
 
92
93
  - `name` is a non-empty string matching `/^[\p{L}\p{N}_:.-]+$/u` — Unicode letters and digits (CJK / Cyrillic / accented Latin all OK), plus `_`, `:`, `.`, `-`. Whitespace, slashes, brackets and other structurally unsafe characters are rejected at parse time.
93
94
  - `description` is a non-empty string
95
+ - `priority` is optional. When present, it must be a finite number. Higher values sort earlier in the `/skills` listing only — slash-command completion (typing `/`) and the `/help` custom commands view stay alphabetical, so a high-priority Skill never reorders built-in commands. Omitted or invalid values are treated as unset, which behaves like `0`.
94
96
 
95
97
  Recommended conventions:
96
98
 
97
99
  - Prefer lowercase ASCII with hyphens for shareable names (e.g. `tsx-helper`)
98
100
  - Make `description` specific: include both **what** the Skill does and **when** to use it (key words users will naturally mention)
101
+ - Use `priority` sparingly for Skills that should reliably appear before the default alphabetical order in `/skills`. Negative priorities are allowed and sort below unset Skills.
99
102
 
100
103
  ### Optional: gate a Skill on file paths (`paths:`)
101
104
 
@@ -0,0 +1,309 @@
1
+ # Structured Output (`--json-schema`)
2
+
3
+ Constrain the model's final answer to a JSON Schema you supply. Qwen
4
+ Code registers a synthetic terminal tool the model is required to call,
5
+ parses the call's arguments against your schema, and exposes the
6
+ validated payload on stdout (or in the JSON / stream-json result
7
+ envelope). The first valid call ends the run.
8
+
9
+ Headless only — works with `qwen -p`, a positional prompt, or a prompt
10
+ piped via stdin.
11
+
12
+ ## Quick start
13
+
14
+ ```bash
15
+ qwen --prompt "Summarize the changes in HEAD with risk_level" \
16
+ --json-schema '{
17
+ "type": "object",
18
+ "properties": {
19
+ "summary": { "type": "string" },
20
+ "risk_level": { "type": "string", "enum": ["low", "medium", "high"] }
21
+ },
22
+ "required": ["summary", "risk_level"],
23
+ "additionalProperties": false
24
+ }'
25
+ ```
26
+
27
+ Output on stdout (default `--output-format text`):
28
+
29
+ ```json
30
+ { "summary": "…", "risk_level": "low" }
31
+ ```
32
+
33
+ The line is exactly the JSON-stringified payload + newline — no
34
+ envelope, no event log. Pipe it straight into `jq` or another consumer.
35
+
36
+ In **text** mode, stdout is reserved for the JSON payload on success
37
+ and is empty on failure; error messages and log lines go to stderr.
38
+ That makes `$(qwen --json-schema …) || exit 1` capture patterns safe
39
+ under text mode — failures land in stderr, not mixed into the captured
40
+ variable. The model's incidental prose during planning is **not**
41
+ mirrored to stderr either — text mode discards it; reach for
42
+ `--output-format json` or `stream-json` if you need to see it.
43
+
44
+ In `--output-format json` and `stream-json`, the failure result
45
+ message is emitted on **stdout** alongside the success path (as the
46
+ final element of the JSON array, or the terminating `result` line on
47
+ the JSONL stream). Not all failure modes emit a result to stdout —
48
+ max-session-turns (exit 53) and signal interrupts (exit 130) exit with
49
+ stderr output only. Check the exit code first; `is_error` on the
50
+ result object disambiguates within the subset of failures that do
51
+ produce a result event.
52
+
53
+ > **Empty schema:** Passing `{}` produces `{}` (an empty JSON object)
54
+ > on stdout. The model calls `structured_output` with no arguments;
55
+ > the upstream argument-normalisation path turns the empty function
56
+ > call into an empty-object payload, which passes validation against
57
+ > the empty schema and is emitted verbatim.
58
+
59
+ ## Supplying the schema
60
+
61
+ Two equivalent forms:
62
+
63
+ ```bash
64
+ # Inline JSON literal
65
+ qwen -p "…" --json-schema '{"type":"object", "properties":{…}}'
66
+
67
+ # Read from a file
68
+ qwen -p "…" --json-schema @./schemas/summary.json
69
+ ```
70
+
71
+ The `@path` form expands `~`, normalizes the path, and reads the file
72
+ with `utf8` encoding.
73
+
74
+ > **Latency note:** Successful runs incur a shutdown holdback **capped
75
+ > at ~500 ms** while in-flight background agents flush their final
76
+ > notifications before the result is emitted. The holdback exits early
77
+ > if no background tasks are pending, so simple runs barely notice it;
78
+ > batch pipelines that fan out hundreds of `--json-schema` invocations
79
+ > against busy agents should account for this upper bound.
80
+
81
+ > **Security note:** Schemas may contain user-supplied regular
82
+ > expressions in `pattern` keywords. Ajv compiles these with the
83
+ > ECMAScript regex engine, which is vulnerable to catastrophic
84
+ > backtracking. Because tool arguments are always objects, the
85
+ > `pattern` keyword only fires inside string properties — a malicious
86
+ > schema like
87
+ > `{"type":"object","properties":{"value":{"type":"string","pattern":"(a+)+b"}}}`
88
+ > can hang the CLI when the model supplies a moderately long
89
+ > matching value. Only run `--json-schema` with schemas from sources
90
+ > you trust.
91
+
92
+ Validation at parse time:
93
+
94
+ - The file must be a regular file (no FIFOs, character devices, or
95
+ directories).
96
+ - File size is capped at 4 MiB. Real-world JSON schemas are well under
97
+ this; multi-MiB files almost always indicate a wrong-path mistake.
98
+ - The schema must be valid JSON. For `@path` input, the parse error is
99
+ generic ("content of `<path>` is not valid JSON") rather than echoing
100
+ the SyntaxError detail, so a wrapping process that surfaces stderr
101
+ can't read a prefix of the file's contents back from the error.
102
+ - The schema must compile under the strict Ajv configuration —
103
+ typos like `propertees` are surfaced, but spec-valid patterns
104
+ (e.g. `required` without listing every key in `properties`) are
105
+ accepted.
106
+ - The schema root must accept object-typed values. Function-calling
107
+ APIs (Gemini, OpenAI, Anthropic) all require tool arguments to be
108
+ JSON objects, so a non-object root would register an unusable tool.
109
+
110
+ The root-acceptance check walks `type`, `const`, `enum`, `anyOf`,
111
+ `oneOf`, `allOf`, `not`, and `if`/`then`/`else` (best-effort for the
112
+ decidable cases). When in doubt it defers to Ajv at runtime.
113
+
114
+ > **Root `$ref` is rejected** by the parse-time check. If your schema
115
+ > reuses a definition via `$ref`, wrap it in `allOf`:
116
+ >
117
+ > ```jsonc
118
+ > // Rejected:
119
+ > { "$ref": "#/$defs/MyObj", "$defs": { "MyObj": { "type": "object", "properties": { "name": { "type": "string" } } } } }
120
+ >
121
+ > // Accepted (root accepts objects via the allOf branch):
122
+ > { "allOf": [{ "$ref": "#/$defs/MyObj" }], "$defs": { "MyObj": { "type": "object", "properties": { "name": { "type": "string" } } } } }
123
+ > ```
124
+ >
125
+ > `$ref` inside `anyOf` / `oneOf` / `allOf` is deferred to Ajv at
126
+ > runtime, so the wrapped form passes the root-acceptance check.
127
+
128
+ ## Output shape per format
129
+
130
+ | `--output-format` | What goes to stdout |
131
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
132
+ | `text` (default) | `JSON.stringify(payload) + "\n"` — one line, the validated object. |
133
+ | `json` | A single JSON **array** of message objects (the full event log). The final element is the `type: "result"` message, which carries both `result` (`JSON.stringify(payload)`) and `structured_result` (the raw object). |
134
+ | `stream-json` | Each event on its own line as JSONL. The terminating `result` line carries `result` (stringified) and `structured_result` (raw object). |
135
+
136
+ In both JSON formats, prefer reading `structured_result` over `result`
137
+ when you want the object; `result` is the stringified form provided for
138
+ consumers that always expect a string in that field. For `--output-format
139
+ json`, read the last element of the array and pull `structured_result`
140
+ from there (e.g. `jq '.[-1].structured_result'`); for `stream-json`,
141
+ read the final `type: "result"` line on the stream.
142
+
143
+ ## Restrictions
144
+
145
+ | Combination | Behavior |
146
+ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | `--json-schema` + `-i` / `--prompt-interactive` | Rejected at parse time. The synthetic tool's "session ends now" message has no terminator in the TUI loop. |
148
+ | `--json-schema` + `--input-format stream-json` | Rejected at parse time. The single-shot terminal contract is incompatible with the long-lived stream-json input protocol. |
149
+ | `--json-schema` + `--acp` / `--experimental-acp` | Rejected at parse time. ACP runs its own turn loop that doesn't honor the synthetic-tool terminal contract. |
150
+ | `--json-schema` with no prompt and no piped stdin | Rejected at parse time. Headless mode needs a prompt — pass `-p`, a positional argument, or pipe one in. |
151
+ | `--bare` + `--json-schema` | Supported. The synthetic tool is registered alongside the bare three (`read_file`, `edit`, `run_shell_command`). |
152
+ | `--json-schema` inside a subagent | Tool is NOT registered. Only the main / drain turns of the top-level run honor the terminal contract; a subagent calling the tool would receive "session ends now" and then keep running because its loop has no terminator. |
153
+
154
+ ## Retry and failure modes
155
+
156
+ > **Cost note.** Two things multiply token spend in a `--json-schema`
157
+ > run, both worth designing for:
158
+ >
159
+ > - **Schema embedded in every turn.** The schema ships as the
160
+ > `structured_output` function declaration's `parameters` block on
161
+ > every model request, not just the first. Large schemas (up to the
162
+ > 4 MiB parse cap) proportionally increase per-turn input tokens
163
+ > for the entire run.
164
+ > - **Each validation retry is a full model turn.** A schema the
165
+ > model misses repeatedly is multiplied per failure (request +
166
+ > inference + response). Keep schemas constrained enough to guide
167
+ > the model and simple enough to nail on the first try; raise
168
+ > `--max-session-turns` when retries are expected.
169
+
170
+ The session ends on the first valid call. Until then:
171
+
172
+ - **Args fail validation.** `structured_output` returns a tool-result
173
+ error with Ajv's message, the model sees it on the next turn, and
174
+ may correct the arguments and call again.
175
+ - **Model calls a side-effecting tool in the same turn as
176
+ `structured_output`.** The pre-scan suppresses the sibling — it
177
+ never runs, regardless of whether the structured call ultimately
178
+ validates. The two paths split on what the model sees next:
179
+ - **Validation succeeds:** the run ends immediately, and the model
180
+ never gets another turn — the suppressed sibling is silently
181
+ discarded.
182
+ - **Validation fails:** the model gets another turn and sees a
183
+ synthesised "Skipped:" `tool_result` for the suppressed call,
184
+ so it can re-issue that call in a **separate turn** (one that
185
+ does not include `structured_output`).
186
+ - **Model emits plain text instead of calling
187
+ `structured_output`.** Exit code `1`. The error message includes
188
+ the turn count and a truncated preview of the model's output so
189
+ you can see what it actually said.
190
+ - **Run reaches `maxSessionTurns`.** Exit code `53`. Standard
191
+ "Reached max session turns" exit, plus a `--json-schema`-specific
192
+ hint that points at the three common stuck-run causes: model never
193
+ called the tool, `structured_output` is denied by permission rules,
194
+ or the schema is unsatisfiable.
195
+ - **Run is interrupted (SIGINT / Ctrl-C).** Exit code `130`. The
196
+ structured result is normally not emitted, but the shutdown
197
+ holdback loop does not poll the abort signal, so a SIGINT that
198
+ arrives after a successful call has been captured but before the
199
+ result reaches stdout may still land on stdout. Treat the exit
200
+ code as the source of truth.
201
+
202
+ ## Privacy
203
+
204
+ The args you submit through `structured_output` ARE the structured
205
+ payload — already emitted on stdout. To avoid persisting the same
206
+ payload a second time into on-device surfaces that may be exported off
207
+ the machine, args are redacted with the placeholder
208
+ `{ __redacted: 'structured_output payload (see stdout result)' }` on:
209
+
210
+ - The `ToolCallEvent` telemetry path (OTLP exports, QwenLogger,
211
+ ui-telemetry stream, chat-recording UI event mirror).
212
+ - The on-disk chat-recording JSONL at
213
+ `~/.qwen/projects/<sanitized-cwd>/chats/<sessionId>.jsonl` (re-fed
214
+ into model context on `--continue` / `--resume`), including every
215
+ validation-failure retry.
216
+
217
+ Tool-call metrics (duration, success, decision) and surrounding event
218
+ metadata are preserved.
219
+
220
+ > **Schema is sent to the model provider.** Redaction covers the
221
+ > _call arguments_ on local surfaces only. The schema itself rides
222
+ > on every model request as the `structured_output` function
223
+ > declaration's `parameters` block — so any literal values you put
224
+ > inside it (`enum`, `const`, `default`, `examples`, `description`,
225
+ > `$comment`, etc.) reach the provider in cleartext just like prompt
226
+ > text. Schemas should describe shape and constraints; treat them as
227
+ > public toward the provider and keep secrets, customer records, and
228
+ > other sensitive payloads out of the schema body.
229
+
230
+ > **Hooks see raw args.** The redaction described above only applies
231
+ > to telemetry and chat-recording. `PreToolUse`, `PostToolUse`, and
232
+ > `PostToolUseFailure` hooks (including HTTP hooks that can forward
233
+ > payloads off-device) receive the unredacted `tool_input` for
234
+ > `structured_output`, since the hook contract is "see what the tool
235
+ > sees." If you operate audit-style catch-all hooks, either disable
236
+ > them for `structured_output` (filter on `tool_name`) or add
237
+ > hook-side redaction before running `--json-schema` against
238
+ > sensitive data.
239
+
240
+ ## Session resumption (`--continue` / `--resume`)
241
+
242
+ `--json-schema` is a per-run flag, not a per-session property. The
243
+ synthetic tool is registered when the CLI parses its arguments, so:
244
+
245
+ - Re-pass `--json-schema` on every `--continue` / `--resume` you want
246
+ the terminal contract to apply to. The same schema as the original
247
+ run is the safe default — a mid-session schema swap is allowed but
248
+ changes the contract the model is being held to.
249
+ - If you `--continue` without `--json-schema`, the resumed run is an
250
+ ordinary headless session: `structured_output` simply doesn't
251
+ exist as a tool, and the model will respond in free-form text.
252
+ - The `__redacted` placeholder in the resumed chat-recording does
253
+ not affect resumability in practice. A successful `structured_output`
254
+ call terminates the session immediately, so the only redacted args
255
+ a resumed run could see are from failed attempts. The model still
256
+ has each attempt's Ajv validation error in the recorded `tool_result`
257
+ and the live parameter schema (re-registered from `--json-schema`),
258
+ which is enough to retry.
259
+
260
+ ## Permission gating
261
+
262
+ `structured_output` deliberately bypasses the `--core-tools` allowlist:
263
+ the tool only exists when `--json-schema` is set, so excluding it
264
+ would leave the run with no terminal contract.
265
+
266
+ Explicit `permissions.deny` rules and `--exclude-tools` settings DO
267
+ take effect — both use the same deny mechanism and both prevent
268
+ `structured_output` from being registered, so the model never sees
269
+ the tool declaration. The typical result is that the model answers in
270
+ plain text (exit 1). If the model loops through other tools without
271
+ ever producing text, it will eventually hit `maxSessionTurns`
272
+ (exit 53) and the `--json-schema` hint in the error message tells you
273
+ where to look.
274
+
275
+ > **`--bare` caveat.** Bare mode ignores most settings-derived inputs,
276
+ > including settings-level `permissions.deny` and `tools.exclude`. The
277
+ > synthetic tool stays registered, so a settings-only deny of
278
+ > `structured_output` will silently no-op under `--bare`. Argv-level
279
+ > `--exclude-tools structured_output` still applies in bare mode — use
280
+ > the flag rather than settings if you need to lock down a bare run.
281
+
282
+ ## Conflict with MCP tools
283
+
284
+ If an MCP server registers a tool literally named `structured_output`,
285
+ the tool-registry collision check renames the MCP tool to
286
+ `mcp__<server-name>__structured_output` so the synthetic tool keeps
287
+ the bare name. The user-supplied schema is always the one the model
288
+ sees.
289
+
290
+ ## Example: gating a multi-step run on the structured output
291
+
292
+ ```bash
293
+ RESULT=$(qwen --prompt "Audit this diff and rate its risk." \
294
+ --json-schema @./schemas/audit.json) || exit 1
295
+
296
+ risk=$(jq -r '.risk_level' <<<"$RESULT")
297
+ if [ "$risk" = "high" ]; then
298
+ echo "High-risk diff; pausing pipeline." >&2
299
+ exit 2
300
+ fi
301
+ ```
302
+
303
+ ## See also
304
+
305
+ - [Headless Mode](headless.md) — the `-p`-based flow `--json-schema`
306
+ builds on.
307
+ - [Dual Output](dual-output.md) — records a JSON-event sidecar
308
+ alongside the TUI (a different approach to machine-readable output;
309
+ does not require `--json-schema`).