# 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

Source: https://socket.dev/glossary/dependency-overrides

Last updated: 2026-11-03

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](https://socket.dev/glossary/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](https://socket.dev/glossary/semantic-versioning-semver), 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](https://github.com/advisories/GHSA-rp65-9cf3-cjxr), 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:

```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](https://socket.dev/blog/create-react-app-officially-deprecated) 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](https://github.com/npm/cli/releases/tag/v8.3.0), published on December 9, 2021. The [npm documentation](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#overrides) 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:

```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:

```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](https://socket.dev/glossary/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](https://socket.dev/blog/npm-overrides-bug-fixed) 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](https://socket.dev/glossary/yarn-package-manager), the field is `resolutions`, which accepts one level of `parent/child` scoping and is only honored in the root workspace. In [pnpm](https://socket.dev/glossary/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](https://pnpm.io/settings/dependency-resolution#overrides), 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`.

| Ecosystem | Mechanism | Scope control | Honored where | Can cross a declared major? |
| --- | --- | --- | --- | --- |
| npm | overrides in package.json | Nested objects, parent@version keys, $ references | Root package.json only | Yes |
| Yarn (Berry) | resolutions in package.json | One level: parent/child | Root workspace; warns elsewhere | Yes |
| pnpm | overrides in pnpm-workspace.yaml | parent\>child, version selectors, "-" deletes an edge | Root only | Yes; convergence overrides never do |
| Bun | overrides or resolutions | One parent level; deeper paths ignored with a warning | Project package.json | Yes |
| Maven | \<dependencyManagement\> | Per artifact | Current POM and its children | Yes |
| Gradle | constraints {} with because() | Per module | Consuming build | Yes, including downgrades |
| NuGet | Central Package Management transitive pinning | Per package | Directory.Packages.props | Upgrades only; a downgrade fails with NU1109 |
| Cargo | \[patch\] | Per source and crate | Workspace root Cargo.toml | No; the patch must satisfy the dependent's requirement |
| Go | replace | Per module path and version | Main module only | Yes |
| uv | override-dependencies | Per package, optionally per version | Project pyproject.toml | Yes |

The table matches each tool's own reference. [Bun](https://bun.sh/docs/pm/overrides) "supports only one parent level." [NuGet](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management) states that "downgrades are not allowed when transitive pinning a package." Go's [module reference](https://go.dev/ref/mod#go-mod-file-replace) says `replace` directives "only apply in the main module's go.mod file and are ignored in other modules." [Cargo's reference](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html) makes `[patch]` root-only too, and the patched version must fall inside the requirement the dependent declared. uv's [documentation](https://docs.astral.sh/uv/concepts/resolution/#dependency-overrides) 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:

```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.

> 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.
>
> The same dependency tree resolved twice. Without an override, css-select 2.1.0 declares nth-check ^1.0.2, npm installs 1.0.2, and npm audit reports 2 high findings. With an npm override, npm installs nth-check 2.0.1, the lockfile still records the ^1.0.2 edge, npm audit reports 0, and the first nth-child selector throws a TypeError.

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](https://overreacted.io/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](https://socket.dev/glossary/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](https://socket.dev/features/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](https://socket.dev/blog/npm-12) 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](https://socket.dev/blog/pnpm-11-adds-new-supply-chain-protection-defaults) 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](https://socket.dev/alerts/socketUpgradeAvailable) alert when such a replacement exists, and `npx socket optimize` writes the overrides. The [Socket Optimize announcement](https://socket.dev/blog/introducing-socket-optimize) 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](https://socket.dev/glossary/npm-shrinkwrap-json), which is exactly why we [flag shrinkwrapped packages](https://socket.dev/blog/understanding-the-security-concerns-of-npm-shrinkwrap) as a supply chain risk.

Tools run through [npx](https://socket.dev/glossary/npx-npm-package-runner) 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](https://socket.dev/blog/axios-npm-package-compromised) 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](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise) 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.
