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
- Node.js and npm on PATH.
- A project that calls a tool from an npm script. The output below comes from this one, with a private copy of prettier installed into a directory beside it rather than into the machine's global prefix:
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
- Step 7 runs this file. Save it as
script-bins.mjsin the project root:
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'));
}
}
npm lsexits 1 when the named package is not in the tree, which makes it usable as a gate.
Steps
- Step 1.
List what is installed globally on this machine.
npm ls -g --depth=0C:\nvm4w\nodejs -> .\ +-- corepack@0.34.6 `-- npm@10.9.8--depth=0keeps the list to packages installed on purpose. Without it npm prints their dependencies as well. - Step 2.
Find where those packages live, so you can tell a global copy from a local one later.
npm root -gC:\nvm4w\nodejs\node_modulesUnder a version manager this path contains the Node version, so switching runtimes silently changes which globals exist.
- Step 3.
Ask the project whether it declares the tool at all.
npm ls prettierglobaldemo@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. - Step 4.
Probe with npx, the reflex most people reach for.
npx --no-install prettier --version3.9.6A 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.
- 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.
- 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.
- Step 7.
Audit every binary the scripts call against what the project actually installs.
node script-bins.mjsformat:check prettier NOT LOCALOne line per command, and a verdict that does not depend on your PATH. The same script on the h2check repository prints
node_modules/.binfor 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
What to check next
- How to check environment parity: the same question about runtimes and dependency versions rather than binaries.
- How to check node version of a project: the other machine property that never reaches the repository.
- How to check installed npm package version: what is on disk for a package the project does declare.
- How to check dependency tree npm: where a package came from once
npm lsdoes find it. - Pull request checklist: the review gate that catches a new script calling an undeclared tool.
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.
Related on this site
basic6 minpublished updated Maks Verny