npm ci fails with "package.json and package-lock.json are not in sync" when the lockfile cannot satisfy what package.json declares, or when it was written by a different npm version than the one running in CI. The fix is almost always to regenerate the lockfile with the same npm version CI uses, commit it, and pin that version so it cannot drift again. This post covers the four real causes, a diagnostic order, and how npm, pnpm and Yarn differ in enforcing lockfiles (as of October 2026).
What does the error actually mean?
The message looks like this:
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 Missing: zod@3.23.8 from lock file
npm error Invalid: lock file's esbuild@0.21.5 does not satisfy esbuild@0.24.0
Unlike npm install, npm ci never edits the lockfile. It deletes node_modules, compares the lockfile against package.json, and refuses to proceed on any mismatch. That strictness is the feature: CI should fail loudly instead of quietly resolving new versions.
The takeaway: this error is npm ci doing its job, so the real question is why the two files disagree.
Why do the two files disagree?
In rough order of how often I see them:
-
Someone edited
package.jsonby hand and never rannpm install, so the lockfile has no entry for the new range. - Merge conflicts resolved badly. Two branches both changed dependencies, and the lockfile conflict was fixed by picking one side.
-
Different npm versions. Older npm majors write older
lockfileVersionformats and resolve peer dependencies differently. A laptop on one major and CI on another can each consider the other's lockfile incomplete. -
Platform-specific optional dependencies. Packages such as esbuild, Rollup, SWC and Lightning CSS ship native binaries as optional dependencies per OS and CPU. A lockfile generated on macOS can lack the Linux entries, producing errors like
Cannot find module @rollup/rollup-linux-x64-gnuin CI.
The takeaway: three of the four causes are human workflow problems, and only the optional-dependency one is a genuine tooling quirk.
How do I diagnose it in the right order?
Start by making the failure reproducible locally with the exact versions CI uses:
# Match CI's Node/npm first
node -v && npm -v
# Reproduce in a clean clone, not your working directory
git clone . /tmp/repro && cd /tmp/repro
npm ci
If it fails in a clean clone, the committed lockfile is the problem. If it passes locally but fails in CI, the npm versions differ. Compare the lockfileVersion field at the top of package-lock.json against what CI's npm would write.
To fix cases 1 to 3:
# Use the same npm major as CI
npm install # reconciles the lockfile with package.json
git add package-lock.json
npm ci # must pass before you push
For case 4, a cleanly regenerated lockfile on a recent npm often includes the other platforms' optional entries. If it does not, delete node_modules and package-lock.json once, reinstall, and verify with npm ci inside a Linux container that matches CI. Dead ends I wasted time on: npm ci --force and --legacy-peer-deps do not repair a stale lockfile. They change resolution rules and can hide the drift rather than remove it.
The takeaway: reproduce in a clean clone with CI's exact npm version before touching the lockfile.
How do I stop it from coming back?
Pin the tool version in the repository so laptops and CI cannot disagree:
{
"packageManager": "npm@10.9.0",
"engines": { "node": ">=20", "npm": ">=10" }
}
The packageManager field is read by Corepack, and engines only warns by default unless engine-strict is set in .npmrc. Then add a cheap guard job that fails pull requests for drift before the slow pipeline starts:
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci --ignore-scripts
- run: git diff --exit-code package-lock.json
The last line catches the case where install silently rewrote the lockfile locally but nobody committed it.
The takeaway: a pinned package manager version removes the most common cause, and git diff --exit-code on the lockfile catches the rest.
How do npm, pnpm and Yarn enforce lockfiles?
| Strict CI command | Behavior on mismatch | Notable limitation | |
|---|---|---|---|
| npm | npm ci |
Always fails, never writes lockfile | Output can be verbose; sensitive to npm version differences |
| pnpm | pnpm install --frozen-lockfile |
Fails; this is the default when pnpm detects a CI environment | Different node_modules layout can expose code relying on undeclared dependencies |
| Yarn (Berry) | yarn install --immutable |
Fails if the lockfile would change | Migration from classic Yarn changes config and plugins |
| Yarn (classic) | yarn install --frozen-lockfile |
Fails | Classic is in maintenance mode |
If you want strict lockfile enforcement without remembering a flag, pnpm is the one that turns it on by default in CI. Its real cost is that its non-flat node_modules will surface imports of packages you never declared, which means a migration day of fixing those. If you stay on npm, npm ci is already strict enough; your gap is version pinning, not the tool. Yarn Berry's --immutable is equally strict, but expect to learn its configuration model.
The takeaway: all three package managers can fail CI on drift, so switching tools matters less than pinning the tool version and running the strict command.
FAQ
Why does npm ci fail but npm install works?
npm install updates the lockfile to match package.json, hiding the mismatch, while npm ci refuses to modify it and fails instead. Run npm install, commit the updated lockfile, and npm ci will pass.
Should I delete package-lock.json to fix npm ci?
Only as a last resort, because regenerating from scratch re-resolves every dependency to the newest version its range allows. Try npm install with the CI-matching npm version first, which keeps existing resolutions where possible.
Why does npm ci fail on Linux with a missing @rollup or esbuild binary?
The lockfile was generated on another OS and lacks the Linux optional dependency entries. Regenerate the lockfile with a recent npm and verify with npm ci in a Linux container.
Bottom line
If you use npm, pin the npm version with packageManager and make npm ci plus a lockfile diff check your first CI step. If drift keeps happening on a team with mixed machines, pnpm's default frozen-lockfile behavior in CI removes one class of mistakes, at the price of a migration. Yarn Berry fits teams already invested in it. Whichever you pick, never paper over this error with --force.
Top comments (1)
The guard job running git diff --exit-code on the lockfile before the slow pipeline starts is the part most teams skip, and it is the one that would have caught most of our failures. I would add that the packageManager pinning only helps if developers actually run corepack enable locally, which many teams never enforce.