Skip to content

[Bug][Windows]: .cmd-resolved CLIs lose empty-string args — qoder setting-sources swallows max-turns, every turn fails with Invalid setting source #7518

Closed

Description

@duanzhijian200443

[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:

  1. Prefer the real executable when both exist. In resolveCodingAgentBinary() / whichFromPath(), if a candidate resolves to a .cmd and a sibling .exe with 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 (no cmd.exe hop at all).
  2. Don't round-trip through cmd.exe when an .exe is available for the same command name, even if the .cmd is what PATH order returned.
  3. 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/#6290 on %DATE% locale parsing and #2039 on port ownership — unrelated).
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. Ingwannu commented on Oct 22, 2026

    @Ingwannu
    Owner

    Confirmed the current source selection hazard at bf4eb1209672: whichFromPath tries .cmd before .exe within a Windows PATH directory, and resolveCodingAgentBinary returns the first matching profile candidate. Qoder intentionally passes empty values for both --tools and --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 escapeCmdArg cannot 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.

  2. added
    bugSomething isn't working
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    toolstool_calls, MCP, web-search / sidecar tools
    on Oct 22, 2026
  3. added a commit that references this issue on Oct 23, 2026
    e46c7cd
  4. lidge-jun commented on Oct 23, 2026

    @lidge-jun
    Owner

    Fixed on dev by #7527: on Windows a PATH directory's same-stem .exe now 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 launches qoderclicn.exe and keeps --tools "" / --setting-sources "". When a turn must still launch a .cmd/.bat and its arguments carry an empty value, the dispatcher chain is inspected and the turn is refused with cli_dispatcher_drops_empty_args before spawn unless the chain is verified to forward %* directly. Native Windows empty-argv behaviour is proven in lane=all. Closing manually because GitHub only auto-closes from main; it ships in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)toolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions