Editorial

Practical guide · CI failures

npm ci: “package.json and package-lock.json are not in sync”

Your build or release job stops at the install step with this error. It means package.json asks for dependency versions that package-lock.json doesn't contain. Here's how to fix it properly, and how to spot the repository setup that keeps causing it.

Published 19 September 2026

npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync. Please update
npm error your lock file with `npm install` before continuing.
npm error
npm error Invalid: lock file's axios@1.13.6 does not satisfy axios@1.20.0
npm error Missing: jscpd@5.0.14 from lock file

What it means

npm ci installs the lockfile exactly, or not at all

npm ci exists for reproducible installs. It deletes node_modules, installs exactly the versions recorded in package-lock.json, and never updates the lockfile. Before it starts, it checks that the lockfile still satisfies every dependency range in package.json.

Each Invalid: line is a package whose locked version is outside the range package.json now asks for. Each Missing: line is a dependency that was added to package.json but never written to the lockfile.

The fix

Regenerate the lockfile and commit it

  1. On a clean checkout of the failing branch, run npm install with the same npm major version your CI uses (npm -v in both places).
  2. Commit the updated package-lock.json. Check that the diff contains the packages named in the error.
  3. Confirm locally with npm ci, or with npm ci --dry-run --ignore-scripts for a quick check without installing.
  4. Keep npm ci in CI. Switching CI to npm install makes the error go away by giving up reproducible builds.

Why it keeps coming back

Most repeat cases have two package managers

A one-off mismatch usually comes from a hand edit to package.json or a merge conflict resolved in package.json but not the lockfile. When the error keeps returning, the usual cause is structural: the repository has two lockfiles.

A typical example: day-to-day work uses Bun or pnpm, so dependency upgrades, whether by people, bots or AI coding tools, update package.json and bun.lock or pnpm-lock.yaml. But a release workflow written earlier still runs npm ci against a package-lock.json that nobody updates any more. Everything looks healthy until the scheduled release runs. Then it fails every time, often for weeks before anyone notices.

Check 1

More than one lockfile in the package? package-lock.json next to bun.lock, pnpm-lock.yaml or yarn.lock.

Check 2

Which manager does each workflow, Dockerfile and deploy config install with? Look for npm ci in jobs that don't match.

Check 3

What does package.json declare? The packageManager field, if present, is the team's stated decision.

If you find two lockfiles, choose one manager. Delete the other lockfile, declare the one you keep in package.json ("packageManager"), and make every install step use it. If Bun or pnpm is the real manager, change the release job to use it instead of npm ci.

Free check

Catch the second lockfile before it breaks a release

CrossCheck is a free, open-source check from zFinia. It flags two package managers configured for the same package, and lists every CI, Docker and Vercel install step with the manager it uses. So “CI tests with Bun but the release runs npm ci” is visible in the pull request, not on release day. It reads configuration files only and doesn't parse lockfile contents, so a hand-edited package.json in an npm-only repository is still npm ci's job to catch.

FAQ

Why does npm install work but npm ci fail?

npm install is allowed to change package-lock.json to match package.json, so it quietly repairs the mismatch on your machine. npm ci never writes the lockfile; it installs exactly what package-lock.json records and stops if that no longer satisfies package.json. That is why the error usually appears only in CI.

Should I change CI from npm ci to npm install?

No. That hides the problem and makes every CI run resolve dependencies afresh, so CI can install something nobody tested. Regenerate and commit package-lock.json instead, and keep npm ci.

Can a repository have both package-lock.json and bun.lock (or pnpm-lock.yaml or yarn.lock)?

Only if different packages deliberately use different managers. For a single package, two lockfiles means two dependency histories. Updates land in one of them, the other falls behind, and any job that installs from the stale one breaks or installs different versions.

How do I stop it coming back?

Pick one package manager, declare it with the packageManager field in package.json, make every CI, release and deploy job install with it, and delete the other lockfiles. A pull-request check such as CrossCheck flags a second lockfile and shows which manager each install step uses.