# Yarn frozen-lockfile needs update

> Fix yarn frozen-lockfile needs update in GitHub Actions: package.json asks for something yarn.lock does not describe. Regenerate it and commit it.

Source: https://latchkey.dev/learn/failures/yarn-frozen-lockfile-needs-update  
Updated: 2026-09-20

The yarn frozen-lockfile needs update error in GitHub Actions means the committed yarn.lock no longer describes what package.json asks for, and yarn was told never to rewrite it. Regenerate the lockfile on your own machine, commit it alongside the manifest change that caused it, and leave the flag exactly where it is.

## What this error means

An install step that passes locally fails on the runner without touching the network. Yarn 1 prints "error Your lockfile needs to be updated, but yarn was run with `--frozen-lockfile`." and stops during resolution. Yarn 2 and later print the YN0028 code instead, followed by "The lockfile would have been modified by this install, which is explicitly forbidden", and on Yarn 4 it prints the exact diff it would have written to yarn.lock before it refuses. Both exit 1. There is no registry error, no timeout and no missing package: yarn resolved everything it needed and then declined to save the result, because you asked it not to.

```Yarn 1 as intuit/auto#2275 titles it, with the error prefix Yarn 1 prints; Yarn 4 from the recorded run
error Your lockfile needs to be updated, but yarn was run with `--frozen-lockfile`.

➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
➤ YN0000: · Failed with errors in 0s 185ms
```

## Common causes

### package.json changed and yarn.lock did not

The common case by a wide margin. A dependency was added, removed or re-pinned and the install that would have updated the lockfile was never run, or was run and the result never staged. Everything works locally because a local install is free to rewrite the file, which is precisely the behavior the flag exists to prevent on a runner.

### The lockfile was never committed at all

A repository that starts from a template with `yarn.lock` in `.gitignore` fails the first time a workflow runs an immutable install. Yarn Berry reports this as the lockfile "would have been created" rather than modified, which is the same YN0028 code and the same fix.

### A merge resolved the manifest but not the lockfile

Lockfile conflicts are ugly, so they get resolved by taking one side wholesale or by hand-editing. Either produces a file that is internally valid and no longer matches the merged manifest. In our experience this is the version of the failure that costs the most time, because the commit looks deliberate.

### The lockfile was written by a different yarn version

Yarn 1 and Yarn Berry write incompatible formats, and Berry releases change the checksum scheme. A lockfile produced by a version other than the one the runner uses will not validate, which is why pinning `packageManager` is worth doing even when only one person ever runs the install.

## How to fix it

### Regenerate the lockfile locally and commit it

1. Run a normal install on your own machine, with the same yarn version the runner uses.
2. Stage the lockfile together with the manifest change that caused it, in the same commit, so the two never travel separately.
3. Push, and keep the flag in the workflow: it is the check that caught the drift, not the cause of it.

```Terminal
yarn install                 # Yarn 1, or Yarn Berry outside CI
git add package.json yarn.lock
git commit -m "deps: add is-even"
```

### Seed a missing Berry lockfile with --no-immutable

When there is no lockfile yet, an immutable install cannot create one, and on a runner immutable is already the default. Create the file where creating files is allowed, which is your machine or a deliberate one-off step, then commit it and let every later install be immutable again.

```Terminal
npm install -g corepack   # Node 25 and later; bundled from 14.19.0 to 24.x
corepack enable
yarn install --no-immutable   # writes yarn.lock
git add yarn.lock && git commit -m "chore: add yarn.lock"
```

### Resolve lockfile conflicts by regenerating, never by editing

Take the manifest resolution you want, discard both sides of the lockfile, and let yarn write a new one. A hand-merged lockfile can be self-consistent and still describe a tree neither branch asked for, and the frozen install will keep rejecting it.

```Terminal
git checkout --ours package.json     # resolve the manifest deliberately
git checkout HEAD -- yarn.lock       # discard the conflicted lockfile
yarn install
git add package.json yarn.lock
```

### Catch the drift in review instead of on the default branch

Run the immutable install in pull request checks, not only on merge. The failure is deterministic, so it will happen either way, and it is much cheaper on a branch where the person who caused it is already looking.

```.github/workflows/ci.yml
jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: yarn
      - run: yarn install --immutable
```

## How to prevent it

- Commit yarn.lock in the same commit as the manifest change that moved it.
- Pin `packageManager` so the runner and your laptop write the same format.
- Run the immutable install on pull requests, not only after merge.
- Regenerate on conflict; never hand-edit a lockfile.

## Which yarn is talking to you

The two messages read as different problems and are the same one. Yarn 1 speaks about the lockfile needing an update, which sounds like an action you should take on the runner. Yarn 2 and later speak about a modification being forbidden, which sounds like a permissions problem. Neither is true: in both cases resolution finished, the result disagreed with the committed file, and the flag said do not save.

The distinction that matters is which flag you are actually running under. On Yarn 1 it is `--frozen-lockfile`, and nothing turns it on for you. On Yarn 2 and later it is `--immutable`, and the official documentation says it "defaults to true on CI", so every install on a runner is immutable whether or not the flag appears in your workflow. That is why a Berry project can fail on a runner with a workflow that never mentions the flag at all.

```Terminal
# Yarn 1: opt in
yarn install --frozen-lockfile

# Yarn 2, 3, 4: already on, because CI is set
yarn install
yarn install --immutable        # the same thing, written down
yarn install --no-immutable    # the escape hatch, for seeding a lockfile locally
```

## What the reproduction changed, and what it did not

The script behind the log above builds two throwaway packages, one per yarn generation. Each one installs once so the manifest and the lockfile agree, then adds a single dependency to package.json and touches nothing else. That is the entire failure, and it is worth seeing how small it is: no version range was widened, no registry was swapped, no workspace moved.

The fixture is deleted and rebuilt at the top of the script, so a second attempt starts from the same clean state and produces the same two refusals rather than inheriting the first attempt's lockfile. Our recorded run exited 1 on a Latchkey `latchkey-small` runner on 20 September 2026.

One detail from that run is worth keeping. Seeding the Berry lockfile needed `--no-immutable`, because the runner sets CI and Yarn Berry treats that as immutable by default. A plain `yarn install` on a runner cannot create the file it is being asked to create, which is a surprising amount of the confusion around this error.

## If the lockfile looks committed and it still fails

Check that the lockfile in the commit is the one your install produced. A `.gitignore` inherited from a template, a partial `git add`, or a lockfile written by a different major version of yarn all present as a file that exists and does not match.

Check the yarn version too. Yarn 1 and Yarn Berry use different lockfile formats, and a repository that pins `packageManager` will use that version on the runner regardless of what your shell resolves locally. An install run under one version and validated under another will disagree every time.

Workspaces add one more source. A change inside any workspace package's manifest is a change to the resolution the root lockfile records, so a dependency bump three directories down produces this failure at the repository root.

```Terminal
git check-ignore -v yarn.lock          # is it ignored?
git log --oneline -1 -- yarn.lock       # when did it last move?
node -p "require('./package.json').packageManager"
```

## FAQ

### Why does yarn say the lockfile needs updating when nobody touched it?

Because the manifest moved instead. A dependency bump anywhere in the repository, including inside a workspace package, changes the resolution the root lockfile records. A different yarn version does it too: a lockfile written by Yarn 1 will not satisfy a Yarn Berry install, and the reverse is also true.

### Should I just drop --frozen-lockfile in CI?

No. Dropping it lets the runner install a dependency tree nobody reviewed, and the difference between what you tested and what you shipped stops being visible. Keep the flag and fix the lockfile: that takes one local install and one commit, and it is the only outcome where the build you ship is the build you tested.

### What is the difference between --frozen-lockfile and --immutable?

They are the same intent in two generations of yarn. Yarn 1 documents `--frozen-lockfile` as "Don't generate a yarn.lock lockfile and fail if an update is needed". Yarn 2 and later replaced it with `--immutable`, which the documentation says "defaults to true on CI", so a Berry install on a runner is immutable even when the flag is not written down.

### Does re-running the job ever fix this?

No. The lockfile in the commit is the same lockfile on the next attempt, so the second run reaches the same conclusion and bills the same minutes. The failure is deterministic, which makes a rerun a way to confirm that rather than a fix.

## References

- [Yarn 1: yarn install and --frozen-lockfile](https://classic.yarnpkg.com/en/docs/cli/install)
- [Yarn: yarn install and --immutable](https://yarnpkg.com/cli/install)
- [Yarn error codes: YN0028 FROZEN_LOCKFILE_EXCEPTION](https://yarnpkg.com/advanced/error-codes)
- [intuit/auto issue 2275: the same message in a real workflow](https://github.com/intuit/auto/issues/2275)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
