Skip to content
Latchkey LogoLatchkey home

ERR_PNPM_OUTDATED_LOCKFILE in GitHub Actions

ERR_PNPM_OUTDATED_LOCKFILE in GitHub Actions means pnpm ran a frozen install, compared pnpm-lock.yaml against every package.json it can see, and found a difference it is not allowed to write away. Regenerate the lockfile with the same pnpm version your workflow uses, commit it beside the manifest change, and leave the frozen install in place.

Runner log: pnpm refusing a frozen install and the probe reporting the lockfile unchanged
The recorded run: pnpm names the added dependency, the probe reports pnpm-lock.yaml naming left-pad zero times, and it still names it zero times when the step ends.
Diagram of pnpm comparing a lockfile to every workspace manifest under a frozen install
A frozen install is a comparison, not an installation strategy. Every manifest in the workspace is an input, which is why the failure can name a package you did not touch.

What this error means

The install step fails immediately with ERR_PNPM_OUTDATED_LOCKFILE, and pnpm prints a failure reason that names the importer and the exact change: "specifiers in the lockfile don't match specifiers in package.json", followed by the dependencies that were added, removed or re-pinned. In a workspace the importer is the path of the package whose manifest drifted, which is the single most useful line in the output. Two sibling codes describe the same refusal from different angles: a frozen-lockfile-with-outdated-lockfile variant when the lockfile simply needs updates, and ERR_PNPM_LOCKFILE_CONFIG_MISMATCH when the manifests agree but an install setting recorded in the lockfile no longer matches your configuration. All three are spelled out below. You did not pass a flag to get here; pnpm turns frozen installs on by itself when it detects CI.

Actions log, install step
Error: ERR_PNPM_OUTDATED_LOCKFILE

  × installing dependencies
  ╰─▶ Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up
      to date with package.json.

        Failure reason:
        specifiers in the lockfile don't match specifiers in package.json:
      * in importers["."]:
      * 1 dependency was added: left-pad@1.3.0

Reproduced on a Latchkey runner

Run 2026-09-20·Runner latchkey-small·Exit code 1

Error: ERR_PNPM_OUTDATED_LOCKFILE

  × installing dependencies
  ╰─▶ Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up
      to date with package.json.
      
        Failure reason:
        specifiers in the lockfile don't match specifiers in package.json:
      * in importers["."]:
      * 1 dependency was added: left-pad@1.3.0
  help: Regenerate the lockfile with `pnpm install --lockfile-only` so that
        pnpm-lock.yaml reflects the current package.json, then re-run `pnpm
        install --frozen-lockfile`.

pnpm 12.5.1 / node v20.20.2: pnpm-lock.yaml names left-pad 0 time(s)
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)

The runner diagnosed the failure and did not retry it; this failure needs the fix below.

You did not turn frozen-lockfile on. pnpm did

This is the part that makes the failure confusing: the same command that updates the lockfile on your laptop refuses to on a runner. pnpm's documentation states that when the setting is true, "pnpm doesn't generate a lockfile and fails to install if the lockfile is out of sync with the manifest", and that it "defaults to true in CI environments when a lockfile is present" while defaulting to false everywhere else.

So the workflow that runs a bare install is running a frozen install, and the local command that seemed to prove the lockfile was fine was not the same command. Reproduce it the way CI runs it before you conclude anything.

The importer path in the failure reason is the other half of the diagnosis. importers["."] is the root package. importers["packages/api"] is that workspace member, and it is the manifest to relock against, not the one you edited.

Terminal
CI=1 pnpm install --frozen-lockfile
# the same comparison the runner makes, on your machine

# the three codes, all of them one refusal to write the lockfile
ERR_PNPM_OUTDATED_LOCKFILE
ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH

Common causes

A manifest changed and the lockfile was not regenerated with it

The ordinary case. A dependency was added, bumped or removed in some package.json, and pnpm-lock.yaml was not rebuilt and committed alongside it. The failure reason names the importer and the dependency, so the diff you are missing is spelled out for you.

You relocked and it failed again, because the two pnpm versions disagree

This is why the obvious fix appears to do nothing. pnpm's lockfile format has a version of its own, and majors write different files from the same manifests. Relock with pnpm 10 while the workflow runs pnpm 12 and the freshly committed lockfile is out of date on arrival, with the same error and a diff you cannot explain. Corepack resolves whatever version it defaults to unless something pins it, so "the same pnpm" is a stronger claim than it looks.

A workspace member you did not touch is the one that drifted

A frozen install compares the lockfile against every manifest in the workspace, so a change in any package invalidates the single root lockfile. In our experience this is the version that produces the "I changed nothing" report: someone else's merge added a dependency to another package, and your branch is the one that ran the install next.

An install setting recorded in the lockfile changed

A setting such as auto-install-peers, node-linker or a hoist pattern is baked into pnpm-lock.yaml when it is written. Editing it in .npmrc or package.json without relocking produces ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, which is the same refusal for a different input. An .npmrc present on the runner and absent from the repository does it too.

How to fix it

Relock with the pnpm the workflow uses, and commit it

  1. Pin the version in package.json with packageManager so Corepack activates the same pnpm locally and on the runner.
  2. Run pnpm install --lockfile-only, which updates pnpm-lock.yaml and writes nothing to node_modules.
  3. Commit pnpm-lock.yaml in the same commit as the manifest change, then re-run the job.
Terminal
corepack use pnpm@12.5.1
pnpm install --lockfile-only
git add package.json pnpm-lock.yaml
git commit -m "chore: relock after adding left-pad"

Pin the pnpm version in the workflow, not just in your shell

The packageManager field is the one place both sides read. Corepack honours it, pnpm/action-setup honours it, and pinning it turns a class of unexplainable lockfile diffs into a version bump you can review. Leave the frozen install where it is while you do this.

.github/workflows/ci.yml
jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: pnpm/action-setup@v6     # reads packageManager from package.json
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile

Find the importer the failure names, and relock from the root

In a workspace the fix is still one command at the root, but knowing which member drifted tells you whose change is missing its lockfile and stops you relocking blind. Read the importer path out of the failure reason first.

Terminal
pnpm install --lockfile-only
git diff --stat pnpm-lock.yaml
# then look at who last touched the manifest the error named
git log --oneline -3 -- packages/api/package.json

Commit the settings the lockfile depends on

For a config mismatch, decide which value you want, put it in a committed .npmrc or in the pnpm settings in package.json, relock so the lockfile records it, and commit both. A setting that exists only in the CI environment will keep invalidating a lockfile that was written without it.

.npmrc
# .npmrc, committed
auto-install-peers=true
node-linker=isolated

Use --no-frozen-lockfile to diagnose, never to ship

It will make the job green and it will make the lockfile meaningless: the runner rewrites it, throws the runner away, and your repository keeps the file that failed. Run it once locally if you want to see what pnpm would have changed, then commit that change instead of disabling the check that found it.

Terminal
# diagnostic only, on your machine, then commit the result
pnpm install --no-frozen-lockfile
git diff pnpm-lock.yaml

When the manifests agree and pnpm still refuses

That is the config mismatch, and it is a different problem with a nearly identical message. pnpm records the install settings that influence resolution inside the lockfile, so changing auto-install-peers, node-linker, a hoist pattern or a packageExtensions entry invalidates the file even though no dependency moved. The error names the setting and prints the expected and actual values, which is the fastest read on the page.

The usual source is an .npmrc that is not committed, or one that exists only on the runner. Commit the settings that the lockfile depends on so the file is generated and consumed under the same configuration, then relock once.

Actions log, the sibling failure (the shape pnpm prints)
 ERR_PNPM_LOCKFILE_CONFIG_MISMATCH  Cannot proceed with the frozen installation. The current "settings.autoInstallPeers" configuration doesn't match the value found in the lockfile

What the runner does about it: detected, not repaired

Latchkey's pattern library carries PNPM_FROZEN_LOCKFILE_DRIFT at confidence 0.85, and it is marked shadow_mode: true. A shadow pattern detects and reports and does not yet repair automatically: it records that it would have acted and applies nothing, so it can be measured before anyone trusts it with a live remediation.

The library says why, and the reason is a good one: "the common cause is sometimes a pnpm version mismatch a lockfile regen won't fix, and CI often enables frozen-lockfile implicitly (no flag to rewrite), so we measure before choosing a heal". An automatic relock on a version mismatch would produce a third lockfile that agrees with neither side.

The reproduction above is that promise being kept: the step fails, the sidecar answers, and the probe on the way out reports pnpm-lock.yaml still naming the added dependency zero times.

How to prevent it

  • Pin pnpm with packageManager so the lockfile is written and read by one version.
  • Commit pnpm-lock.yaml with every manifest change, in every workspace member.
  • Commit the .npmrc that carries lockfile-affecting settings, and relock when you change one.
  • Leave the frozen install on in CI. It is the check, not the obstacle.
  • Run CI=1 pnpm install --frozen-lockfile locally before pushing a dependency change, so the comparison happens before the pipeline does.

Frequently asked questions

Why is frozen-lockfile on when my workflow never passes the flag?
pnpm turns it on for you. The documentation states the setting defaults to true in CI environments when a lockfile is present, and to false otherwise, so the identical command behaves differently on a runner. That is the intended design: CI installs what was reviewed, and local installs update it.
The error names a package I never touched. Why?
A frozen install compares the lockfile against every manifest in the workspace, not just the one your branch edited. If another package gained a dependency and the lockfile was not regenerated, the next install anywhere in the repository reports it. The importer path in the failure reason tells you which manifest is actually out of step.
What is the difference between ERR_PNPM_OUTDATED_LOCKFILE and ERR_PNPM_LOCKFILE_CONFIG_MISMATCH?
The first means the dependencies in the lockfile do not match the manifests. The second means the dependencies agree but an install setting recorded inside the lockfile, such as auto-install-peers or node-linker, no longer matches your configuration. Both are refusals to write the lockfile; only the second is fixed by committing a setting rather than a dependency.
Does Latchkey fix this automatically on the runner?
No, and the pattern says so. The detection for pnpm frozen-lockfile drift is in shadow mode: it recognises the failure and records what it would have done, and applies nothing, because the common cause is sometimes a pnpm version mismatch that a lockfile regeneration would not fix. The reproduction on this page shows the lockfile unchanged when the step ends.

Related guides

References

An outdated lockfile is a commit you owe, not a flake. Latchkey reports it instead of installing around it. Start free → 30-day trial · No credit card