Skip to content

fix: cross-spawn for Windows .cmd resolution - #1

Open
NexusProperty wants to merge 1 commit into
josephyaduvanshi:mainfrom
NexusProperty:fix/cross-spawn-windows-cmd
Open

NexusProperty wants to merge 1 commit into
josephyaduvanshi:mainfrom
NexusProperty:fix/cross-spawn-windows-cmd

Conversation

@NexusProperty

Copy link
Copy Markdown

Summary

On Windows + Node ≥18.19/20.10/22, task and rescue modes silently fail with spawn qwen ENOENT while setup --json reports the plugin as ready. Root cause is CVE-2024-27980's mitigation refusing to resolve .cmd shims (every npm-globally-installed CLI) without an explicit shell. Setup probe uses lib/process.mjs::runCommand which already does shell: true on Windows; runQwenTurn uses the streaming spawn directly, so it dies before producing any output.

Empirical repro (this machine, Node v22.14.0 + Windows 10)

> where qwen
C:\Users\Jackc\AppData\Roaming\npm\qwen.cmd

> node ~/.claude/plugins/cache/qwen-companion/qwen/1.1.2/scripts/qwen-companion.mjs setup --json
{ \"ready\": true, \"qwen\": { \"available\": true, \"detail\": \"0.16.1\" } ... }

> node ~/.claude/plugins/cache/qwen-companion/qwen/1.1.2/scripts/qwen-companion.mjs task \"Say hi\"
spawn qwen ENOENT

Fix

Single import change in plugins/qwen/scripts/lib/qwen.mjs:

-import { spawn } from \"node:child_process\";
+import crossSpawn from \"cross-spawn\";
+const spawn = (command, args, options) => crossSpawn(command, args, options);

cross-spawn resolves the .cmd shim to its absolute path and exec's it through Node's spawn primitives, bypassing cmd.exe entirely. No shell-injection risk from user prompts passed as argv to runQwenTurn.

Test plan

  • All runQwenTurn: tests in tests/runtime.test.mjs pass (8/8). These already use installFakeQwen() which writes a qwen.cmd shim on Windows (tests/fake-qwen-fixture.mjs:162-164), so they exercise the streaming-spawn-against-.cmd path this fix targets. Pre-patch they ENOENT'd; post-patch they pass — no new regression test needed.
  • npm audit signatures: 6/6 verified
  • Supply-chain check on cross-spawn@7.0.6: CLEAN (Socket: vulnerability=100, supplyChain=99; single maintainer satazor since 2014; GHSA-3xgq-45jj-v275 ReDoS patched in 7.0.5; no 2026 publish activity = not in current Shai-Hulud / Mini Shai-Hulud waves)

Out of scope (pre-existing, NOT caused by this patch)

Pristine main already has these failures on Windows + Node 22 — confirmed by git stash && node --test tests/process.test.mjs:

  • process.test.mjs: 4 failures (runCommand: captures stdout on success, runCommandChecked: throws on non-zero exit, runCommandChecked: returns result on success, terminateProcessTree: ESRCH on non-existent pid is handled gracefully)
  • tests/stop-gate.test.mjs: 90s timeout

Happy to file a separate issue/PR for these if you'd like — they look like Windows-shell-config-sensitivity in lib/process.mjs where shell: process.env.SHELL || true may pick up git-bash's sh.exe.

Other spawn sites verified immune

  • qwen-companion.mjs:630 spawns process.execPath (node.exe, true executable)
  • stop-review-gate-hook.mjs:106 uses spawnSync(process.execPath, ...)

Version bumped 1.1.2 → 1.1.3 (backwards-compatible bugfix).

Mirrors the canonical fix from sibling plugin cli-companion (commit 5f7d65e, PR #106 there). Same Node 22 + Windows class, identical patch shape.

On Windows + Node ≥18.19/20.10/22, `child_process.spawn("qwen", ...)` with
`shell: false` returns ENOENT against the `.cmd` shim that npm installs for
globally-installed CLIs. This is CVE-2024-27980's mitigation refusing to
resolve `.cmd` files without an explicit shell. The setup probe in
`lib/process.mjs::runCommand` masks the bug — it uses `shell: true` on
Windows — but `runQwenTurn` in `lib/qwen.mjs` uses the streaming `spawn`
directly, so `qwen-companion task` and `qwen-companion rescue` die with
`spawn qwen ENOENT` before producing any output.

Empirically reproducible:

    > where qwen
    C:\Users\Jackc\AppData\Roaming\npm\qwen.cmd

    > node qwen-companion.mjs setup --json
    { "ready": true, "qwen": { "available": true } ... }   # probe lies

    > node qwen-companion.mjs task "Say hi"
    spawn qwen ENOENT

The fix adds `cross-spawn@7.0.6` as the project's first runtime dependency.
It resolves the `.cmd` shim to its absolute path and exec's it through
Node's spawn primitives, bypassing cmd.exe entirely (so no shell-injection
risk from user prompts passed as argv to `runQwenTurn`).

Single patch site:

- `plugins/qwen/scripts/lib/qwen.mjs` — replace `import { spawn } from
  "node:child_process"` with a `crossSpawn` wrapper. All existing
  `spawn(...)` call sites work unchanged.

Other spawn sites verified immune:
- `qwen-companion.mjs:630` spawns `process.execPath` (node.exe, true exe)
- `stop-review-gate-hook.mjs:106` uses `spawnSync(process.execPath, ...)`
- `lib/process.mjs:5` (probe path) uses `shell: win32 ? ... : false`

No new test added — the existing `runQwenTurn:` tests in
`tests/runtime.test.mjs` already use `installFakeQwen()` which writes a
`qwen.cmd` shim on Windows (`fake-qwen-fixture.mjs:162-164`), so those
tests already exercise the streaming-spawn-against-.cmd path this fix
targets. Pre-patch on Windows + Node 22 they ENOENT'd; post-patch all
8 `runQwenTurn:` tests pass.

Version bumped 1.1.2 → 1.1.3 (backwards-compatible bugfix).

Verification (Windows + Node v22.14.0):
- All `runQwenTurn:` tests pass post-patch
- `npm audit signatures`: 6/6 verified
- `cross-spawn@7.0.6` supply-chain CLEAN (Socket vulnerability=100,
  supplyChain=99; single maintainer `satazor` since 2014;
  GHSA-3xgq-45jj-v275 ReDoS patched in 7.0.5; no 2026 publish activity)
- NOT FIXED BY THIS PATCH: 4 pre-existing `process.test.mjs` failures
  (`runCommand`, `runCommandChecked`, `terminateProcessTree: ESRCH`) and
  a 90s timeout in `tests/stop-gate.test.mjs` reproduce on pristine main
  pre-patch — unrelated to spawn ENOENT and out of scope here.

Mirrors the canonical fix shipped in sibling plugin cli-companion
(commit 5f7d65e at NexusProperty/cli-companion, PR #106).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant