bare npm is ENOENT and npm.cmd is EINVAL - the same tool, two different lies
aliases · case
Symptom
Section titled “Symptom”Any Node program that spawns a package-manager CLI dies on Windows, and the two obvious spellings fail with two different errors, which sends you down two different wrong paths:
spawnSync npm ENOENTspawnSync npm.cmd EINVALENOENT reads as “npm is not installed” and EINVAL reads as “bad arguments”.
Neither is true. npm works fine from the same shell.
const { spawnSync } = require("node:child_process");spawnSync("npm", ["--version"], { encoding: "utf8" }).error.code; // ENOENTspawnSync("npm.cmd", ["--version"], { encoding: "utf8" }).error.code; // EINVALVerified on Windows 11, Node 22.14 and 24.19.
Two unrelated facts stacked:
- There is no file named
npmon disk. PATHEXT resolution is a shell behavior, andspawnSyncwithoutshell: truedoes not perform it, so the bare name genuinely does not exist ->ENOENT. npm.cmddoes exist, but Node refuses to spawn.cmd/.batshell-less after the CVE-2024-27980 hardening ->EINVAL.
So the correct-looking fix for the first error walks straight into the second.
Why shell: true is not the answer
Section titled “Why shell: true is not the answer”It is the first thing everyone reaches for, and it is a security regression when
the command is user-supplied: Node does not escape cmd metacharacters in that
mode, so an argument containing & or ^ becomes command injection. This repo’s
own oss-native-arg-quoting case is the same wound from the other side.
Workaround
Section titled “Workaround”Resolve the command yourself, then route by extension:
// PATH x PATHEXT walk -> absolute path// .exe -> spawn directly// .cmd/.bat -> cmd.exe /d /s /c "<caret-escaped line>" with// windowsVerbatimArguments: truePrefer .exe over .cmd, and never .ps1 (see npm-ps1-not-comspec).
npm-ps1-not-comspec covers the .ps1 shim winning the PATH race. This is the
adjacent trap: the .cmd shim is the one you are told to prefer, and Node
refuses it too. Worth its own entry because the fix is different (an escaped
ComSpec hop, not a preference reorder) and the error pair is what people search.
Real-world hit
Section titled “Real-world hit”lidge-jun/codexclaw#40 — cxc receipt test -- npm test could not run on Windows,
which blocked the documented path to close a work phase.
Fix: https://github.com/lidge-jun/codexclaw/commit/5c03acb