How to check if a npm package is installed globally

Run npm ls -g --depth=0 for the global list and npm ls prettier for the project. A CLI a script calls that neither reports comes from somewhere else. Here npm run format:check passed with a global prettier on PATH and failed with prettier is not recognized once it was removed.

Why check this

A global install is a property of one laptop. It is not in package.json, not in the lockfile, and not in the repository, so it survives every review and disappears on a fresh runner. The build that depended on it fails with a message about an unrecognised command, which points at the CI configuration rather than at the missing dependency.

Run this when a script works for you and fails for a colleague, when onboarding a new machine, and before the first pipeline run of a new repository. The failure it prevents is a release blocked by a formatter or a migration tool that nobody remembers installing.

Prerequisites

mkdir globaldemo && cd globaldemo && mkdir src
printf '{\n  "name": "globaldemo",\n  "version": "1.0.0",\n  "private": true,\n  "scripts": { "format:check": "prettier --check src" },\n  "devDependencies": {}\n}\n' > package.json
printf 'export const a = 1;\n' > src/index.js
npm install -g --prefix ../globalprefix prettier@3.6.2
import { readFileSync, existsSync, readdirSync } from 'node:fs';

const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
const localBins = new Set(
  existsSync('node_modules/.bin')
    ? readdirSync('node_modules/.bin').map((f) => f.replace(/\.(cmd|ps1)$/, ''))
    : []
);
const shell = new Set(['node', 'npm', 'npx', 'cd', 'rm', 'cp', 'echo', 'set']);

for (const [name, cmd] of Object.entries(pkg.scripts ?? {})) {
  for (const part of cmd.split(/&&|\|\||;/)) {
    const bin = part.trim().split(/\s+/)[0];
    if (!bin || shell.has(bin) || bin.startsWith('"')) continue;
    console.log(name.padEnd(14) + ' ' + bin.padEnd(12) + ' ' + (localBins.has(bin) ? 'node_modules/.bin' : 'NOT LOCAL'));
  }
}

Steps

  1. Step 1.

    List what is installed globally on this machine.

    npm ls -g --depth=0
    
    C:\nvm4w\nodejs -> .\
    +-- corepack@0.34.6
    `-- npm@10.9.8

    --depth=0 keeps the list to packages installed on purpose. Without it npm prints their dependencies as well.

  2. Step 2.

    Find where those packages live, so you can tell a global copy from a local one later.

    npm root -g
    
    C:\nvm4w\nodejs\node_modules

    Under a version manager this path contains the Node version, so switching runtimes silently changes which globals exist.

  3. Step 3.

    Ask the project whether it declares the tool at all.

    npm ls prettier
    
    globaldemo@1.0.0 C:\Users\khark\…\scratchpad\globaldemo
    `-- (empty)

    Exit code 1. (empty) means the package is nowhere in this project's tree, at any depth.

  4. Step 4.

    Probe with npx, the reflex most people reach for.

    npx --no-install prettier --version
    
    3.9.6

    A version, exit code 0, from a project with no prettier and a machine with no global prettier. The number does not match the 3.6.2 installed beside the project either. The first note under Common mistakes says where it came from.

  5. Step 5.

    Run the script with the private prefix on PATH, which is what a developer's machine looks like.

    PATH="$PWD/../globalprefix:$PATH" npm run format:check
    
    > globaldemo@1.0.0 format:check
    > prettier --check src
    
    Checking formatting...
    All matched files use Prettier code style!

    Exit code 0. Nothing in the repository explains why this worked.

  6. Step 6.

    Run it again without that directory on PATH, which is what a clean runner looks like.

    npm run format:check
    
    > globaldemo@1.0.0 format:check
    > prettier --check src
    
    'prettier' is not recognized as an internal or external command,
    operable program or batch file.

    Exit code 1. The message names the shell, not the missing dependency, which is why this gets filed as a CI problem.

  7. Step 7.

    Audit every binary the scripts call against what the project actually installs.

    node script-bins.mjs
    
    format:check   prettier     NOT LOCAL

    One line per command, and a verdict that does not depend on your PATH. The same script on the h2check repository prints node_modules/.bin for all twelve of its script entries.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | A tool in npm ls -g and not in npm ls | The project relies on your machine | Add it to devDependencies and reinstall. | | (empty) from npm ls, exit 1 | Not a dependency of this project at any depth | Check whether a script calls it anyway, as in step 7. | | NOT LOCAL from the audit | A script calls a binary the install does not provide | Either add the package or replace the call with node. | | Script passes for you, fails on a runner | PATH differs, not the code | Compare step 5 and step 6 before reading the CI logs. | | npm ls -g lists only npm and corepack | A clean global prefix | Any tool a script resolves is local, cached or on PATH from elsewhere. |

Common mistakes

Sign: npx --no-install answers with a version, so the tool is assumed to be installed.Cause: npx also resolves from its own cache under npm-cache/_npx. Here it printed 3.9.6 with no local dependency and no global install, from a cache entry whose package.json declares prettier ^3.9.6. The flag stops npx downloading, and it does not stop npx finding an earlier download.
Sign: A global install is added to fix a failing pipeline, and the pipeline keeps failing on other agents.Cause: npm root -g under a version manager points inside the active Node version directory. Installing globally on Node 22 and then switching to Node 20 leaves the package present and unreachable, with no error to explain it.
Sign: The audit is run and reports nothing because node_modules is missing.Cause: The check compares scripts against node_modules/.bin, so it needs an install to have happened. With no node_modules the set is empty and every entry reads NOT LOCAL. Run npm ci first, then the audit.

What to check next

FAQ

How do I check if an npm package is installed globally?

npm ls -g --depth=0 prints the global list, and npm ls -g prettier prints (empty) and exits 1 when that package is absent. Both read the directory that npm root -g reports, which changes with the active Node version.

Why does npx find a package that is not installed?

npx keeps downloaded packages in a cache directory and reuses them. Step 4 shows --no-install returning 3.9.6 from that cache. Use npm ls against the project when the question is whether the project declares the tool.

Should CLI tools be global or local?

Local, as a devDependency, so the version is in the lockfile and the binary lands in node_modules/.bin, which npm puts on PATH for scripts. Keep global installs for things that create projects rather than build them.

How do I reproduce a clean runner locally?

Run the script from a shell with the global bin directory removed from PATH, as in step 6. That reproduced the exact CI message here without a container, which this machine does not have.

Does npm ls check devDependencies too?

Yes. It walks the installed tree, so a devDependency present in node_modules is reported. A package listed in package.json but never installed is reported as missing, which is a different finding.

Verified

Verified by Maks Vernynpm 10.9.8node 22.23.2prettier 3.6.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.

basic6 minpublished updated Maks Verny