Fixing Yarn 'The Lockfile Would Have Been Modified' (YN0028)
Yarn Berry (Yarn 2 and later) turns on immutable installs automatically in CI. If running the install would change yarn.lock — because a manifest changed, a resolution was added, or the lockfile was written by a different Yarn version — the install fails with YN0028 rather than rewriting the file inside the runner. This guide explains what Yarn compares, how to read the diff it prints, and how to fix the lockfile without disabling the protection.
Exact symptoms and error messages
The failure appears at the end of the resolution step, with a diff of the lines Yarn would have changed:
➤ YN0000: ┌ Resolution step
➤ YN0085: │ + zod@npm:3.24.1
➤ YN0085: │ - zod@npm:3.23.8
➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
➤ YN0000: └ Completed in 1s 204ms
➤ YN0000: Failed with errors in 1s 207ms
With more verbose output, Yarn prints the actual lockfile diff:
➤ YN0028: │ -"zod@npm:^3.23.0":
➤ YN0028: │ +"zod@npm:^3.24.0":
➤ YN0028: │ version: 3.24.1
➤ YN0028: │ - resolution: "zod@npm:3.23.8"
➤ YN0028: │ + resolution: "zod@npm:3.24.1"
A variant appears when the lockfile's metadata version differs from what the running Yarn writes:
➤ YN0028: │ - version: 6
➤ YN0028: │ + version: 8
➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
Root cause analysis
Yarn Berry resolves the project from the manifests and .yarnrc.yml, then compares the lockfile it would write with the one on disk. With enableImmutableInstalls true — the default whenever Yarn detects CI — any difference is an error. The lockfile is also normalised: Yarn records a __metadata.version and a cacheKey, and a different Yarn release can produce a different value for either, which counts as a modification. The policy behind immutable installs is covered in Lockfile Management Strategies.
Common triggers:
- A manifest change without
yarn install— a new dependency or range edited by hand or by a bot that did not run Yarn. - A different Yarn version. Projects pin Yarn through
packageManagerand, commonly, a checked-in release under.yarn/releases/referenced byyarnPath. A developer or runner using a global Yarn of another version writes different metadata. - Configuration changes. Editing
resolutions,packageExtensionsin.yarnrc.yml, or protocol settings changes resolution without touching dependency ranges. - Plugins or patches. A new
patch:protocol entry, or a Yarn plugin that changes resolution, needs a regenerated lockfile. - Cross-platform checksum differences when
checksumBehaviorand cache settings differ between machines, occasionally producing modified checksums.
Resolution and configuration patch
1. Regenerate with the project's Yarn
corepack enable
yarn --version # must match "packageManager" in package.json
yarn install # rewrites yarn.lock as needed
git diff yarn.lock | head -40 # review
yarn install --immutable # reproduce the CI check locally
git add yarn.lock && git commit -m "Update yarn.lock"
2. Pin Yarn once for everyone
{
"packageManager": "yarn@4.6.0"
}
# .yarnrc.yml
nodeLinker: node-modules
enableImmutableInstalls: true # explicit, so local installs in CI mode behave the same
Running yarn set version 4.6.0 updates both packageManager and, if you use it, the checked-in release. With Corepack enabled, every machine runs the pinned version automatically — see Pinning the Package Manager with Corepack.
3. Upgrading Yarn deliberately
When you move to a new Yarn major, the lockfile metadata version changes. Do the upgrade in its own pull request: yarn set version stable, yarn install, commit package.json, .yarnrc.yml, .yarn/releases (if used) and yarn.lock together. Every open branch then rebases onto a single, reviewed format change.
Do not turn off immutability in CI
yarn install --no-immutable or YARN_ENABLE_IMMUTABLE_INSTALLS=false in CI makes the pipeline pass by resolving new versions inside the runner, which means the tested tree is not the reviewed one. Keep the default and fix the lockfile in the branch.
Resolutions, protocols and patches
Yarn Berry records more than version ranges in yarn.lock, which gives it more ways to drift. Each lockfile key includes the protocol used to fetch the package — npm:, workspace:, patch:, portal:, link: or git URLs — and the resolution records the exact source. Changing how a dependency is fetched, even at the same version, changes the lockfile.
The resolutions field in the root package.json is Yarn's equivalent of npm overrides. Adding or editing an entry forces every matching descriptor to a specific version, which rewrites every lockfile entry that depended on it:
{
"resolutions": {
"semver": "7.6.3",
"@acme/ui/clsx": "2.1.1"
}
}
The patch: protocol, created by yarn patch and yarn patch-commit, stores a reference to a patch file plus a checksum in the lockfile. Editing the patch file without reinstalling changes the checksum Yarn computes, so the next immutable install reports YN0028 with a patch: line in the diff.
packageExtensions in .yarnrc.yml adds missing dependencies or peers to third-party packages. Adding an extension changes the dependency set of the extended package and therefore its lockfile entry. In all three cases the fix is the same local yarn install, and the lesson is the same: resolution-affecting configuration and the lockfile must be committed together.
Yarn Classic projects
Teams still on Yarn 1 see a different message for the same situation: error Your lockfile needs to be updated, but yarn was run with --frozen-lockfile. Yarn Classic does not enable frozen installs automatically in CI, so the flag has to be passed explicitly, and many older pipelines never did — which means their CI has been silently rewriting yarn.lock for years. When migrating such a project to Yarn Berry, expect a burst of YN0028 failures the first week: they are drift that was always there, now made visible. Treat each one as a missing lockfile commit rather than a Yarn bug, and consider whether the migration is a good moment to move to pnpm instead, as covered in Migrating from Yarn 1 to pnpm Workspaces.
Worked example: a Renovate update that never goes green
Renovate opens a pull request bumping zod in packages/schema/package.json. The pull request includes a yarn.lock change, yet CI fails with YN0028 showing version: 6 changing to version: 8. Renovate ran a different Yarn than the one the project pins, because the repository did not declare packageManager and relied on a checked-in yarnPath that Renovate's environment ignored. Adding "packageManager": "yarn@4.6.0" to the root manifest lets Renovate and Corepack pick the same version, and the next update pull request passes. Renovate configuration for monorepos is covered in Configuring Renovate for Grouped Updates in a Monorepo.
Zero-installs and the offline cache
Some Yarn Berry projects commit .yarn/cache (the zip archives of every package) so that a clone can run without downloading anything — "zero-installs". In that setup YN0028 has a sibling: if the cache contains archives for versions the lockfile no longer references, or lacks archives for new ones, yarn install --immutable --immutable-cache fails with YN0056 or similar cache errors. The fix is the same pattern: run yarn install locally, commit the updated lockfile and the changed cache files together, and let CI verify with the immutable flags. If the repository does not use zero-installs, make sure .yarn/cache is git-ignored so nobody commits a partial cache by accident.
CLI validation and debug commands
# Reproduce the CI behaviour exactly
yarn install --immutable
# See why a package resolved the way it did
yarn why zod
# Confirm which Yarn is running and what the project pins
yarn --version
node -p "require('./package.json').packageManager"
# Show effective configuration that affects resolution
yarn config get enableImmutableInstalls
yarn config get nodeLinker
Prevention and CI/CD guardrails
- Declare
packageManagerso every environment — laptops, CI, bots — runs the same Yarn. - Run
yarn install --immutableas an early pull-request check so drift is caught before review. - Upgrade Yarn in dedicated pull requests that touch nothing else.
- Commit
.yarnrc.ymlchanges with the lockfile, since configuration affects resolution.
Frequently Asked Questions
Why does yarn install succeed locally but fail in CI?
Outside CI, immutable installs are off by default, so Yarn silently updates the lockfile on your machine. If that change is not committed, CI sees the difference. Run yarn install --immutable locally before pushing.
Does YN0028 apply to Yarn 1 (Classic)?
No. Yarn Classic uses yarn install --frozen-lockfile, which fails with "Your lockfile needs to be updated". The fix is the same: install locally with the right version and commit yarn.lock.
Can I update the lockfile without installing packages?
Yes. yarn install --mode=update-lockfile resolves and writes yarn.lock without fetching or linking packages, which is useful in automation.
Why does the diff only show metadata lines?
Because the dependency set is unchanged and only the Yarn version differs. Align the Yarn version with packageManager; do not commit a lockfile rewritten by the wrong version.
Related
- Lockfile Management Strategies sets the overall lockfile policy.
- Setting Up Yarn Berry Workspaces with the node-modules Linker configures the workspace this error usually appears in.
- Fixing pnpm ERR_PNPM_OUTDATED_LOCKFILE in CI is the pnpm counterpart.
- Pinning the Package Manager with Corepack removes version drift between machines.