How to check why a package is installed
Run npm explain <name>. It prints every installed copy, the path of each on disk, and the chain of requesters above it, ending at the root project. For semver in this repository the chain runs through sharp, then next, then the root. npm why is the same command.
Why check this
A tester reaches for this when an advisory, a licence report or a failed build names a package that is nowhere in package.json. The tree shows that the package is present. It does not say who asked for it, and that is the part a fix depends on.
The failure it prevents is a wasted upgrade. Pinning an advisory-hit package in your own manifest looks like a fix and is silently undone the next time the real requester resolves its own range. The chain tells you which direct dependency has to move instead.
Prerequisites
- Node 22 and npm 10. Every output below came from npm 10.9.8 on Node 22.23.2.
- A project with node_modules installed, because
npm explainreads the installed tree. - The npm explain documentation for the accepted selectors.
- Outputs here are this site's repository on 2026-09-12.
Steps
- Step 1.
Ask for a package that no manifest of yours names.
npm explain semversemver@7.8.5 optional node_modules/semver semver@"^7.8.5" from sharp@0.35.4 node_modules/sharp optional sharp@"^0.35.4" from next@16.3.5 node_modules/next next@"^16.3.5" from the root projectRead it bottom to top. The root project wants next, next optionally wants sharp, sharp wants semver. The word
optionalon the first line describes how this copy entered the tree. - Step 2.
Ask for a package with several requesters.
npm explain picocolorspicocolors@1.1.1 node_modules/picocolors picocolors@"^1.1.1" from @babel/code-frame@7.29.7 node_modules/@babel/code-frame @babel/code-frame@"^7.23.5" from next-mdx-remote@6.0.0 node_modules/next-mdx-remote next-mdx-remote@"^6.0.0" from the root project picocolors@"^1.1.1" from postcss@8.5.23 node_modules/next/node_modules/postcss … picocolors@"^1.1.1" from postcss@8.5.28 node_modules/postcss …One copy, three requesters. The three
picocolors@"^1.1.1" fromlines sit at the same indentation, so they are alternatives, not a chain. Removing one of them leaves the package installed. - Step 3.
Ask for a package installed twice.
npm explain zodzod@3.25.76 dev node_modules/chromium-bidi/node_modules/zod zod@"^3.24.1" from chromium-bidi@17.0.2 node_modules/chromium-bidi chromium-bidi@"17.0.2" from puppeteer-core@25.10.0 node_modules/puppeteer-core dev puppeteer-core@"^25.10.0" from the root project zod@4.1.8 dev node_modules/zod dev zod@"4.1.8" from the root projectTwo blocks separated by a blank line, two directories, two major versions.
npm ls --depth=0on the same tree printszod@4.1.8and nothing else. - Step 4.
Narrow the answer to one copy when the output is long.
npm explain zod@3.25.76zod@3.25.76 dev node_modules/chromium-bidi/node_modules/zod zod@"^3.24.1" from chromium-bidi@17.0.2 node_modules/chromium-bidi chromium-bidi@"17.0.2" from puppeteer-core@25.10.0 node_modules/puppeteer-core dev puppeteer-core@"^25.10.0" from the root projectA folder path works as a selector too, so
npm explain node_modules/chromium-bidi/node_modules/zodprinted the same block here. - Step 5.
Ask about a package nobody requires.
npm explain @emnapi/runtime@emnapi/runtime@1.11.3 extraneous node_modules/@emnapi/runtime @emnapi/runtime@"^1.11.3" from @img/sharp-wasm32@0.35.4 node_modules/@img/sharp-wasm32The chain stops at
@img/sharp-wasm32and never reaches the root project, because that package isextraneousas well. Two directories on disk, no manifest behind either.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| The last line is from the root project | A direct dependency of yours leads here | Change the range on that direct dependency. |
| The chain ends before the root project | Nothing in the tree requires this copy | Remove node_modules and run npm ci, then ask again. |
| Two blocks separated by a blank line | The package is installed in two directories | Decide which copy the failing code loads before upgrading either. |
| optional, dev or peer before a name | How that edge entered the tree | A dev chain does not ship, so an advisory on it has a different weight. |
Common mistakes
What to check next
- How to check dependency tree npm: the whole tree this command answers one question about.
- How to check unused dependencies in package json: finds the declared packages that no chain of yours needs.
- How to check npm packages for vulnerabilities: produces the package names this command explains.
- How to check if package lock is in sync: confirms the tree you explained is the tree CI will build.
FAQ
How do I find what depends on a package in npm?
npm explain <name>. npm why <name> is an alias and printed byte-identical output here. Both read the installed tree, so run an install first.
Why does a package appear that is not in my package.json?
Because something you do depend on requires it. The chain in the output names that dependency on its last line. Only direct dependencies appear in package.json.
What does extraneous mean in the output?
The directory is installed and no manifest in the tree asks for it. npm ci rebuilds node_modules from the lockfile, which is the fastest way to confirm whether it should be there.
Can I ask about one specific version?
Yes. npm explain zod@3.25.76 selected one of the two copies here, and a directory path such as node_modules/chromium-bidi/node_modules/zod selected the same one.
Verified
Verified by Maks Vernynpm 10.9.8Node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic5 minpublished updated Maks Verny