Back to glossary

Dependency Overrides

A dependency override forces the package manager to install a version you choose for a transitive dependency, as npm overrides and Yarn resolutions do.

Also known as npm overrides, Yarn resolutions, pnpm overrides, dependency resolutions, selective dependency resolutions, package overrides, override transitive dependency

Dependency overrides are root-level rules, such as npm overrides, Yarn resolutions, and pnpm overrides, that make the package manager ignore the version range a package declared for its own dependency and install the version you choose instead. Teams use them to patch vulnerable transitive dependencies, but they bypass the semver contract that kept the tree working.

A dependency override is the only lever a project has over code it never declared. Every other version in your tree is chosen by the author of some transitive dependency, not by you. We reproduced the textbook case for this article. One line of npm overrides took npm audit from 2 high-severity findings to 0, and the app then threw TypeError: getNCheck is not a function on its first CSS selector. Our position: dependency overrides are the right emergency tool and the wrong default. Use one to evict a malicious version or a vulnerability your code can actually reach. Keep it inside the range the parent declared whenever you can, and delete it once upstream ships the fix.

How does a package manager choose a transitive dependency version?

#

A package manager chooses each transitive version from a range the parent package declared, not from anything you wrote. css-select@2.1.0 declares "nth-check": "^1.0.2". Under semantic versioning, the caret means any version from 1.0.2 up to, but not including, 2.0.0. The resolver installs the highest published version inside that range and records it in the lockfile. Your package.json only controls the edges that start at your project.

That division of authority is deliberate. The author of css-select is the only party who tested css-select against nth-check. Ranges let that author accept bug fixes automatically while refusing a major version that may change the API. Ranges also let the package manager share one copy of nth-check between every parent whose ranges overlap.

The design fails when a fix exists only in a new major version. nth-check had a regular expression denial of service, CVE-2021-3803, rated 7.5 (high) under CVSS 3.1. The fix first shipped in nth-check 2.0.1 on September 16, 2021. No version satisfying ^1.0.2 will ever contain the fix, so npm audit offers only a breaking upgrade of the parent:

Plain text
nth-check  <2.0.1
Severity: high
Inefficient Regular Expression Complexity in nth-check - https://github.com/advisories/GHSA-rp65-9cf3-cjxr
fix available via `npm audit fix --force`
Will install css-select@7.0.0, which is a breaking change
node_modules/nth-check
  css-select  <=3.1.0
  Depends on vulnerable versions of nth-check
  node_modules/css-select

2 high severity vulnerabilities

The css-select chain is not contrived. react-scripts@5.0.1, the last release of Create React App, depends on @svgr/webpack, which reaches svgo@1, then css-select@^2.0.0, then nth-check@^1.0.2. Every app generated from it inherits a finding the app's own package.json cannot reach. With Create React App's deprecation in February 2025, no upstream release is coming to remove it. That gap is what dependency overrides exist to fill.

What are npm overrides?

#

npm overrides are an overrides object in the root package.json that replaces the spec of any matching dependency edge in the tree. npm added the field in version 8.3.0, published on December 9, 2021. The npm documentation states the scope plainly: "Overrides provide a way to replace a package in your dependency tree with another version, or another package entirely."

A global override rewrites every edge that names the package:

JSON
{
  "dependencies": { "css-select": "2.1.0" },
  "overrides": { "nth-check": "2.0.1" }
}

A nested override rewrites the edge only beneath one parent, optionally one parent version. This form limits the blast radius to the subtree you actually tested:

JSON
{
  "dependencies": { "css-select": "2.1.0" },
  "overrides": {
    "css-select@2.1.0": { "nth-check": "2.0.1" }
  }
}

npm records the substitution where you can see it. npm ls marks the node overridden, and npm explain prints the range the parent originally asked for:

Plain text
nth-check@2.0.1 overridden
node_modules/nth-check
  overridden nth-check@"2.0.1" (was "^1.0.2") from css-select@2.1.0
  node_modules/css-select
    css-select@"2.1.0" from the root project

Three rules catch people. First, npm only reads overrides from the root project: "Overrides are only considered in the root package.json file for a project." Second, npm refuses to override a package you also depend on directly unless the specs match. We hit the error by declaring "nth-check": "^1.0.2" as a direct dependency:

Plain text
npm error code EOVERRIDE
npm error Override for nth-check@^1.0.2 conflicts with direct dependency

The fix is a $ reference, "nth-check": "$nth-check", which makes the override reuse whatever spec your direct dependency declares. Third, the override is invisible in the lockfile's dependency edges. The root entry of package-lock.json does not record overrides, and the parent entry still declares the old range:

JSON
"node_modules/css-select": {
  "version": "2.1.0",
  "dependencies": {
    "nth-check": "^1.0.2"
  }
},
"node_modules/nth-check": {
  "version": "2.0.1",
  "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz"
}

A reviewer reading only the lockfile sees an edge whose installed version does not satisfy its declared range. The explanation lives in package.json. npm does enforce the pairing: when we changed the override to 2.1.1 without reinstalling, npm ci stopped with EUSAGE and "lock file's nth-check@2.0.1 does not satisfy nth-check@2.1.1".

The feature also had a long-lived correctness bug. From 2022 until npm 11.2.0, released March 5, 2025, an override could silently revert to the original, vulnerable version after the first install. We covered the fix, which Socket and Seal Security pushed through upstream, in the npm overrides bug write-up. If a project depends on overrides for a security fix, run npm 11.2.0 or later.

How do you force a transitive dependency version?

#

You force a transitive dependency version by writing a dependency override in the root project, and every major package manager has one. The implementations differ in exactly the places that matter for safety: scope, where the rule is honored, and whether it can cross a major version. The comparison below reflects each tool's documentation as of November 2026. In Yarn, the field is resolutions, which accepts one level of parent/child scoping and is only honored in the root workspace. In pnpm, overrides lives in pnpm-workspace.yaml and supports parent>child selectors, version selectors such as nth-check@<2.0.1, and "-" to delete a dependency edge outright.

pnpm also has the most careful mode we found. A convergence override, written with an empty range such as "nth-check@": "2.0.1", rewrites only the edges whose declared range the new version already satisfies. We ran it on pnpm 12.16.1 against css-select's ^1.0.2 edge. pnpm left nth-check at 1.0.2 and printed no warning, because 2.0.1 does not satisfy ^1.0.2.

EcosystemMechanismScope controlHonored whereCan cross a declared major?
npmoverrides in package.jsonNested objects, parent@version keys, $ referencesRoot package.json onlyYes
Yarn (Berry)resolutions in package.jsonOne level: parent/childRoot workspace; warns elsewhereYes
pnpmoverrides in pnpm-workspace.yamlparent>child, version selectors, "-" deletes an edgeRoot onlyYes; convergence overrides never do
Bunoverrides or resolutionsOne parent level; deeper paths ignored with a warningProject package.jsonYes
Maven<dependencyManagement>Per artifactCurrent POM and its childrenYes
Gradleconstraints {} with because()Per moduleConsuming buildYes, including downgrades
NuGetCentral Package Management transitive pinningPer packageDirectory.Packages.propsUpgrades only; a downgrade fails with NU1109
Cargo[patch]Per source and crateWorkspace root Cargo.tomlNo; the patch must satisfy the dependent's requirement
GoreplacePer module path and versionMain module onlyYes
uvoverride-dependenciesPer package, optionally per versionProject pyproject.tomlYes

The table matches each tool's own reference. Bun "supports only one parent level." NuGet states that "downgrades are not allowed when transitive pinning a package." Go's module reference says replace directives "only apply in the main module's go.mod file and are ignored in other modules." Cargo's reference makes [patch] root-only too, and the patched version must fall inside the requirement the dependent declared. uv's documentation calls overrides a last resort and notes they "can expand the set of acceptable versions," unlike pip-style constraints, which can only narrow them.

Our verdict on when to use which:

  • If the fixed version satisfies the parent's declared range, do not write an override. Re-resolve that lockfile entry instead, for example with pnpm audit --fix=update.
  • If the fix requires crossing the declared range, scope the override to the parent you tested: a nested object in npm, parent>child in pnpm, parent/child in Yarn.
  • Reserve global, unscoped overrides for evicting a known-malicious version everywhere during an incident.
  • Prefer modes that refuse to break a declared range. pnpm convergence overrides and NuGet's no-downgrade rule are the safest modes we found; npm and Yarn have no equivalent.

The override that passes npm audit and crashes at runtime

#

We reproduced the css-select case end to end on Node.js 22 with npm 10.9.9 and npm 12.2.1. The setup is the global override shown above, plus a test that parses three list items and selects every odd one:

JavaScript
const { parseDOM } = require("htmlparser2"); // htmlparser2@3.10.1
const CSSselect = require("css-select");
const dom = parseDOM("<ul><li>a</li><li>b</li><li>c</li></ul>");
console.log(CSSselect.selectAll("li:nth-child(2n+1)", dom).length);

Without the override, the script prints 2. With it, npm audit reports found 0 vulnerabilities, and the script fails:

Plain text
TypeError: getNCheck is not a function
    at nth-child (node_modules/css-select/lib/pseudos.js:60:20)

The cause is a module-shape change across the major version. css-select 2.1.0 calls require("nth-check") as a function. nth-check 2.x exports an object with parse, compile, and default keys. The scanner compared version numbers and was satisfied. Nothing checked that the parent still worked.

Declared ranges only

package.json: no overrides
overrides-demo (root)
declares "css-select": "2.1.0"
css-select@2.1.0
declares "nth-check": "^1.0.2"
nth-check@1.0.2
exports a function
  • npm audit2 high
  • :nth-child(2n+1)returns 2 nodes

With an npm override

"overrides": { "nth-check": "2.0.1" }
overrides-demo (root)
declares "css-select": "2.1.0"
css-select@2.1.0
declares "^1.0.2"
rewritten to "2.0.1" by the root; lockfile still records "^1.0.2"
nth-check@2.0.1 overridden
exports { parse, compile, default }
  • npm audit0 vulnerabilities
  • :nth-child(2n+1)TypeError
Figure 1. The same tree resolved with npm 12.2.1, without and with an override. The override fixes the version the scanner sees and breaks the API the parent calls.

Figure 1 shows both resolutions of the same tree. Without an override, the root declares css-select 2.1.0, css-select declares nth-check ^1.0.2, npm installs nth-check 1.0.2, which exports a function, npm audit reports 2 high findings, and the :nth-child(2n+1) selector returns 2 nodes. With "overrides": { "nth-check": "2.0.1" }, the root rewrites css-select's edge to 2.0.1 while the lockfile still records ^1.0.2, npm installs nth-check 2.0.1, which exports { parse, compile, default }, npm audit reports 0 vulnerabilities, and the same selector throws a TypeError.

Automated fixes make the same trade. pnpm audit --fix on this project wrote nth-check@<2.0.1: ^2.0.1 into pnpm-workspace.yaml. After pnpm install, nth-check resolved to 2.1.1, and the test failed with the identical TypeError. A tool that writes cross-major overrides from advisory data alone is guessing about compatibility.

The risk being traded away was small. In Create React App, nth-check ran inside svgo at build time over the developer's own SVG files. Dan Abramov reviewed the neighboring css-what finding in the same chain in npm audit: Broken by Design and concluded: "Verdict: this “vulnerability” is absurd in this context." A ReDoS that only your own build inputs can trigger is a weak reason to break your build. Reachability analysis, not the CVSS score, should decide whether a finding justifies an override.

When the vulnerability is reachable and the only fix is a new major, a dependency override is not the only option. You can patch the version you already have. Socket Certified Patches apply a small, reviewed change to the vulnerable code in the installed version, so no dependency edge crosses its declared range and no new upstream code enters the tree.

Can a dependency override install a different package entirely?

#

A dependency override can install a different package entirely, under the original package's name. npm accepts npm: aliases, file: paths, and git specifiers as override values. We pointed nth-check at an unrelated package to see what each tool reports:

JSON
{
  "dependencies": { "css-select": "2.1.0" },
  "overrides": { "nth-check": "npm:is-number@7.0.0" }
}
JSON
"node_modules/nth-check": {
  "name": "is-number",
  "version": "7.0.0",
  "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
}

The install succeeded, and npm audit reported found 0 vulnerabilities. The directory on disk is node_modules/nth-check, so require("nth-check") now loads is-number. npm ls does print nth-check@npm:is-number@7.0.0, but only if someone runs it and reads the line. A one-line diff in package.json replaced every copy of nth-check in the tree. That is why we analyze what is actually inside each installed package rather than trusting the name the tree assigns it, and why changes to overrides and resolutions deserve the same review as adding a new dependency.

Newer defaults close part of this channel. npm v12 blocks git and remote-URL dependencies unless allowed. Our git override, "nth-check": "github:fb55/nth-check#v2.0.1", failed on npm 12.2.1 with EALLOWGIT and the message Fetching packages of type "git" have been disabled. With --allow-git=all, npm installed it but held its prepare script for approval. pnpm 11 turned on blockExoticSubdeps, which limits git and tarball sources to direct dependencies. Neither default touches the npm: alias path, because the replacement comes from the registry like any other package.

The same mechanism has legitimate uses. Socket Optimize writes overrides that alias common packages to tested drop-in replacements from the Socket registry. is-regex@1.2.1 declares 4 dependencies, and @socketregistry/is-regex@1.0.10 declares none. Socket raises the Socket optimized override available alert when such a replacement exists, and npx socket optimize writes the overrides. The Socket Optimize announcement describes the tests each replacement must pass.

Where dependency overrides stop applying

#

Dependency overrides govern exactly one install: the root project's. Three common paths fall outside it.

Published libraries cannot hand their overrides to consumers. npm ignores overrides in any installed dependency, Cargo ignores [patch] outside the workspace root, Go ignores replace outside the main module, pnpm reads overrides only at the project root, and Yarn warns about resolutions outside the root workspace. If your library needs a forced version, every consumer must repeat the override. The one npm mechanism that does pin a published package's subtree for its consumers is npm-shrinkwrap.json, which is exactly why we flag shrinkwrapped packages as a supply chain risk.

Tools run through npx or installed globally resolve their own tree. We ran npm exec --package=css-select@2.1.0 from a directory whose package.json overrides nth-check to 2.0.1. npm 10.9.9 and npm 12.2.1 both installed nth-check 1.0.2 into ~/.npm/_npx/.

The npx gap matters most during incidents. In the axios compromise on March 31, 2026, axios@1.14.1 and axios@0.30.4 pulled in the malicious plain-crypto-js@4.2.1. An override such as "axios": "1.14.0" pins every transitive copy of axios in your project's tree; if axios is also a direct dependency, pin it there or use $axios. The override does nothing for a CLI that a CI job fetches with npx. Our analysis of the hidden blast radius traced exposure through exactly those paths, where no project lockfile or override applied.

Overrides also act only at resolution time. An existing node_modules keeps the old version until the next install, and a lockfile generated before the override keeps the old entry until it is re-resolved. As shown above, npm ci refuses to proceed when the two disagree, which is the behavior you want in CI.

Every override needs a reason and an expiry

#

Dependency overrides accumulate, and none of npm, Yarn, or pnpm records why one exists. JSON has no comments, so the justification lives in a commit message, if anywhere. Gradle gets this right: its constraints take a because() string that travels with the rule. pnpm gets half of it right: convergence overrides warn when every declared range already admits a newer version.

We think every package manager should require a reason on each override and warn when the parent's own range has caught up. Until then, scope each override to the parent you tested, link the advisory in the pull request, and delete the override when the parent ships a release that declares the fix. A forgotten exact-version override is a pin nobody chose: it keeps forcing that version after the parent moves on, including past the next security fix.