Fixing pnpm-lock.yaml Merge Conflicts
When two branches both add or bump dependencies and then meet at a merge, pnpm-lock.yaml conflicts almost every time — and Git's line-based merge produces a file that no longer parses as a valid dependency graph. This page walks through the exact recovery sequence that regenerates a clean lockfile, plus the repository configuration that stops the conflicts from blocking you in the first place.
Exact symptoms and error messages
Git halts the merge or rebase and injects standard conflict markers into pnpm-lock.yaml:
<<<<<<< HEAD
/react@18.2.0:
resolution: {integrity: sha512-...HEAD...}
=======
/react@18.3.1:
resolution: {integrity: sha512-...branch...}
>>>>>>> feature-branch
If you commit that file as-is, or hand-edit the markers, the next strict install fails with a parse or integrity error:
ERR_PNPM_LOCKFILE_BREAKING_CHANGE Lockfile is broken
ERR_PNPM_UNEXPECTED_STORE Unexpected store location
ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE
Cannot perform a frozen installation because the lockfile is out of sync
The frozen-install failure is the one that surfaces in CI, blocking the pipeline even after the merge "succeeds" locally.
Root cause analysis
pnpm-lock.yaml is a strict, deterministic YAML graph mapping exact package versions, integrity hashes, and peer-dependency resolutions. Git's three-way merge operates line by line and has no model of that graph, so when two branches change overlapping subtrees it interleaves their text rather than reconciling the resolution. Because pnpm uses a content-addressable store and strict peer-dependency enforcement, even a small dependency change can shift the topology of unrelated entries — which is why these conflicts are both frequent and impossible to resolve by editing markers. The right mental model comes from Lockfile Management Strategies: the lockfile is generated output, so you regenerate it rather than patch it.
Resolution and configuration patch
Do not edit conflict markers. Follow this exact sequence to regenerate a valid graph and unblock the pipeline:
- Abort the conflicted merge state:
git merge --abort - Check out the up-to-date base branch:
git checkout main git pull origin main - Merge the feature branch, accepting only the manifest changes. The lockfile will conflict — discard it and keep the merged
package.jsonfiles:git merge feature-branch git checkout --theirs pnpm-lock.yaml # or --ours; the file is about to be regenerated anyway - Regenerate the lockfile deterministically from the merged manifests:
pnpm install --lockfile-only - Verify graph integrity with a frozen install — this must exit 0:
pnpm install --frozen-lockfile - Commit the regenerated lockfile:
git add pnpm-lock.yaml git commit -m "chore: resolve pnpm-lock.yaml merge conflict"
To make this automatic on future merges, register a merge driver so Git stops trying to text-merge the lockfile:
# .gitattributes
pnpm-lock.yaml merge=ours
# .git/config (run once, or distribute via a setup script)
[merge "ours"]
driver = true
With merge=ours, Git keeps the current branch's lockfile on conflict; you then run pnpm install --lockfile-only in a post-merge hook to reconcile it against the merged manifests. Pin the pnpm version so every contributor regenerates the file identically:
{
"packageManager": "pnpm@10.4.1"
}
CLI validation and debug commands
# Confirm no conflict markers remain anywhere in the lockfile
grep -nE '^(<<<<<<<|=======|>>>>>>>)' pnpm-lock.yaml && echo "MARKERS LEFT" || echo "clean"
# Prove the lockfile is internally consistent and in sync with the manifests
pnpm install --frozen-lockfile
# Trace why a specific version resolved the way it did
pnpm why react
# List the resolved workspace graph at top level
pnpm ls -r --depth=0
A clean grep, a zero-exit --frozen-lockfile, and a pnpm why that shows a single resolved version together confirm the conflict is genuinely resolved rather than papered over.
Prevention and CI/CD guardrails
- Add the
pnpm-lock.yaml merge=oursdriver plus a post-mergepnpm install --lockfile-onlyhook so the lockfile is regenerated, never text-merged. - Pin pnpm with the
packageManagerfield and Corepack so the lockfile serializes identically on every machine. - Run
pnpm install --frozen-lockfileas the first CI step to catch any conflicted or stale lockfile before it reaches a build. - Add a pre-push grep for conflict markers (
<<<<<<<) across the repo to block obviously broken lockfiles. - Keep dependency PRs small and merge them promptly to shrink the window where two branches diverge the same subtree.
Frequently Asked Questions
Is it safe to manually edit conflict markers inside pnpm-lock.yaml?
No. Hand edits almost always break YAML structure or invalidate an integrity hash, and the next pnpm install --frozen-lockfile will fail. Always discard the conflicted file and regenerate it with pnpm's resolver.
Why does pnpm-lock.yaml change even when package.json is untouched? pnpm resolves transitive and peer dependencies dynamically, so refreshed registry metadata, a new patch version, or a workspace topology change will re-serialize parts of the lockfile to keep resolution exact and reproducible.
How can CI pipelines handle pnpm lockfile conflicts automatically?
Configure the merge=ours driver in .gitattributes, then run a post-merge pnpm install --lockfile-only step followed by a --frozen-lockfile verification. The driver avoids the text merge; the regeneration reconciles the graph deterministically.
Should I commit pnpm-lock.yaml in a library package? Yes. The committed lockfile guarantees reproducible builds for contributors and CI. It is not published to consumers, but it remains essential for internal testing and workspace consistency.
Related
- Lockfile Management Strategies — the determinism model and frozen-install discipline behind this fix.
- Workspace Configuration Deep Dive — how a shared root lockfile and the workspace protocol shape pnpm's graph.
- Understanding package.json Fields — the manifest ranges that the regenerated lockfile resolves.