ENOENT spawning node_modules/.bin/… on Windows
Verified on Windows 11, Node 24.20.0 · OpusMill
Do not spawn the shim by path. Resolve the package's real entry
point and run it with node:
const bin = require.resolve("mocha/bin/mocha.js");
execFile(process.execPath, [bin, ...args], cb);
If you must use the shim, append .cmd on Windows
and pass shell: true — both, not either.
Why it happens
On macOS and Linux, node_modules/.bin/mocha is an
extensionless file with a #!/usr/bin/env node line, and the
kernel knows what to do with it.
On Windows there is no shebang mechanism, so npm writes
different files: mocha.cmd for cmd.exe and
mocha.ps1 for PowerShell. The extensionless
mocha is either absent or is a shell script Windows cannot
execute. Asking for it by that exact path is asking for a file that is
not there, so you get ENOENT.
Reproducing it
execFile("node_modules/.bin/mocha", ["--version"], (e) => {
console.log(e && e.code); -> ENOENT
});
Verified on Windows 11 / Node 24.20.0. Note that unlike
the .cmd EINVAL
case, this one does arrive through the callback
rather than throwing synchronously — so if (error)
return will catch it, and if that is all you do, your program
carries on believing the tool ran.
Why .cmd alone is not enough
The obvious patch is to append the extension on Windows. That gets you
a file that exists, and then it fails differently: since Node 18.20.2 /
20.12.2, spawning a .cmd without shell: true
throws EINVAL (the fix for CVE-2024-27980). So the
half-fix moves you from
ENOENT to
EINVAL.
The one that avoids all of it
If the tool is something you can invoke as a library or a JS file,
require.resolve plus process.execPath sidesteps
shims, extensions and shells entirely, and behaves identically on every
platform. It is also faster, because nothing spawns a shell.
For npm scripts specifically you usually do not need to spawn anything:
"test": "mocha" in package.json already
resolves through node_modules/.bin correctly on every
platform, because npm does this work for you.
Check your own project for the rest of this class of bug.
Paste your code or your package.json into
the browser checker — nothing is uploaded,
it runs on your machine — or run
npx github:Hackierz/winbreak over the whole repository.
--fix repairs the npm scripts that have one obvious
answer and refuses the ones that need a human.
Other errors in the same family:
- The fix for 'NODE_ENV' is not recognized
- The fix for 'rm' is not recognized
- The fix for spawn EINVAL on Windows
- Path too long on Windows
- EPERM on Windows
Background: I scanned the 600 most-downloaded
npm CLI packages — 17.4% have a package.json
script that cannot run on Windows.