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

Steps

  1. Step 1.

    Ask for a package that no manifest of yours names.

    npm explain semver
    
    semver@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 project

    Read it bottom to top. The root project wants next, next optionally wants sharp, sharp wants semver. The word optional on the first line describes how this copy entered the tree.

  2. Step 2.

    Ask for a package with several requesters.

    npm explain picocolors
    
    picocolors@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" from lines sit at the same indentation, so they are alternatives, not a chain. Removing one of them leaves the package installed.

  3. Step 3.

    Ask for a package installed twice.

    npm explain zod
    
    zod@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 project

    Two blocks separated by a blank line, two directories, two major versions. npm ls --depth=0 on the same tree prints zod@4.1.8 and nothing else.

  4. Step 4.

    Narrow the answer to one copy when the output is long.

    npm explain zod@3.25.76
    
    zod@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

    A folder path works as a selector too, so npm explain node_modules/chromium-bidi/node_modules/zod printed the same block here.

  5. 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-wasm32

    The chain stops at @img/sharp-wasm32 and never reaches the root project, because that package is extraneous as 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

Sign: The output of npm explain looks truncated because it never says from the root project.Cause: That is the answer, not a cut. A chain reaching no root means no manifest in the tree requires the copy. Here npm explain @emnapi/runtime stopped at an extraneous parent, and the command still exited 0, so a script checking only the exit status learns nothing.
Sign: Three from lines under one copy are read as a three-level chain.Cause: Indentation carries the chain. Lines at the same indentation are separate requesters of the same copy. picocolors above has three of them, so dropping next-mdx-remote would leave picocolors installed by both copies of postcss.
Sign: A package is pinned in package.json to resolve an advisory, and the old version comes back.Cause: Adding your own entry creates a second requester, it does not change the range the real requester asks for. npm can satisfy both by installing two copies, exactly as it did for zod here. Upgrade the direct dependency the chain names.

What to check next

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.

basic5 minpublished updated Maks Verny