Running npm install --legacy-peer-deps tells npm to ignore peer dependency conflicts entirely and resolve packages the way npm 4 through 6 did — peers are not auto-installed and version mismatches are not treated as errors. It is the fastest way to get past an ERESOLVE wall, and also the fastest way to ship a dependency tree that your libraries never agreed to run against. Before you paste that flag from Stack Overflow into your CI config, it is worth understanding exactly what you are trading away.
A Short History of Peer Dependencies
A peer dependency is a package's way of saying "I plug into X, but you provide X." A React component library does not bundle its own React; it declares react as a peer and expects exactly one copy in your app. Same for ESLint plugins, webpack loaders, and ORM adapters.
How npm treats that declaration has flip-flopped:
- npm 4-6: peers were not installed automatically. npm printed a warning if your tree did not satisfy them and moved on.
- npm 7 and later: peers are installed automatically, and an unsatisfiable peer range is a hard
ERESOLVEerror that blocks the install.
The npm 7 change was correct in principle — a warning nobody reads is not a contract — but it collided with an ecosystem full of packages whose peer ranges lagged reality. A library declaring "peerDependencies": { "react": "^17.0.0" } blocks your React 18 upgrade even if the library works fine on 18. When you install npm dependencies in an older project after a Node upgrade, this is usually the first thing that breaks. --legacy-peer-deps was added as the escape hatch: pretend it is npm 6 again.
What the Flag Actually Changes
Three behaviors switch off:
npm install --legacy-peer-deps
- npm stops installing missing peer dependencies for you.
- npm stops validating that installed versions satisfy declared peer ranges.
ERESOLVEconflicts arising from peers simply do not occur.
Compare that with the other panic flag:
npm install --force
--force still runs full resolution but accepts known-bad outcomes, potentially installing multiple conflicting versions of a peer. --legacy-peer-deps skips the reasoning; --force overrules it. Of the two, --legacy-peer-deps is the less destructive, which is faint praise.
One more thing teams miss: the flag persists. If it was ever run with npm config set legacy-peer-deps true, or someone committed it to .npmrc, every subsequent install in that repo inherits npm 6 semantics silently:
# .npmrc — check your repos for this line
legacy-peer-deps=true
Grep for it. Plenty of projects are running with peer checking disabled and nobody remembers why.
The Failure Modes It Hides
Runtime version mismatches
Peer ranges encode real compatibility claims. Skip the check and you can end up with a plugin compiled against API v3 receiving API v4 at runtime. The failure is rarely an obvious crash at install time — it is a hook returning undefined in production, or two copies of a library that must be a singleton (React, graphql, prosemirror) each holding their own internal state.
Frozen, aging transitive trees
The flag is almost always applied to keep an old thing working. That old thing keeps its old transitive dependencies, and those accumulate known vulnerabilities. The package whose peer range forced the flag is typically unmaintained — the same reason its peers are stale is the reason its CVEs will not be patched. A stale tree held together by --legacy-peer-deps is exactly the pattern a software composition analysis tool like Safeguard SCA flags as compounding risk: outdated pins plus an abandoned maintainer.
Lockfile divergence between environments
If developers install with the flag but CI does not (or the reverse), package-lock.json gets rewritten with different trees depending on who ran the last install. Pick one behavior, encode it in the committed .npmrc, and make CI use npm ci so drift fails loudly — we covered why in our package-lock.json guide.
What to Do Instead
Work the conflict before suppressing it:
# See who is demanding what
npm ls react
npm why react-dom
# Try the current versions first
npm install some-plugin@latest
If the blocking package is alive, the fix is usually just upgrading it — maintainers widen peer ranges shortly after major releases of the host library. If it is dead, you have three honest options:
1. overrides in package.json — surgically pin or widen a single package's expectation instead of disabling checks globally:
{
"overrides": {
"dead-plugin": {
"react": "$react"
}
}
}
2. Fork or patch — patch-package or a scoped fork with a corrected peer range keeps validation on for the other 900 packages in your tree.
3. Replace the dependency — an unmaintained package that blocks framework upgrades today will block security patches tomorrow. Budget the migration now while it is one package, not five.
If You Must Use It
Sometimes the pragmatic call is to take the flag — a legacy app in maintenance mode, a migration mid-flight, a vendor package you cannot touch. Do it with guardrails:
- Scope it to the one install command that needs it; do not set it globally.
- Commit the decision visibly (
.npmrcin the repo with a comment, not a shell alias on one laptop). - Write down which package forced it and check quarterly whether that is still true.
- Increase scanning cadence on that repo, since peer skew will not surface at install time. Runtime behavior testing helps here too — a DAST pass against the running app catches the class of breakage that static installs no longer report.
- Treat removal of the flag as a tracked engineering task, not an aspiration.
The Rule of Thumb
ERESOLVE errors are npm telling you two packages disagree about a contract. --legacy-peer-deps does not resolve the disagreement — it stops npm from mentioning it. Use it as a tourniquet during upgrades and incident response, never as the steady state for a production application. If a repo has had the flag for more than a quarter, the real dependency problem underneath has been compounding the whole time.
FAQ
What does npm install --legacy-peer-deps actually do?
It restores npm 4-6 behavior: peer dependencies are not automatically installed and version conflicts between declared peer ranges and your actual tree are ignored instead of raising ERESOLVE errors.
Is --legacy-peer-deps the same as --force?
No. --force performs full resolution but proceeds past conflicts, and can install multiple conflicting versions of a peer. --legacy-peer-deps skips peer resolution entirely. They produce different trees from the same package.json.
Is it safe to use --legacy-peer-deps in production builds?
It is safe in the sense that the install succeeds. The risk is deferred: version mismatches surface as runtime bugs rather than install errors, and the stale packages that forced the flag keep accumulating unpatched vulnerabilities. Use it consciously and temporarily.
How do I find out why an ERESOLVE error is happening?
Read the full error — npm names the conflicting packages and ranges. Then use npm why <package> and npm ls <package> to see every demand on that package, and try upgrading the package with the narrowest peer range first.