By the end of this you will know which of four unrelated faults produced npm ci can only install packages when your package.json and package-lock.json are in sync, from one line of output, without deleting node_modules. Two of those faults were still open npm bugs as of 30 October 2026, and npm install repairs neither.

Short version: the headline sentence carries no information. The line below it does. Missing: @scope/pkg@ with an empty version slot means an unavailable optional dependency that npm deliberately left out of the lockfile (npm/cli#9846, open). Invalid: lock file's A does not satisfy B on npm 10, while your current npm installs happily, means an overrides rule stopped applying (npm/cli#10137, open). The stale lockfile every answer online assumes is one case out of four.

That matters more this quarter than last, because the fourth case behaves backwards from every other npm 12 break. npm 12 requires node ^22.22.2 || ^24.15.0 || >=26.0.0, and with Node 26 reaching Active LTS on 28 October estates are moving runners up now. The upgrade removes the error and leaves the fault in the tree. For the rest of what shifts underneath a green pipeline on this major, see npm 12 Rollout: What Breaks After CI Goes Green.

Prerequisites

  • npm as your CI actually runs it, not as your laptop runs it. Every branch below turns on that gap. Latest at writing is 12.2.1, released 2026-10-14 per the official v12 changelog.
  • jq 1.7 or later, for the lockfile queries.
  • Network access for npx to fetch pinned npm versions (npm@10, npm@12.2.1) on demand.
  • A committed package-lock.json. Readers on lockfileVersion: 1 have the fastest answer and get it in step 5.

Every terminal block quoted below comes from the linked npm issue rather than a local run, and is attributed where it appears.

Step-by-step

1. Print the npm that fails, not the npm you have

npm --version
node --version
node -p process.versions.npm
npx -y npm@12.2.1 --version

node -p process.versions.npm reports the npm bundled with that interpreter, which is what a container image or a setup-node step hands your job. The reporter in npm/cli#10137 hit exactly this split: a lockfile that passed locally with npm 11 but failed in CI on Node 18, 20 and 22, which ship npm 10.

2. Reproduce the failure without deleting node_modules

npm ci --dry-run --ignore-scripts --no-audit --no-fund

npm ci removes node_modules before installing, per the v12 command docs. --dry-run returns the validation verdict and the second line without paying for the rebuild, and it is the invocation the npm/cli#9846 reporter used to show the failure on npm 12.0.2.

3. Read the second line and match it to a cause

From npm/cli#9846, on npm 12.0.2 with Node 24.18.0:

npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and package-lock.json are in sync.
npm error Missing: @mdream/rust-wasm32-wasi@ from lock file

From npm/cli#10137, on npm 10.9.9, in a project npm 11.16.0 installed cleanly:

npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Invalid: lock file's brace-expansion@5.0.9 does not satisfy brace-expansion@5.0.12

The version slot in @mdream/rust-wasm32-wasi@ is empty. An inert optional node has no resolved version to print, so the empty slot is a fingerprint rather than a copy-paste slip. The headline sentences also differ between the two pastes: one carries or npm-shrinkwrap.json. Please update your lock file..., one does not. I could not confirm from the npm source whether that is a v12 wording change or the reporter trimming output, so search both forms when you look for your own case.

Second lineReal causenpm versionsDoes npm install repair it?Tracking
Missing: x@1.2.3 from lock file (version present)package.json edited, lockfile not regeneratedallYes
Missing: @scope/x@ from lock file (version empty)unavailable optional dep kept as an inert node, omitted from the lockfile11.16.0, 12.x (11.13.0 fine)No. Install succeeds, the next ci fails again#9846 / PR #9850, open
Invalid: lock file's A does not satisfy Bone overrides rule written at two scopes stopped applying to a shared hoisted nodefails on npm 10, accepted on 11.16.0+No. npm 11 treats the lockfile as in sync#10137 / PR #10138, open
No second line, or lockfileVersion 1 or 2lockfile generated by a much older npmallYes, regenerate#5125

What I read before trusting the two new rows: both are open issues with unmerged fix PRs, #9850 since 7 August 2026 and #10138 since 16 October 2026, and the v12 docs never print the error string at all. They say only that npm ci "will exit with an error, instead of updating the package lock." An assistant trained before August 2026 answers this question with "delete node_modules and package-lock.json, then run npm install," which reproduces the optional-dependency case and destroys the evidence in the overrides case.

4. Keep the branch in front of you

npm ci --dry-run prints EUSAGESecond line?Edited package.jsonnpm install, commit the lockfileInert optional depnpm/cli#9846, pin npm 11.13 or skip ciOverride droppedregenerate with npm@10jq .lockfileVersionOld lockfile, regenerate on npm 12Open an issue with the dry-run output"Missing: pkg@1.2.3""Missing: pkg@ with no version""Invalid: ... does not satisfy ...""no second line""1 or 2""3"

5. Run the cheap eliminations

jq '.lockfileVersion' package-lock.json
jq -r '.packages | to_entries[] | select(.value.optional == true) | .key' package-lock.json

A lockfileVersion of 1 or 2 is a 30-second answer: regenerate on the npm your CI runs and commit. The second query lists the optional placeholder entries your lockfile still carries. npm 11.13.0 wrote "node_modules/mdream/node_modules/@mdream/rust-wasm32-wasi": { "optional": true }, and per the version table in #9846, 11.16.0 and 12.0.2 stop writing it. An empty result on a project with platform-specific native packages is the shape of the bug. Native optional dependencies that do install still have their own class of failure, covered in Fix NODE_MODULE_VERSION 147.

6. Repurpose npm 10 as a lockfile linter

npx -y npm@10 ci --dry-run --ignore-scripts 2>&1 | grep -E 'Invalid|Missing' || echo "npm 10 agrees it is in sync"

Current npm answers "in sync" on a tree where the override has been dropped. npm 10 still checks it. Per the #10137 reporter, "Running npm install or npm install --package-lock-only with npm 11 doesn't repair the lockfile either; it treats the lockfile as already in sync. Regenerating with npm 10 does fix it." One error code splitting into causes that need opposite responses is a recurring npm 12 shape, and EALLOWGIT covers four of them under a single string.

7. Attempt the repair, then check that npm wrote something

npm install --package-lock-only --ignore-scripts
git diff --stat package-lock.json

An empty diff after a repair that was supposed to fix an out-of-sync lockfile tells you npm considers the file correct already, which puts you in the optional-dependency case or the overrides case.

Verify it works

Verification is a tree assertion, not a green install. Name the overridden package and check what landed:

jq -r '.overrides | keys[]' package.json
npm ls brace-expansion
npm explain brace-expansion

(npm explain is the npm spelling; npm why belongs to pnpm and yarn.) The #10137 reporter's npm 11.16.0 run printed added 25 packages, and audited 26 packages in 406ms, then showed brace-expansion@5.0.9 under minimatch@10.2.6, with the pinned 5.0.12 absent. PR #10138 states the fixed behaviour: "npm ci now fails with Invalid: lock file's brace-expansion@5.0.9 does not satisfy brace-expansion@5.0.12, and npm install updates the tree."

Then the gate, which is what you copy into the job that already runs npm ci. Substitute your own package and pinned version from the jq output above:

set -euo pipefail
npm ci --dry-run --ignore-scripts --no-audit --no-fund
npx -y npm@10 ci --dry-run --ignore-scripts >/tmp/npm10.log 2>&1 || true
grep -E 'Invalid:' /tmp/npm10.log && { echo "override no longer applied"; exit 1; }
npm ls brace-expansion | grep -q 'brace-expansion@5\.0\.9' && { echo "pinned version missing from tree"; exit 1; }
exit 0

The last line asserts on the version that appears when the override is dropped, so a match is the failure condition.

A note on method from my own notes. On 22 October I checked the defensive assertion I was about to recommend against my own box before trusting it, using the decoder's error code on a known-bad input rather than node -v, which distro and vendored builds make a liar in both directions. The probe came back unpatched on a runtime whose version string said otherwise, and I wrote that up in php2shell and Node Brotli RCE. The lockfile deserves the same treatment: it claims to be in sync, and npm ci --dry-run on the npm your CI runs is the probe that answers.

Common pitfalls

rm -rf node_modules package-lock.json && npm install is the wrong first move. In the optional-dependency case it regenerates a lockfile without the placeholder and the next npm ci fails identically. In the overrides case it discards the one artifact recording your pin, and the error disappears for the worst available reason.

An error that vanishes after a Node upgrade is a finding, not a fix. The overrides fault only raises Invalid: on npm 10. Moving runners to Node 22.22+, 24 or 26 moves you to an npm that accepts the stale lockfile, and the override, likely added to force a patched transitive version, stops being in the installed tree.

--omit=optional does not dodge the missing-optional case. The #9846 reporter notes it explicitly, "because lockfile validation happens first."

Dependency bots write the overrides fault for you. The #10137 reporter traces the bad override shape to "an automated dependency update tool, which uses npm 11," and calls the result "quite significant for security." The trigger is one rule written at two scopes reaching the same hoisted node, which per PR #10138 makes findSpecificOverrideSet fall back to a common ancestor that "doesn't carry the nested rule," behaviour introduced by PR #9110, merged 16 March 2026. Which release first shipped #9110 is not stated in the thread; the npm 11.12.0 changelog entry or the git tag would settle it.

Pin npm in CI, through packageManager or an explicit npx npm@x.y.z. It makes the failure deterministic, which is the point of running ci rather than install.

Wrap-up

You now have a discriminator (line two, and the empty version slot), a table matching your output to one of four causes, a --dry-run probe that tests the lockfile without a rebuild, npm 10 serving as an override linter, and a gate that fails on a missing pin instead of a missing package. Wire the gate into the job that already runs npm ci, and add every name from jq -r '.overrides | keys[]' package.json to it.