TypeScript

Fix ERR spawn EINVAL from npx install-peerdeps on Windows

Why a lint script that adds Airbnb ESLint config with Yarn dies on Windows with ERR spawn EINVAL, the one-word change that makes it run, and what the fixed release does to your version ranges.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

On Windows, ERR spawn EINVAL from an npm script that runs npx install-peerdeps is not a Yarn problem. install-peerdeps 3.0.3 starts npm.cmd or yarn.cmd through child_process.spawn() without the shell option, and since the April 2024 Node.js security releases Node refuses to do that and raises EINVAL. install-peerdeps prints the error message after its red ERR label.

The fix is one change in the script: name a fixed release, npx install-peerdeps@3.0.7. Release 3.0.4 and later pass shell on Windows. Or drop the tool, because npm 7 and later install peer dependencies on their own.

The report comes from a Stack Overflow question about a create-react-app project on Windows 11, Node.js v20.12.2 and npm 10.5.0. Its lint script chained yarn add calls with npx install-peerdeps --dev eslint-config-airbnb. Answering y to the "use Yarn?" prompt ended the run with ERR spawn EINVAL. The same failure was reproduced on 30 October 2026 directly on a Windows 11 host, not in a container: Node.js v26.7.0, npm 11.19.0, and Yarn 1.22.22 installed locally in the test project. The script was trimmed to the failing command, with a yarn.lock present and y piped into the prompt:

text
> app2@1.0.0 lint
> npx install-peerdeps@3.0.3 --dev eslint-config-airbnb
 
install-peerdeps v3.0.3
It seems as if you are using Yarn. Would you like to use Yarn for the installation? (y/n) ERR spawn EINVAL
exit: 1

The prompt is install-peerdeps' own, not Yarn's. Its 3.0.3 CLI checks for a yarn.lock in the current folder and offers to switch, and the yarn add calls earlier in the script leave one in the project. Answering n, or having no yarn.lock at all, does not avoid the error: the CLI then keeps npm as the package manager, and the reproduction without a yarn.lock failed with the same ERR spawn EINVAL on npm.cmd.

Why it happens: Node.js refuses .cmd files without a shell#

The cause is CVE-2024-27980. Batch files on Windows do not parse arguments the way executables do, so an argument passed to spawn() could inject a command even without a shell. The Node.js security release post states the consequence:

Node.js will now error with EINVAL if a .bat or .cmd file is passed to child_process.spawn and child_process.spawnSync without the shell option set.

The fix shipped on 10 April 2024 in Node.js 18.20.2, 20.12.2 and 21.7.3. The asker's v20.12.2 is the very first patched release of the 20.x line. In July 2024, 18.20.4 and 20.15.1 also fixed CVE-2024-36138, listed as a bypass of the incomplete CVE-2024-27980 fix.

On the install-peerdeps side, the 3.0.3 source appends .cmd to the command name on Windows and calls spawn(command + extra, args, { cwd: process.cwd() }), with no shell. That .cmd suffix used to be the standard workaround for spawning npm on Windows. Since the security fix, it is exactly the call Node rejects. The tool catches the error and prints ERR followed by err.message, which is the literal text spawn EINVAL.

The behaviour fits in a ten-line script. spawn-test.js calls spawn('npm.cmd', ['--version']) with no options. On Node.js 26.7.0 it threw synchronously, before any error event fired:

text
node v26.7.0 win32 mode: plain
thrown: EINVAL spawn EINVAL

That is the whole mechanism. Yarn, create-react-app, Prettier and the global Yarn install that the asker tried play no part.

Fix: run a release of install-peerdeps that passes shell#

The bug was reported in install-peerdeps issue #252, opened in February 2025 and closed by the maintainer in March 2025. The source published as 3.0.4 on npm adds shell: isWindows to the spawn() call, with a comment that links the Node.js advisory. At the time of writing, the latest tag on npm is 3.0.7.

  1. Pin the tool to a fixed release in the script. An exact version tells npx which release to run, whereas @latest is a dist-tag that moves with every publish:
json
{
  "scripts": {
    "lint": "yarn add -D prettier && yarn add -D babel-eslint && npx install-peerdeps@3.0.7 --dev eslint-config-airbnb && yarn add -D eslint-config-prettier eslint-plugin-prettier"
  }
}
  1. If you call it by hand in a Yarn project, pass --yarn: the reproduction ran this command with no prompt, and the install-peerdeps CLI source defines the option as "Install with Yarn":
bash
npx install-peerdeps@3.0.7 --dev --yarn eslint-config-airbnb
  1. Run the script again:
bash
npm run lint

In the reproduction, the only change between the failing run and this one was the version in scripts.lint, from 3.0.3 to 3.0.7. Everything else stayed the same: the same project, the same yarn.lock, and the same y answer:

text
install-peerdeps v3.0.7
It seems as if you are using Yarn. Would you like to use Yarn for the installation? (y/n) (node:42372) [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.
(Use `node --trace-deprecation ...` to show where the warning was created)
Installing peerdeps for eslint-config-airbnb@latest.
yarn add eslint-config-airbnb@19.0.4 eslint@^8.2.0 eslint-plugin-react@^7.28.0 eslint-plugin-import@^2.25.3 eslint-plugin-jsx-a11y@^6.5.1 eslint-plugin-react-hooks@^4.3.0 --dev
 
SUCCESS eslint-config-airbnb and its peerDeps were installed successfully.
exit: 0

On the npm path, with no yarn.lock, 3.0.3 failed with the same ERR spawn EINVAL and 3.0.4 succeeded. That run confirms 3.0.4 is enough on the npm path.

Fix without install-peerdeps: let npm install the peers#

The tool exists because older npm never installed peer dependencies. The npm package.json documentation says that "In npm versions 3 through 6, peerDependencies were not automatically installed", and then: "As of npm v7, peerDependencies are installed by default." The page on npm's "requires a peer of X but none is installed" warning covers that older behaviour.

On npm 7 or later, a plain install avoids spawn() entirely:

bash
npm install -D eslint-config-airbnb

There is one difference worth knowing. In the reproduction, npm put the peers in the tree, with eslint@8.57.1 under eslint-config-airbnb@19.0.4, but package.json gained a single entry:

text
{
  "eslint-config-airbnb": "^19.0.4"
}

install-peerdeps writes every peer into devDependencies. npm installs the peers but records only the package you named. If your ESLint config or editor relies on eslint being a direct dependency, add it explicitly.

Verify the fix#

  • npm run lint ends with the SUCCESS ... and its peerDeps were installed successfully. line and exit code 0.
  • npm pkg get devDependencies lists eslint, eslint-plugin-import, eslint-plugin-jsx-a11y, eslint-plugin-react and eslint-plugin-react-hooks next to eslint-config-airbnb.
  • The first line of the run names the release npx fetched: install-peerdeps v3.0.7 in the reproduction, never v3.0.3.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

On Windows, the fixed release drops the caret from the ranges#

The fixed releases come with a side effect that shows up in the reproduction above. install-peerdeps 3.0.7 asked Yarn for eslint@^8.2.0, yet package.json ended up with exact versions, and the installed ESLint was the floor of the range:

text
$ npm pkg get devDependencies
{
  "eslint": "8.2.0",
  "eslint-config-airbnb": "19.0.4",
  "eslint-plugin-import": "2.25.3",
  "eslint-plugin-jsx-a11y": "6.5.1",
  "eslint-plugin-react": "7.28.0",
  "eslint-plugin-react-hooks": "4.3.0",
  "yarn": "^1.22.22"
}

With shell: true, Node joins the arguments into one command line and hands it to cmd.exe, for which ^ is the escape character. A one-line test, caret.js, passed eslint@^8.2.0 to a child process with shell: true and printed what arrived:

text
argument received by the child: eslint@8.2.0

Node's own deprecation notice, DEP0190, describes this: with the shell option, "the values are not escaped, only space-separated". It became a runtime warning in Node.js v24.0.0, which is why the [DEP0190] DeprecationWarning line appears in the successful run. Through npm, 3.0.4 and 3.0.7 also installed ESLint 8.2.0, but npm saved the range as ^8.2.0. Both cases can be repaired, and both repairs were reproduced in fresh projects on the same host.

On the Yarn path the saved range is the exact 8.2.0, so yarn upgrade eslint stays on 8.2.0 (the reproduction confirmed it). Re-add the package with the caret in quotes, which rewrites the range and moves the install:

bash
yarn add -D "eslint@^8.2.0"
text
$ npm pkg get devDependencies.eslint
"^8.2.0"
$ node -p "require('eslint/package.json').version"
8.57.1

The quotes matter in cmd.exe too: typed there as "eslint@^8.2.0", the caret reached the program intact in the reproduction. Repeat the same command for each plugin whose range lost its caret. On the npm path the range is already ^8.2.0, so npm update eslint is enough: it moved the reproduction from 8.2.0 to 8.57.1. The plain npm install route keeps npm's own resolution and is not affected.

If the EINVAL comes from your own spawn() call#

The same spawn EINVAL shows up in any Node script that spawns npm.cmd, yarn.cmd, npx.cmd or a .bat file. The Node.js child_process documentation lists three ways to run them on Windows: spawn() with the shell option ("not recommended", it says, pointing to DEP0190), child_process.exec(), or spawning cmd.exe with the batch file as an argument. The last form worked in the reproduction with no deprecation warning:

js
const { spawn } = require('node:child_process');
 
const child = spawn('cmd.exe', ['/d', '/s', '/c', 'npm.cmd --version']);
child.stdout.on('data', (d) => process.stdout.write('stdout: ' + d));
child.on('exit', (code) => console.log('exit code:', code));
text
node v26.7.0 win32 mode: cmd
stdout: 11.19.0
exit code: 0

Whichever form you choose, the command line now goes through cmd.exe, so only pass it values you control. That is the injection the security fix closed.

Two remedies from the original thread do not hold up today. The --security-revert=CVE-2024-27980 switch is still listed in the v20.x branch's node_revert.h, and the security post "strongly advise[s] against" it. On Node.js 26.7.0 it does not even start:

text
C:\Program Files\nodejs\node.exe: Error: Attempt to revert an unknown CVE [CVE-2024-27980]
exit: 12

The second answer on Stack Overflow, "switching back to nodejs 18 LTS version", works only on an 18.x older than 18.20.2, because that release has the same change. If you run several Node versions on Windows anyway, the page on nvm-windows "exit status 1: Access is denied" explains why switching needs an elevated shell or Developer Mode.

The error after this one: ESLint 9 already in the project#

Once the spawn error is gone, the next failure comes from the peer range, not from Windows. eslint-config-airbnb 19.0.4 declares eslint as ^7.32.0 || ^8.2.0. In a project that already had ESLint 9, installing the Airbnb config with npm failed on the peer range. These are the key lines of that output:

text
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error Found: eslint@9.39.5
npm error peer eslint@"^7.32.0 || ^8.2.0" from eslint-config-airbnb@19.0.4

That is a dependency conflict to resolve on purpose, not something to silence with --legacy-peer-deps. Choose ESLint 8 for the Airbnb config, or a config that supports ESLint 9. A worked case of the same ERESOLVE report, where a Firebase release candidate fails a stable-only peer range, shows how to pick compatible versions instead of bypassing the peer check. For a TypeScript project, the page on ESLint's "The keyword enum is reserved" error explains why eslint-config-airbnb 19.0.4 keeps you on ESLint 7 or 8 and which parser setting that format needs.

Most of what the asker tried could not have helped: reinstalling Yarn globally, checking environment variables, looking through the create-react-app configuration. The error comes from one spawn() call inside a helper fetched by npx, and Node has refused that call on Windows since April 2024. A version tag on the npx command fixes it. Dropping the helper for npm's own peer installation avoids both the error and the caret side effect on Windows.

Further reading: Fix Failed to replace env in config: NPM_TOKEN.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote