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.
A pnpm-lock.yaml conflict happens because two branches each regenerated the lockfile against different dependency changes, and git cannot merge the resulting large, structured file line by line. The lockfile encodes the entire resolved graph — every package's version, integrity hash, and edges — so two independent regenerations touch overlapping regions in ways git sees as conflicting. The conflict is not a corruption but a signal that both branches changed dependencies and their resolutions must be reconciled by re-solving, not by stitching text together.
The reason hand-merging is dangerous is that a lockfile is a consistent solution to a constraint problem, and manually combining two solutions can produce an inconsistent one — a graph that installs differently than either branch intended, or that no longer matches the merged package.json. Because the file is machine-generated and internally cross-referenced, a human editing conflict markers cannot reliably preserve that consistency, which is why the correct recovery regenerates the lockfile rather than resolving the markers by eye.
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"
}
Take the merged manifest, discard the conflicted lockfile, and regenerate it with a fresh install:
# Accept the merged package.json files, then regenerate the lockfile
git checkout --theirs pnpm-lock.yaml # or --ours; the content is replaced anyway
pnpm install # re-solves against the merged manifests
git add pnpm-lock.yaml
git commit
pnpm install reads the merged package.json files and produces a consistent lockfile for the combined dependency set, replacing whatever conflict markers were present. Verify with pnpm install --frozen-lockfile that the regenerated lockfile matches the manifests, and run the test suite to confirm the combined dependency set resolves and works.
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.
- Never resolve lockfile conflict markers by hand — regenerate the lockfile from the merged manifest.
- Route dependency updates through dedicated, scheduled PRs so lockfile changes do not collide with feature work.
- Rebase frequently so a long-lived branch does not accumulate a large lockfile divergence.
- Verify the regenerated lockfile with a frozen install before committing the merge.
Why the regenerate-don't-merge rule holds
The rule to regenerate rather than merge a lockfile follows directly from what a lockfile is: the deterministic output of the manifests plus the resolver. Given the merged package.json files and a pinned pnpm version, there is exactly one correct lockfile, and running pnpm install produces it. A hand-merge, by contrast, tries to reconstruct that output by combining two partial views, which has no guarantee of matching what the resolver would produce and can silently encode a version that satisfies neither branch's actual requirements. Trusting the resolver to regenerate is both easier and more correct than trying to out-think it.
This is also why pinning the pnpm version with packageManager and Corepack matters for conflict recovery specifically. If two developers regenerate the lockfile with different pnpm versions, the outputs can differ in ways that create spurious conflicts even when the dependency changes are compatible. With the version pinned, everyone's pnpm install produces the same lockfile for the same manifests, so a conflict genuinely reflects a dependency divergence to reconcile rather than a tooling difference. The combination — pin the resolver, regenerate on conflict, verify with a frozen install — turns lockfile conflicts from a recurring source of friction into a mechanical, reliable step.
Preventing lockfile conflicts before they happen
Most lockfile conflicts are avoidable by controlling when and how the lockfile changes. The single most effective practice is to route dependency updates through dedicated pull requests — an update bot's grouped, scheduled PRs — rather than letting them ride along on feature branches. When lockfile changes are isolated to their own PRs, they merge quickly and rarely collide with each other or with feature work, because feature branches touch source, not the lockfile. A feature branch that does need a new dependency should add it, regenerate, and rebase promptly so its lockfile change lands before it diverges far from main.
Frequent integration is the other half. A long-lived branch accumulates a large lockfile divergence that is more likely to conflict and harder to reconcile; rebasing regularly keeps each branch's lockfile close to main so any conflict is small and recent. Together with pinning the resolver, these practices mean the lockfile changes in small, isolated, well-understood increments rather than in large, overlapping ones — which is what keeps conflicts rare, and the ones that do occur trivial to regenerate away.
Configuring git to reduce lockfile conflict noise
Git offers a few settings that make lockfile conflicts less frequent and less noisy, worth configuring once for a team that hits them often. A .gitattributes entry can mark the lockfile with a merge strategy that prefers regeneration, and some teams set the lockfile to use git's union merge to reduce spurious textual conflicts — though the safest practice remains to regenerate rather than trust any automatic merge. More impactful is enabling git rerere (reuse recorded resolution), which remembers how you resolved a conflict and replays it, useful when rebasing a branch repeatedly against a moving main.
The most effective configuration, though, is organizational rather than git-level: ensuring everyone regenerates the lockfile with the same pinned pnpm version so conflicts reflect genuine dependency divergence rather than tooling differences. A .gitattributes marking and rerere reduce the friction of the conflicts that do occur, but pinning the resolver with packageManager and Corepack, and routing dependency changes through isolated PRs, is what reduces how many occur in the first place. Combining the git-level noise reduction with the workflow-level prevention gives a team both fewer lockfile conflicts and a mechanical, low-stress way to resolve the ones that remain.
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.
How do I resolve a pnpm-lock.yaml merge conflict?
Don't merge it by hand. Keep the merged package.json files, replace the conflicted lockfile (git checkout --theirs pnpm-lock.yaml), and run pnpm install to re-solve against the merged manifests. Verify with pnpm install --frozen-lockfile and commit.
Why shouldn't I edit the lockfile conflict markers directly?
A lockfile is a consistent solution to a constraint problem; hand-combining two solutions can produce an inconsistent graph that installs differently than either branch intended. Regenerating with pnpm install produces the one correct lockfile for the merged manifests.
How do I avoid frequent lockfile conflicts?
Route dependency updates through dedicated, scheduled PRs so lockfile changes stay isolated, pin pnpm with packageManager so everyone regenerates identically, and rebase branches frequently so lockfile divergence stays small.
Can I configure git to auto-resolve lockfile conflicts?
You can reduce noise with a .gitattributes merge marking and git rerere, but never rely on an automatic textual merge for a lockfile — it can produce an inconsistent graph. The reliable resolution is always to regenerate the lockfile from the merged manifest with pnpm install.
Which side should I keep, --ours or --theirs, for the lockfile?
It does not matter — the content is replaced entirely by pnpm install, which re-solves against the merged manifests. Pick either to clear the conflict markers, then regenerate and commit the result.
Should the lockfile be committed to version control?
Always. The lockfile is what makes installs reproducible and what CI's frozen install verifies against. Committing and reviewing it alongside the manifest is required; a repo without a committed lockfile has no reproducibility guarantee.
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.