Safeguard
Software Supply Chain•

npm ERESOLVE: Fix Peer Dependency Errors Without the Flag

Fix npm ERESOLVE unable to resolve dependency tree errors without npm install --legacy-peer-deps or --force: one recipe per error shape, from peer range conflicts to overrides and workspaces, plus what each flag really costs.

Safeguard Security Team
36 min read

To fix an npm ERESOLVE error without --legacy-peer-deps, read the three lines npm prints (what it found, what could not be resolved, and who asked for it), then change the version that is actually wrong: upgrade the plugin whose peer range is stale, upgrade or replace the parent that pins an old copy, align the versions that two packages disagree on, or, as a deliberate and documented exception, add a scoped overrides entry in the root package.json. The flags make the error go away by switching off the check. The recipes below make the conflict go away.

This post is the practical companion to our explainer on what npm's legacy-peer-deps flag does. If you need a refresher on why peer dependencies exist at all, read npm peer dependencies explained first. If the person reaching for the flag is an AI coding assistant rather than a human, we covered that case in when an AI fixes an install error with --force or --legacy-peer-deps. Here we stay with one question: for each shape of ERESOLVE error, what is the fix, and what does forcing past it cost?

Key takeaways

  • ERESOLVE is npm telling you that the dependency graph you asked for contradicts itself. Since npm 7, peer dependencies are installed by default, so a plugin's declared peer range is a real constraint, not a warning.
  • Every ERESOLVE report has the same anatomy: While resolving, Found, Could not resolve dependency and sometimes Conflicting peer dependency. Those lines tell you which of five shapes you are in.
  • The five shapes are: an incompatible peer range at the root, a stale parent, a transitive peer conflict between two packages, a case where an override is the honest answer, and a workspace or monorepo conflict. Each has its own recipe.
  • npm explain, npm ls --all and npm view <pkg> peerDependencies answer almost every diagnostic question before you change anything.
  • --legacy-peer-deps makes npm ignore peerDependencies for the whole tree, not just the package that failed. npm's own documentation says its use "is not recommended".
  • --force removes a long list of protections at once, of which allowing conflicting peers in the root project is only one.
  • overrides is the precise tool when you have evidence a package works outside its declared range. It only works in the root package.json, and it should be scoped and written down.
  • The tree npm builds is the tree you ship and the tree your security scanner sees. A forced tree is a combination nobody tested, and it can quietly block the upgrade that fixes a vulnerability later.

What ERESOLVE actually means

npm resolves your dependencies into an ideal tree before it writes anything to node_modules. When it cannot place a package so that every declared requirement is satisfied, it stops with the error code ERESOLVE.

Most of the time the requirement that cannot be satisfied is a peer dependency. The package.json documentation describes peer dependencies as the way a package expresses "compatibility of your package with a host tool or library, while not necessarily doing a require of this host". It then states the change that produced most of the ERESOLVE errors developers see today: "In npm versions 3 through 6, peerDependencies were not automatically installed, and would raise a warning if an invalid version of the peer dependency was found in the tree. As of npm v7, peerDependencies are installed by default. Trying to install another plugin with a conflicting requirement may cause an error if the tree cannot be resolved correctly."

So when a project that installed cleanly years ago on npm 6 fails on a modern npm, nothing in the project got worse. The conflict was always there. npm 6 printed a warning and installed a tree that broke the plugin's stated contract. npm 7 and later refuse.

There is a second, quieter behaviour worth knowing, because it explains why some conflicts fail and others only warn. The strict-peer-deps setting is documented like this: "By default, conflicting peerDependencies deep in the dependency graph will be resolved using the nearest non-peer dependency specification, even if doing so will result in some packages receiving a peer dependency outside the range set in their package's peerDependencies object. When such an override is performed, a warning is printed, explaining the conflict and the packages involved." In the npm source, that warning is logged as ERESOLVE overriding peer dependency. If you have seen that warning scroll past during an install that otherwise succeeded, you already have a peer conflict in your tree. npm just decided it was deep enough to guess.

The hard failure happens when the conflict involves your root project or cannot be resolved by that guess. In npm's resolver code, a conflict found while building the tree from the root raises unable to resolve dependency tree, and a conflict found while placing a dependency deeper in the graph raises could not resolve. Both carry the code ERESOLVE, and both are explained by the same report.

How to read an ERESOLVE report

npm builds the human readable explanation in a small file in the npm/cli repository, lib/utils/explain-eresolve.js. Reading it tells you exactly which lines can appear and in what order:

  1. While resolving: names the package npm was working on when it hit the conflict. Often this is your own project.
  2. Found: names the version that is already in the tree, followed by the chain of packages that put it there.
  3. Could not resolve dependency: names the edge that cannot be satisfied: the dependency type (for a peer, the line starts with peer), the package name and range, and which package declared it.
  4. Conflicting peer dependency: appears only when the failure is between two peers. It names the version a peer would need to be.
  5. The fix line, which is generated from a fixed template: "Fix the upstream dependency conflict, or retry this command with --force or --legacy-peer-deps to accept an incorrect (and potentially broken) dependency resolution." If strict-peer-deps is on, --no-strict-peer-deps is offered too.

The same code also writes a full report, at unlimited depth, to eresolve-report.txt in your npm cache folder. The terminal only shows four levels of the chain. When the chain is long, open the report file instead of guessing.

Here is what a root level conflict looks like. The package names come from npm's own documentation examples and the versions are illustrative: @npm/tea-latte is a plugin that declares a peer dependency on @npm/tea 2.x, and the project has moved to @npm/tea 3.

npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: my-app@1.0.0
npm error Found: @npm/tea@3.0.0
npm error node_modules/@npm/tea
npm error   @npm/tea@"^3.0.0" from the root project
npm error
npm error Could not resolve dependency:
npm error peer @npm/tea@"2.x" from @npm/tea-latte@1.3.5
npm error node_modules/@npm/tea-latte
npm error   @npm/tea-latte@"^1.3.5" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.

The prefix at the start of each line varies between npm releases, and older releases print it differently. The body is what matters. In plain language: your project asks for tea 3, the plugin only supports tea 2, and npm will not choose for you.

A deeper conflict, where two of your dependencies disagree about a shared peer, adds the extra heading. This output is illustrative too, built from the same documentation example names and made up versions:

npm error code ERESOLVE
npm error ERESOLVE could not resolve
npm error
npm error While resolving: @npm/soy-milk@1.2.0
npm error Found: @npm/tea@3.0.0
npm error node_modules/@npm/tea
npm error   peer @npm/tea@"^3.0.0" from @npm/chai-kit@4.0.0
npm error   node_modules/@npm/chai-kit
npm error     @npm/chai-kit@"^4.0.0" from the root project
npm error
npm error Could not resolve dependency:
npm error peer @npm/tea@"2.x" from @npm/soy-milk@1.2.0
npm error node_modules/@npm/soy-milk
npm error   @npm/soy-milk@"^1.2.0" from the root project
npm error
npm error Conflicting peer dependency: @npm/tea@2.2.0
npm error node_modules/@npm/tea
npm error   peer @npm/tea@"2.x" from @npm/soy-milk@1.2.0

The Found block tells you who brought the current version in. The Could not resolve dependency block tells you who disagrees. The Conflicting peer dependency line tells you what the disagreeing package wants.

Three questions to answer before you change anything

Before you edit package.json, answer these three questions. Each has a one line command.

Who put this version here? npm explain (alias npm why) "will print the chain of dependencies causing a given package to be installed in the current project". If you already have a node_modules folder from a previous install, run:

npm explain @npm/tea

npm ls gives the top down view. The npm documentation notes that "the tree shown is the logical dependency tree, based on package dependencies, not the physical layout of your node_modules folder", which is exactly what you want when chasing a conflict:

npm ls @npm/tea --all

What range does the complaining package actually declare, and has that changed in newer releases? Ask the registry, not the README:

npm view @npm/tea-latte peerDependencies
npm view @npm/tea-latte@latest peerDependencies
npm view @npm/tea-latte versions --json

Walk the versions list from newest down until you find one whose peer range includes the host version you are on. That single lookup resolves the majority of ERESOLVE errors.

Is the peer optional? Some packages mark peers as optional in peerDependenciesMeta. The documentation says npm "will not automatically install optional peer dependencies". An optional peer still has a range, so an installed version outside it can still conflict, but an optional peer you do not use is a strong hint that removing the unused host from your tree is a valid fix.

With those answers in hand, you can tell which of the five shapes you are in.

The five ERESOLVE shapes at a glance

ShapeWhat the report showsFirst commandThe fixWhat not to do
1. Incompatible peer range at the rootFound is a direct dependency of the root project; Could not resolve is a peer edge from another direct dependencynpm view <plugin> peerDependencies across versionsUpgrade the plugin to a release that supports your host, or hold the host back until it doesInstall with --legacy-peer-deps and commit the lockfile
2. Stale parentThe conflicting version is pinned by a direct dependency that has not moved in a long timenpm explain <pkg> and the parent's release historyUpgrade the parent; if it is abandoned, replace itOverride the parent's child without testing the parent
3. Transitive peer conflictConflicting peer dependency appears; two packages want different majors of the same peernpm ls <peer> --allFind the version set that satisfies both, then align or drop one sidePick whichever version makes the error disappear
4. Overly strict range, proven compatibleThe plugin works with your host version but its range has not caught upnpm view plus your test suiteScoped overrides in the root package.json, with a dated noteGlobal override with no scope and no note
5. Workspaces or monorepoWorkspaces want different majors of the same host, or an override in a workspace package.json is ignorednpm ls <pkg> --all from the root, -w per workspaceAlign host versions across workspaces; put overrides in the root onlyAdd legacy-peer-deps=true to the root .npmrc for everyone

The sections below work through each shape with commands and package.json changes. Every example uses illustrative names. Swap in your own packages, and keep the logic.

Recipe 1: incompatible peer range at the root

How you recognise it. The report starts with ERESOLVE unable to resolve dependency tree. While resolving names your own project. Found shows a version that your root package.json asks for directly. Could not resolve dependency shows a peer edge from another of your direct dependencies.

This is the most common shape, and it usually appears right after you upgrade a framework or host library. You bumped the host to a new major. One plugin still declares a peer range for the old major.

Step 1: confirm the declared range.

npm view @npm/tea-latte@1.3.5 peerDependencies
# { '@npm/tea': '2.x' }

Step 2: look for a release of the plugin that has moved on.

npm view @npm/tea-latte versions --json
npm view @npm/tea-latte@2 peerDependencies
# { '@npm/tea': '2.x || 3.x' }

If a newer plugin release includes your host version, the fix is a normal upgrade:

npm install @npm/tea-latte@2

Read the plugin's changelog for that major, because a plugin major often changes its own API. That is a real cost, but it is a cost you can see and test, which is the entire point.

Step 3: if no release supports your host yet, choose which side moves. You have three honest options.

  • Hold the host back. Keep @npm/tea on 2.x until the plugin catches up. This is the conservative answer, and it is usually right for production applications where the host upgrade was optional.
  • Replace the plugin. If the plugin's repository shows no activity and open issues asking for the new host, the plugin may be finished. Find a maintained alternative or bring the small amount of code you used into your own project.
  • Prove compatibility and override. If you have run the plugin against the new host and your tests pass, an override is legitimate. That is Recipe 4.

Step 4: verify.

rm -rf node_modules
npm install
npm ls @npm/tea --all
npm test

npm ls should show one copy of the host at the version you chose, and no invalid markers. npm ls prints extraneous, missing and invalid packages, so a clean output here is real evidence.

Why not the flag? With --legacy-peer-deps, npm would install @npm/tea@3 and @npm/tea-latte@1.3.5 side by side and say nothing. The plugin would then run against a host major it was never written for. Sometimes that works. Sometimes it fails at import time. Sometimes it fails only on the code path a customer hits first.

Recipe 2: the stale parent

How you recognise it. npm explain on the conflicting package shows it arriving through one particular direct dependency, and that dependency has not had a release in a long time. The root project and other dependencies have moved on. This parent has not.

A stale parent creates ERESOLVE in two ways. It can declare a narrow peer range on something everyone else has upgraded, which looks like Recipe 1 but is caused by a package you do not think of as a plugin. Or it can depend directly on an old major of a library that is also a peer for something else in your tree.

Step 1: find the parent.

npm explain @npm/tea

Read the chain from the bottom up. The first entry that says from the root project is the direct dependency responsible.

Step 2: check whether the parent has a newer release.

npm view @npm/old-parent time --json
npm view @npm/old-parent@latest dependencies peerDependencies
npm outdated @npm/old-parent

The time field lists publish dates per version, which tells you whether the package is still maintained. If latest has a newer range, upgrade the parent:

npm install @npm/old-parent@latest

Step 3: if the parent is abandoned, replace it. This is the step teams skip, and it is the one that matters for security. An unmaintained parent does not only cause peer conflicts. It also pins old copies of its own dependencies, and when one of those gets a vulnerability advisory, the fixed version will not reach you through the parent. An ERESOLVE caused by a stale parent is often the first visible symptom of a dependency that has become a long term liability.

Step 4: if you cannot replace it this sprint, scope an override to that parent only. The npm documentation shows how to override a package only when it is a child of a specific parent:

{
  "overrides": {
    "@npm/old-parent": {
      "@npm/tea": "3.0.0"
    }
  }
}

That changes only the copy of @npm/tea that sits under @npm/old-parent. Test the code paths that use the parent, and open a ticket to replace it. An override that nobody remembers adding is how projects end up running untested combinations for years.

Step 5: verify.

npm install
npm explain @npm/tea
npm ls --all | grep -i invalid

npm ls marks packages that came from an override as overridden in its output, which makes your exceptions easy to audit later.

Recipe 3: the transitive peer conflict

How you recognise it. The report says ERESOLVE could not resolve rather than unable to resolve dependency tree, and it includes a Conflicting peer dependency line. Two packages in your tree, often two plugins for the same host, want different majors of the same peer.

This is the shape where people most often reach for a flag, because neither package is "wrong". Each one is internally consistent. They just cannot share a tree.

Step 1: map every requester of the peer.

npm ls @npm/tea --all

If the install never finished, npm ls will reflect the previous tree or report missing packages. In that case use the full eresolve-report.txt from the npm cache folder, which lists the entire chain, and npm view on each requester.

Step 2: build the compatibility table. For each package that declares the peer, list the ranges declared by its recent releases.

npm view @npm/soy-milk versions --json
npm view @npm/soy-milk@1 peerDependencies
npm view @npm/soy-milk@2 peerDependencies
npm view @npm/chai-kit@4 peerDependencies
npm view @npm/chai-kit@3 peerDependencies

Write the results in a small table:

Package and versionDeclared peer range for @npm/tea
@npm/soy-milk 1.2.02.x
@npm/soy-milk 2.0.02.x or 3.x
@npm/chai-kit 3.x2.x
@npm/chai-kit 4.0.0^3.0.0

The values here are illustrative. Yours come from npm view.

Step 3: pick the intersection. In the illustrative table, @npm/soy-milk 2.0.0 and @npm/chai-kit 4.0.0 both accept tea 3. Upgrading soy-milk resolves the conflict without touching anything else. If there is no intersection on the newest releases, look for one on older releases of the package you care least about, or pin both to the newest set that agrees and plan the next upgrade.

Step 4: if there is no intersection at all, one side has to go. Two packages that require different majors of a shared host cannot both be correct in one tree. Installing both with --legacy-peer-deps does not fix that. It hides it, and at runtime one of them talks to a host it was not written for. Remove the one you use least, or replace it.

Step 5: deduplicate and verify.

npm install
npm dedupe
npm ls @npm/tea --all

The prefer-dedupe setting is documented as "prefer to deduplicate packages if possible, rather than choosing a newer version of a dependency", which can help when the conflict is with a non-peer duplicate rather than a true peer clash. Use it as a check, not as a fix for a real range mismatch.

Recipe 4: when an override is the honest answer

Sometimes the plugin works fine with your host, and its peer range is simply out of date. Maintainers often widen peer ranges weeks or months after a host release, because widening a range is a promise and they want to test first. If you have done that testing yourself, an override is the right tool. It is precise, it is visible in package.json, and it is limited to the packages you name.

The package.json documentation describes overrides as a way "to replace a package in your dependency tree with another version, or another package entirely", and notes that "these changes can be scoped as specific or as vague as desired." It lists the reasons you might want one, which include "replacing the version of a dependency with a known security issue" and "making sure that the same version of a package is used everywhere".

Rule 1: overrides only work in the root. The documentation is explicit: "Overrides are only considered in the root package.json file for a project. Overrides in installed dependencies (including workspaces) are not considered in dependency tree resolution."

Rule 2: scope the override to the package that needs it. Prefer the nested form, which only affects the peer as seen by one plugin:

{
  "dependencies": {
    "@npm/tea": "^3.0.0",
    "@npm/tea-latte": "^1.3.5"
  },
  "overrides": {
    "@npm/tea-latte": {
      "@npm/tea": "^3.0.0"
    }
  }
}

Rule 3: if you override a package you also depend on directly, the specs must match. The documentation says: "You may not set an override for a package that you directly depend on unless both the dependency and the override itself share the exact same spec." A mismatch throws an EOVERRIDE error. To keep the two in lockstep, the docs offer a reference syntax: prefix the name of a direct dependency with $ and the override will use that dependency's spec.

{
  "dependencies": {
    "@npm/tea": "^3.0.0"
  },
  "overrides": {
    "@npm/tea": "$@npm/tea"
  }
}

When you later upgrade @npm/tea in dependencies, the override follows.

Rule 4: you can scope by the parent's version too. The documentation shows keys that include a version, for example overriding a child only when the parent is @npm/bar@2.0.0. Use that when you know the newer parent release has already fixed the range, so the override stops applying the moment you upgrade.

Rule 5: write down why. package.json does not allow comments, so keep a short note in the pull request description and in your repository's dependency notes: which package, which range, which tests you ran, and the upstream issue or pull request that will make the override unnecessary. Review overrides on a schedule. Each one is a small, deliberate exception to what a maintainer promised.

Step: verify the override took effect.

rm -rf node_modules package-lock.json
npm install
npm ls @npm/tea --all
npm test

Look for overridden in the npm ls output. If your npm release still reports ERESOLVE with a correctly placed override, upgrade npm before you try anything else. Override handling has changed across npm releases, and the npm/cli changelog records those changes. Do not fall back to a flag because an old npm handled overrides less well.

Recipe 5: workspaces and monorepos

npm workspaces let one root package manage several packages from the local file system. npm describes them as "a much more streamlined workflow handling linked packages from the local file system" that "automates the linking process as part of npm install". All workspaces share one install, one lockfile and, by default, one hoisted node_modules at the root. That makes peer conflicts more likely and harder to read, for three reasons.

First, two workspaces can legitimately want different majors of the same host. One app is on tea 3; an older internal tool still uses tea 2 and a plugin that only supports 2.x. npm has to place both, and peers resolve relative to where a package sits in the tree.

Second, overrides written in a workspace's own package.json are ignored. The documentation states that overrides in workspaces "are not considered in dependency tree resolution". Teams often add an override to the workspace that fails, see no change, and conclude overrides do not work.

Third, a single legacy-peer-deps=true line in the root .npmrc turns off peer checking for every workspace at once, including the ones that had no conflict.

Step 1: see the whole picture from the root.

npm ls @npm/tea --all
npm ls @npm/tea --all -w packages/legacy-tool
npm explain @npm/tea

Step 2: align host versions where you can. The best monorepo fix is the boring one: decide which major of each host the repository uses, and move every workspace to it. Install per workspace with the -w flag so the change lands in the right package.json:

npm install @npm/tea@^3.0.0 -w packages/legacy-tool
npm install @npm/tea-latte@2 -w packages/legacy-tool

Step 3: put any override in the root package.json, scoped as tightly as you can.

{
  "name": "my-monorepo",
  "workspaces": ["packages/*"],
  "overrides": {
    "@npm/tea-latte": {
      "@npm/tea": "^3.0.0"
    }
  }
}

Step 4: if one workspace truly needs a different major, isolate it. Some teams move the outlier into its own repository or its own install root until it can be upgraded. That costs some convenience, and it is far cheaper than a tree in which every workspace ignores peer contracts.

Step 5: catch undeclared dependencies while you are here. npm's documentation for install-strategy recommends that package authors use --install-strategy=linked during development "to catch undeclared ('phantom') dependencies before publishing", because the isolated layout only exposes a package's declared dependencies. In a monorepo, hoisting can hide that a workspace imports a host it never declared. A linked install surfaces that, and fixing it often makes the real peer conflict easier to see.

Step 6: verify per workspace and in CI.

npm ci
npm ls --all
npm test --workspaces

The CI shape: works on my machine, ERESOLVE in the pipeline

There is a sixth situation that is not a new conflict shape but shows up constantly. A developer installs locally with --legacy-peer-deps, commits the lockfile, and the pipeline runs npm ci without the flag. The lockfile describes a tree that npm's normal rules reject, so CI can fail. The usual reaction is to add the flag to the CI command too, and now nobody is checking peers anywhere.

The pipeline failure is a common symptom, because the lockfile describes a tree that normal rules reject. The fix is to treat the lockfile as the output of one consistent set of rules:

  1. Remove the flag from local habits and from any .npmrc that set it.
  2. Delete node_modules and package-lock.json, and run a normal npm install.
  3. Work through whichever recipe above matches the ERESOLVE you now see.
  4. Commit the clean lockfile, and run npm ci in CI exactly as developers run it.

If your team decides a flag is unavoidable for a period, put it in the project .npmrc so local and CI behave the same, write down the reason and an end date, and track it like any other exception. The worst state is the inconsistent one, where the lockfile was produced by rules that CI does not apply.

What --legacy-peer-deps and --force actually cost

The fix line in every ERESOLVE report offers both flags "to accept an incorrect (and potentially broken) dependency resolution". That wording comes from npm itself. Here is what each one does to your project, based on npm's documentation.

--legacy-peer-deps

The configuration reference says it "causes npm to completely ignore peerDependencies when building a package tree, as in npm versions 3 through 6", and ends with a direct warning: "Use of legacy-peer-deps is not recommended, as it will not enforce the peerDependencies contract that meta-dependencies may rely on." We walk through the failure modes in what the flag does. For choosing a recipe, three points matter:

  • It is global to the install. The flag does not apply to the one package that failed. Every peer declaration in the whole tree is ignored, including ones that were fine.
  • The lockfile remembers. The tree you got with the flag is written into package-lock.json, so teammates and CI inherit the incorrect resolution even if they never typed the flag.
  • Future conflicts are hidden. Once the flag is in an .npmrc, the next real peer conflict produces no error at all.

It also differs from --omit=peer, which the documentation explains "will avoid unpacking peerDependencies on disk, but will still design a tree such that peerDependencies could be unpacked in a correct place". If your goal is a smaller install rather than ignoring a conflict, --omit=peer keeps the tree honest.

--force

--force is broader. The force setting "removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input", and one item on its list is allowing "conflicting peerDependencies to be installed in the root project". The same list includes engine checks and letting npm audit fix leave your declared ranges, which we cover in the AI assistant post. Using it to get past ERESOLVE switches those guards off in that same command, which makes it the most expensive way to make an error disappear.

The flags compared

OptionPeer conflictsScopeLockfile effectWhen it is acceptable
Default (no flag)Fails at the root; deep conflicts resolved with a warning (ERESOLVE overriding peer dependency)Whole treeLockfile reflects a tree that satisfies root peer contractsAlways the starting point
strict-peer-deps=trueEvery peer conflict fails, including deep onesWhole treeStrictestLibraries and teams that want to see every conflict
Scoped overridesThe named package gets the version you chooseOnly the packages and paths you nameRecorded and marked as overriddenProven compatibility, documented, reviewed
--legacy-peer-depsAll peer dependencies ignoredWhole treeIncorrect resolution written to the lockfileShort term unblock on a branch, never committed silently
--forceConflicting peers allowed in the root, plus many other guards removedWhole commandIncorrect resolution written to the lockfileAlmost never for ERESOLVE

The security cost

There is also a supply chain cost, and it is easy to miss because nothing fails.

The tree npm resolves is the tree you build, ship and scan. When peers are ignored, the combination of versions in that tree is one no maintainer has declared support for. Software composition analysis will still tell you which versions are present and which advisories apply, but the conversation about upgrading becomes harder. A security fix in the host may require a plugin upgrade that a forced tree has been quietly postponing for months.

There is a mirror image too. When an advisory lands on a transitive dependency and the fix needs a host major, the correct path is often exactly the kind of upgrade that triggers ERESOLVE. Teams that habitually force past ERESOLVE also tend to force past the error that a security upgrade raises, and end up with a fixed package next to a plugin that cannot use it. Teams that resolve ERESOLVE properly have a repeatable way to take security upgrades.

Where Safeguard fits

Safeguard does not replace npm's resolver, and it does not make ERESOLVE go away. What it does is help you see and change the tree npm builds.

Software composition analysis in Safeguard resolves the dependency graph from your manifests and lockfiles, including transitive dependencies, and each finding carries the vulnerable component and version, the advisory identifiers, the full transitive path from your project root to the vulnerable package, and the safe upgrade target. That path is the same information npm explain gives you, shown next to the vulnerability that made you care. The documentation notes that manifests and lockfiles are both honored, so resolution matches what actually installs.

Deep dependency scanning walks every edge in the resolved dependency graph and, for npm, pnpm and Yarn, resolves workspaces, peer dependencies and optional dependencies. In the ESSCM UI, a finding shows the full transitive path from your project to the vulnerable package, so you can trace why it is present.

AI Remediate uses Griffin, Safeguard's security model, to propose dependency upgrades as pull requests. The documentation lists handling transitive dependency conflicts and updating lock files among its dependency capabilities, and its settings include requiring approval before pull requests, running tests before creating them, and a breaking change policy. The upgrade arrives as a pull request you can review, rather than as a flag on an install command.

The honest limit: if a plugin has no release that supports your host and nobody has tested the combination, no tool can make that combination safe. Someone still has to choose Recipe 1, 2 or 4 and own the decision.

Common mistakes

  • Adding legacy-peer-deps=true to .npmrc to fix one package. It switches off peer checks for every package, for every developer and every pipeline that reads that file.
  • Committing a lockfile produced with a flag. The incorrect resolution becomes everyone's default, and installs without the flag fail in ways that look unrelated.
  • Using --force as a stronger --legacy-peer-deps. It is not stronger. It is different and broader, and it also disables engine checks and lets npm audit fix jump majors.
  • Putting overrides in a workspace package.json. npm ignores them there. Only the root package.json counts.
  • Writing a top level override for a direct dependency with a different spec. That throws EOVERRIDE. Use the matching spec or the $ reference.
  • Global overrides with no scope. An unscoped override changes the package for every requester in the tree, including ones that were fine.
  • Upgrading the host without checking plugins first. Run npm view <plugin> peerDependencies for each plugin before you bump a framework major.
  • Ignoring the ERESOLVE overriding peer dependency warning. It means npm already placed a package outside its declared peer range. It is a conflict waiting to become an error.

FAQ

How do I fix npm ERESOLVE unable to resolve dependency tree?

Read the Found and Could not resolve dependency lines to see which package wants which version. Then run npm view <package> peerDependencies across that package's releases, upgrade to a release whose peer range includes your installed version, or hold the host back. If neither works and you have tested the combination, add a scoped override in the root package.json.

Is it safe to use npm install --legacy-peer-deps?

It is a short term unblock, not a fix. npm's documentation says it makes npm "completely ignore peerDependencies when building a package tree" and that its use "is not recommended". It applies to the whole tree, and the result is written to your lockfile.

What is the difference between --force and --legacy-peer-deps?

--legacy-peer-deps ignores peer dependencies while building the tree. --force allows conflicting peer dependencies in the root project and also removes other protections, such as engine checks and the limit that keeps npm audit fix inside your declared ranges. Neither fixes the conflict.

Why did ERESOLVE start after upgrading npm?

From npm 7, peer dependencies are installed by default and conflicts are enforced. Under npm 3 to 6 the same conflict only printed a warning. Your project had the conflict before; the newer npm reports it.

Can npm overrides fix a peer dependency conflict?

Yes, when you have evidence the package works with the version you choose. Put the override in the root package.json, scope it to the package that needs it, match the spec of any direct dependency (or use the $ reference), and confirm with npm ls that the package shows as overridden.

Why are my overrides ignored in an npm workspace?

npm only reads overrides from the root package.json. Overrides in workspace package.json files are not considered in dependency tree resolution. Move the override to the root.

What does "Conflicting peer dependency" mean in an npm error?

Two packages in your tree want different versions of the same peer. The line names the version the disagreeing package would need. Map both requesters with npm ls <peer> --all, then find releases of each that accept a common version.

Where is the full ERESOLVE report?

npm writes the full explanation, at unlimited depth, to eresolve-report.txt in your npm cache folder. The terminal shows only the first few levels of each chain.

Should I set strict-peer-deps?

For libraries and teams that want to see every conflict, yes. By default npm resolves deep peer conflicts with a warning. strict-peer-deps=true turns those warnings into failures, so you find out early.

Peer dependency errors are rarely about one package. They show up exactly when you try to move a host library forward, which is also when security upgrades land, so a team that can resolve ERESOLVE cleanly is a team that can take a fixed version the day it ships. The same discipline applies after the conflict is gone: the tree npm resolved is what you build and run, and keeping vulnerable versions out of it is the next job.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Griffin, Safeguard's security model, opens a policy gated pull request that moves the affected dependency to the safe version, and your own CI runs your tests on it before anyone merges. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Auto-merge is a separate switch and stays off by default, so your team decides what merges on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Safeguard Gold rebuilds patched versions of open-source packages and images from source, signs each build, attaches an SBOM, SLSA provenance and VEX, and serves them from registry.safeguard.sh to your organization. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

The demo below is a real Safeguard CLI session. It shows the CLI being installed, the assistant being asked to fix a dependency finding in an npm project, each proposed step being approved, and a scan confirming the result.

Click through it at your own pace, or open it full screen.

This demo sets up the Safeguard local runner on your own machine, approves it with a device code and then scans from the terminal, so the code stays where it lives.

Click through it at your own pace, or open it full screen.

The last demo connects Claude to the Safeguard MCP server with OAuth and shows how you choose which tools Claude can use.

Click through it at your own pace, or open it full screen.

To try it on your own code, download Safeguard and follow the CLI guide.

Quick check

You added an override for a peer in packages/web/package.json inside an npm workspace, and ERESOLVE did not change. Why?

npm only reads overrides from the root package.json. Overrides in workspaces are not considered in dependency tree resolution. Move the override to the root package.json and scope it to the package that needs it.

Your team set legacy-peer-deps=true in .npmrc to fix one plugin. Which packages does it affect?

All of them. The setting makes npm ignore peerDependencies for the whole tree, and the resulting resolution is written to the lockfile that every teammate and pipeline installs from.

The report shows a "Conflicting peer dependency" line. Which recipe applies?

The transitive peer conflict. Two packages want different versions of a shared peer. Map both with npm ls <peer> --all, list the peer ranges each release declares with npm view, and pick releases that agree.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

We email you a link to confirm, and only then add you to the list. Unsubscribe in one click. Privacy

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.