Repository navigation
Description
[Bug]: Windows — .cmd-resolved coding-agent CLIs lose empty-string arguments, so qoder's --setting-sources "" swallows --max-turns and every turn dies with Invalid setting source
Client or integration
Codex App
Area
Provider adapter / Platform (Windows)
Summary
On Windows, when a coding-agent CLI is resolved to a .cmd shim, OpenCodex routes it through cmd.exe /d /s /c and the empty-string arguments are lost. For the qoder adapter that is fatal, because buildQoderArgs() deliberately emits two of them:
"--tools", "",
"--strict-mcp-config",
"--setting-sources", "",
"--max-turns", "1",The CLI receives --setting-sources --max-turns 1, treats --max-turns as the value of --setting-sources, and aborts before any work:
An unexpected critical error occurred:Error: Invalid setting source: "--max-turns".
Valid options are: user, project, local
at GaA (B:/~BUN/root/chunk-46tge90n.js:2:2624)
at Zc (B:/~BUN/root/chunk-46tge90n.js:5:10897)
at async bs (B:/~BUN/root/chunk-j3cbh6wv.js:18:594)
Result: 100 % of qoder-cn turns fail on any machine where the CN CLI installs its qodercn.cmd dispatcher ahead of the real executable — which is the default layout of the official installer (%USERPROFILE%\.qoder-cn\entry\qodercn.cmd, with the real binary at %USERPROFILE%\.qoder-cn\bin\qoderclicn\qoderclicn.exe).
Second, quieter problem: --tools "" is lost the same way. If a future CLI tolerates the --setting-sources value, the turn would then run with the CLI's default tool set instead of the documented "built-in tools disabled" isolation.
Reproduction
1. Resolution picks the .cmd
src/adapters/coding-agent/profile.ts scans PATH with win32 ? [".cmd", ".exe", ".bat", ""] : [""], and the CN profile's candidates are ["qodercn", "qoderclicn"]. With the official install layout, qodercn resolves to the .cmd dispatcher.
2. Observed spawn (instrumented turn.ts for this trace only)
[DIAG-SPAWN] {"file":"cmd.exe","args":["/d","/s","/c",
"\"C:\\...\\.qoder-cn\\entry\\qodercn.cmd ^\"-p^\" ^\"--output-format^\" ^\"stream-json^\" ...
^\"--tools^\" ^\"^\" ^\"--strict-mcp-config^\" ^\"--setting-sources^\" ^\"^\" ^\"--max-turns^\" ^\"1^\" ...\""]}
3. Control experiments (decisive)
| # | Invocation | Result |
|---|---|---|
| A | qoderclicn.exe with the exact argv above (includes both "" args) |
✅ args parsed, turn runs, --tools becomes an empty list ("tools":[] in the init frame) |
| B | qodercn.cmd with the same argv, called directly from a script — no OpenCodex involved |
❌ identical Invalid setting source: "--max-turns" |
| C | qodercn.cmd with --max-turns 1 removed |
✅ runs |
B matters for the fix: the loss is not caused by OpenCodex's own quoting/escaping alone. The .cmd dispatcher forwards via powershell.exe -File <dispatcher>.ps1 -- %*, and the empty argument does not survive that hop either. So re-quoting inside escapeCmdArg() will not be enough — the .cmd has to be avoided.
4. Workaround that works today
Reordering the CN profile's candidates so the real executable wins:
binaryCandidates: ["qoderclicn", "qodercn"], // was ["qodercn", "qoderclicn"]With that, the spawn becomes {"file":"C:\\...\\qoderclicn.exe","args":[... "--setting-sources","","--max-turns","1" ...]} and turns succeed. (See the note below before shipping this as the fix.)
Version
2.89.0
Operating system
Windows 11 (build 26200), win32 x64
Provider and model
qoder-cn / Qwen3.8-Flash (model-independent — the failure happens during argument parsing, before the model is contacted)
Logs or error output
Qoder CN CLI exited with code 1: An unexpected critical error occurred:Error:
Invalid setting source: "--max-turns". Valid options are: user, project, local
# resolved binary
qodercn -> C:\Users\<user>\.qoder-cn\entry\qodercn.cmd (a .cmd → powershell dispatcher)
qoderclicn-> C:\Users\<user>\.qoder-cn\bin\qoderclicn\qoderclicn.exe (real binary, v1.1.70)
Suggested fix
Preferred, in order:
- Prefer the real executable when both exist. In
resolveCodingAgentBinary()/whichFromPath(), if a candidate resolves to a.cmdand a sibling.exewith the same stem exists in the same directory, resolve to the.exe. This fixes the whole class, not just qoder, and keeps argument boundaries intact (nocmd.exehop at all). - Don't round-trip through
cmd.exewhen an.exeis available for the same command name, even if the.cmdis what PATH order returned. - If neither is acceptable, validate the built command line for arguments that are empty strings and refuse with a pre-flight error instead of letting the vendor CLI misinterpret the next flag. That turns a confusing vendor crash into an actionable message.
Note on the local workaround (["qoderclicn", "qodercn"]): it works, but it hard-codes a vendor filename order and would break if a future installer renames binaries. A resolution-level fix is preferable.
Config (redacted)
{ "providers": { "qoder-cn": { "adapter": "qoder", "baseUrl": "https://qoder.cn" } } }Environment detail that may matter
The .cmd in question is not an npm shim (node_modules\.bin\*.cmd), so IS_CMD_SHIM in src/lib/win-exec.ts is false and escapeCmdArg() uses single escaping. The dispatcher body is:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0qodercn-dispatcher.ps1" -- %*Checks
- I searched existing issues and documentation (
cmd.exe,PATHEXT,empty argument,win-exec; nearest are#5093/#6290on%DATE%locale parsing and#2039on port ownership — unrelated). - I removed secrets, tokens, account details, request credentials, and personal data.
Activity
Ingwannu commentedon Oct 22, 2026
Confirmed the current source selection hazard at
bf4eb1209672:whichFromPathtries.cmdbefore.exewithin a Windows PATH directory, andresolveCodingAgentBinaryreturns the first matching profile candidate. Qoder intentionally passes empty values for both--toolsand--setting-sources; preserving these is part of its isolation contract. Your direct-EXE/direct-dispatcher controls also distinguish the reported vendor PowerShell forwarding loss from OpenCodex quoting alone.The repair should preserve PATH/profile ownership: prefer a proven native executable in the same selected command/profile resolution context, and refuse before spawn when a remaining batch dispatcher cannot preserve the required empty arguments. It should not guess installation paths, globally rewrite PATHEXT, or weaken the empty tool/settings isolation. Merely changing
escapeCmdArgcannot certify the downstream PowerShell hop.Keeping this open for a focused resolver/preflight PR with competing PATH directories, same-stem EXE/CMD, CN aliases, explicit selections, cached discovery and a native Windows empty-argv receiver/dispatcher control. No installed CLI, PATH, credentials or live service was changed in this maintenance pass; Linux quoting fixtures alone will not be reported as Windows acceptance.
fix(coding-agent): verify the whole batch dispatcher chain before tru…
fix(windows): prefer real executables over .cmd shims when resolving …
fix(coding-agent): launch native Windows CLIs and refuse empty-arg-dr…
lidge-jun commentedon Oct 23, 2026
Fixed on
devby #7527: on Windows a PATH directory's same-stem.exenow beats its.cmd, and across a profile's candidate names the first native hit wins over an earlier batch dispatcher, so the official Qoder CN layout launchesqoderclicn.exeand keeps--tools ""/--setting-sources "". When a turn must still launch a.cmd/.batand its arguments carry an empty value, the dispatcher chain is inspected and the turn is refused withcli_dispatcher_drops_empty_argsbefore spawn unless the chain is verified to forward%*directly. Native Windows empty-argv behaviour is proven inlane=all. Closing manually because GitHub only auto-closes frommain; it ships in the next release.