Skip to content

fix(cli): spawn npm without DEP0190 on win32 — pass a joined command line instead of an argv array - #15328

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.52from
Yi-111-a:fix/cli-npm-exec-dep0190-15327
Oct 6, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.52from
Yi-111-a:fix/cli-npm-exec-dep0190-15327

Conversation

@Yi-111-a

@Yi-111-a Yi-111-a commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Summary

On Windows + Node >= 24, omniroute update (and the doctor's version check) print a deprecation warning on every run:

(node:NNNN) [DEP0190] DeprecationWarning: Passing args to a child process with shell
option true can lead to security vulnerabilities, as the arguments are not escaped,
only concatenated.

It is cosmetic, but it shows up on the command whose whole job is diagnosing the install, so it reads like the tool reporting a fault with itself.

Two earlier fixes collided:

The shell is therefore required for the .cmd shim and deprecated alongside an argv array — the argv array #11335 kept passing is the one thing that cannot go through execFile with the shell enabled.

Fix

Node builds that command line itself in this path (${file} ${args.join(" ")}), so the same string is now handed over pre-joined with an empty args array. npmExecInvocation() returns the exact execFile arguments for a platform:

platform file args options
win32 npm.cmd view omniroute version --prefer-online [] { shell: true, windowsHide: true, timeout }
others npm ["view", "omniroute", "version", "--prefer-online"] { shell: false, timeout }

Byte-identical input to the shell, minus the deprecation — the npm.cmd spawn from #11335 is untouched, so the original spawn EINVAL cannot come back.

Hard Rule #13 is now enforced rather than assumed. The shell is handed a raw string, so joining one is only safe while every token is a literal. A token outside a literal grammar (whitespace, a quote, a cmd/POSIX metacharacter, a backslash) throws instead of being concatenated, and the error names escapeWindowsShellArg() from bin/cli/utils/winShellArgs.mjs — the helper #15327 points at — or the environment. A caller that genuinely has a runtime value fails loudly instead of letting the shell reinterpret it.

Off Windows the argv array is returned verbatim and the tokens are not inspected at all, so the platforms that never needed a shell inherit no new restriction.

Scope is the CLI (bin/cli/), as reported. The server-side runNpm() in src/lib/services/installers/utils.ts passes a runtime args array with shell: true too; moving that to a joined string needs its --prefix/version handling reworked first, so it is left alone here rather than half-done.

Related Issues

Validation

  • Change type: CLI
  • Focused tests and category gates from the golden path
  • Reconciled with the current active release base (release/v3.8.52)
  • Production-code changes include a new or updated automated test in this PR

⚠️ base-red inherited: #15306 (release/v3.8.52 is already red before this PR).

Node ≥ 22 is required to observe the deprecation itself, and it is emitted at most once per process, so the regression guard asserts the property Node keys the warning on — no argv array travels with shell: true — rather than trying to capture the warning. Also checked out-of-band that both shapes really spawn and produce identical stdout (echo hello world -> hello world on both paths).

Not run locally: npm run lint, npm run typecheck:core and the full npm run test:unit. This checkout has no node_modules and the disk is at 98%, so the suite could not be installed; CI covers all three.

Tests Added Or Updated

Coverage Notes

bin/ is covered by the two files above. The new export is pure and fully exercised on both platform branches plus its rejection path; the only uncovered line would be the args ?? [] default, which no caller uses.

Reviewer Notes

  • The one deliberate judgement call: npmExecInvocation throws on a non-literal token instead of quoting it. Escaping would need the double-caret rule escapeWindowsShellArg already implements for a different caller, and silently applying it to a value that was never validated would be the more dangerous default. Both current call sites pass literals, so nothing changes for them today — the throw is there so the next caller cannot quietly break Hard Rule deps: bump qs from 6.14.1 to 6.14.2 #13.
  • getLatestVersion() gained an optional second platform parameter (defaults to process.platform) so the win32 path is testable from a non-Windows runner. Existing single-argument callers and tests are unaffected.
  • The two printPostApplyGuidance lines Prettier wants to rewrap are pre-existing violations in the base file and were deliberately left alone to keep this diff to the defect.

`npmExecOptions()` enables the shell on win32 so `npm.cmd` can be spawned at all
(#11335), but `update.mjs` still handed `execFile` an argv array alongside it —
and Node's `normalizeSpawnArguments` emits DEP0190 for exactly that combination
(`options.shell` set AND `args.length > 0`). Node >= 24 therefore printed

  (node:NNNN) [DEP0190] DeprecationWarning: Passing args to a child process with
  shell option true can lead to security vulnerabilities, as the arguments are not
  escaped, only concatenated.

on every `omniroute update` and on the doctor's version check — the command whose
job is diagnosing the install. The two earlier fixes had collided: the shell is
required for the `.cmd` shim and deprecated alongside an argv array.

Node builds that command line itself in this path (`${file} ${args.join(" ")}`), so
the same string is now handed over pre-joined with an EMPTY args array.
`npmExecInvocation()` returns the exact `execFile` arguments for a platform.

Hard Rule #13 now has to be enforced rather than assumed, because the shell is
handed a raw string: a token outside a literal grammar throws instead of being
joined, so a runtime value can never reach the shell's own parser. Off Windows the
argv array is returned verbatim and no new restriction applies.

Fixes #15327
@Yi-111-a
Yi-111-a requested a review from diegosouzapw as a code owner October 2, 2026 04:25
@diegosouzapw
diegosouzapw merged commit 6a8ee68 into cheaper-inference:release/v3.8.52 Oct 6, 2026
3 checks passed
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.

fix(nodejs): Windows npm spawn passes an argv array with shell:true — Node 24 prints DEP0190 on omniroute update/doctor

2 participants