npx and Global Packages
Node.js ships with two related but different tools for running command-line packages: global installs via npm install -g, and npx, a package runner bundled with npm since version 5.2. A global install copies a package onto your system and links its executable onto your PATH so you can call it from any directory forever after. npx instead runs a package’s binary on demand — reusing a local copy if your project already has one, or fetching it temporarily if not — without permanently installing anything. Getting this distinction right matters: global installs drift out of date, cause version mismatches between projects, and often need extra permissions, while npx keeps commands reproducible and disposable. This lesson covers how both mechanisms work under the hood, how to build and run your own CLI tool, and the mistakes that trip up most Node.js developers.
Overview: How npx and Global Installs Work
Every npm package can declare a bin field in its package.json, mapping a command name to a script file. When you run npm install -g <package>, npm copies that package into a global modules directory (find it with npm root -g) and creates a symlink for each of its bin entries inside npm’s global bin folder, which is <prefix>/bin on macOS/Linux (find the prefix with npm config get prefix) or the prefix folder itself on Windows. As long as that bin folder is on your shell’s PATH, the command becomes available everywhere on your machine, in every project, forever — until you explicitly npm uninstall -g it.
That “everywhere, forever” behavior is exactly the problem npx solves. Two projects on the same machine might need two different major versions of the same CLI tool, and a single global install can only satisfy one of them at a time. npx, bundled with npm since 5.2.0 and effectively a thin wrapper around npm exec since npm 7, resolves a command in this order: first it looks for a matching binary in the current project’s node_modules/.bin (walking up parent directories the same way Node resolves modules), so a project-local devDependency always wins. If nothing local matches, it checks its own download cache. If it still finds nothing, it downloads the package from the registry into a temporary, version-keyed folder, installs only what that single run needs, and executes it. Nothing is written to your project’s package.json or node_modules, and nothing is installed globally.
Why this design matters
Because npx prefers the local copy first, adding a tool as a devDependency and invoking it through npx (or through an npm script) guarantees every contributor and every CI run uses the exact version pinned in package.json — no “it works on my machine because I have version 9 installed globally” bugs. Because it falls back to a temporary download, you can also try a scaffolding tool or a one-off utility without committing to installing it anywhere at all.
Syntax
| Command | What it does |
|---|---|
npx <package> [args] |
Run a package’s bin, preferring a local copy, otherwise downloading it temporarily |
npx -y <package> |
Same, but auto-confirm the install prompt (required for CI/non-interactive shells) |
npx <package>@<version> |
Run a specific, pinned version instead of whatever is cached or latest |
npx --package=<pkg> <command> |
Run <command> from a package whose bin name differs from the package name |
npx . |
Run the bin defined in the current directory’s package.json, without publishing or linking it first |
npm install -g <package> |
Install a package globally and link its bin onto PATH |
npm uninstall -g <package> |
Remove a globally installed package |
npm list -g --depth=0 |
List top-level globally installed packages |
npm root -g |
Print the directory where global packages live |
npm config get prefix |
Print npm’s global install prefix (bin folder is prefix/bin on Unix) |
Examples
Example 1: Running a package once, with nothing left behind
npx cowsay "Hello Node"
Output:
______________
< Hello Node >
--------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
The first run downloads cowsay into npx’s cache and executes it; nothing is added to any package.json, and there is no node_modules folder involved at all. Run the exact same command again and it returns almost instantly, because npx reuses the cached copy instead of re-downloading it.
Example 2: Serving files without installing a server
npx http-server ./public -p 8080
Output:
Starting up http-server, serving ./public
Available on:
http://127.0.0.1:8080
http://192.168.1.5:8080
Hit CTRL-C to stop the server
This is a common use of npx: you need a static file server for five minutes to check something, and installing http-server globally (or adding it to a project that will never use it again) would be overkill. npx fetches it, runs it in the foreground, and once you hit Ctrl+C nothing permanent is left on disk beyond the shared npx cache.
Example 3: Building a CLI tool that npx can run
A package becomes runnable through npx the moment it declares a bin field. Here is a minimal CLI tool with two files:
{
"name": "greet-cli",
"version": "1.0.0",
"bin": {
"greet": "./bin/greet.js"
}
}
#!/usr/bin/env node
const name = process.argv[2] ?? "World";
console.log(`Hello, ${name}! This CLI is running via npx.`);
cd greet-cli
npx . Node
Output:
Hello, Node! This CLI is running via npx.
npx . tells npx “treat the current directory as the package,” so it reads package.json‘s bin field and runs bin/greet.js directly — no npm link, no global install, no publishing required while you are developing it. Once this package is actually published to the npm registry, anyone can run npx greet-cli Node and get the same result, downloaded on the fly.
How npx Resolves a Command: Step by Step
- npx parses the command name and checks whether it exists as a binary in
node_modules/.binin the current directory, then each parent directory up to the filesystem root — exactly like Node’s module resolution walks up looking fornode_modules. - If a local match is found, npx runs that binary directly. This is why a project’s own pinned
devDependencyalways takes priority over anything global or cached. - If no local match exists, npx checks its own cache directory (
~/.npm/_npxby default) for a previously downloaded copy matching the requested package and version spec. - If nothing is cached, npx prints an install confirmation (unless
-y/--yeswas passed) and, once confirmed, installs the package and its dependencies into a uniquely hashed folder under the cache. - npx reads the resolved package’s
binfield, spawns Node to execute that file with any remaining command-line arguments, and forwards stdout, stderr, and the exit code back to your shell. - The downloaded files stay in the cache afterward — contrary to a common assumption, current npx does not delete them after the run — so the next invocation with the same version spec skips the download entirely.
Common Mistakes
Mistake 1: Forgetting to auto-confirm in CI
Since npx requires confirmation before downloading and running a package it doesn’t recognize, a script that works fine in your interactive terminal can hang forever in a non-interactive CI shell that has no TTY to answer the prompt.
Wrong (hangs waiting for input in CI):
npx create-react-app my-app
Corrected:
npx --yes create-react-app my-app
Mistake 2: Reaching for sudo on EACCES errors
On macOS and Linux, npm’s default global prefix is often a root-owned directory, so npm install -g fails with an EACCES permission error. The instinctive fix — prefixing the command with sudo — works, but it also means future global installs and updates need sudo too, and it’s easy to end up with root-owned files inside your own home directory.
Wrong:
sudo npm install -g nodemon
Better — point npm’s prefix at a folder you own:
npm config set prefix ~/.npm-global
export PATH=~/.npm-global/bin:$PATH
npm install -g nodemon
An even more common fix today is skipping global installs entirely with a Node version manager such as nvm, which installs Node (and npm’s global prefix) entirely inside your home directory, so no sudo is ever needed.
Mistake 3: Assuming npx always runs the latest version
Because npx caches downloads under ~/.npm/_npx, running npx some-tool repeatedly can quietly keep using a version that was current the first time you ran it, even after a newer release ships. The cache is keyed by the exact version resolved at download time, so this is usually harmless, but it surprises people who expect every invocation to “check for updates.”
Pin the version explicitly when it matters, especially for scaffolding tools where reproducibility matters:
npx cowsay@1.5.0 "Pinned version"
Or clear npx’s cache if you suspect it is serving something stale:
npx clear-npx-cache
Best Practices
- Prefer adding CLI tools as
devDependenciesand invoking them throughnpxor an npm script, so every contributor and every CI run uses the exact version recorded inpackage.json. - Reserve
npm install -gfor tools you genuinely want available everywhere regardless of project — a version manager, npm itself, a personal utility — not project build tooling. - Always pass
-y/--yesto npx in CI pipelines and other non-interactive scripts to avoid hanging on the install confirmation prompt. - Pin an exact version with
npx <package>@<version>for scaffolding commands you want to be reproducible across machines and over time. - Fix
EACCESpermission errors by changing npm’s global prefix or switching to a version manager like nvm, rather than routinely running npm withsudo. - Give your own CLI packages a
binfield inpackage.jsonso they’re runnable withnpx .immediately after cloning, with no separate install or link step. - Run
npm list -g --depth=0occasionally to audit what’s actually installed globally on a machine, and remove anything you no longer use.
Practice Exercises
- Run
npx cowsay "your name"twice in a row and time both runs. Explain in one sentence why the second run finishes noticeably faster than the first. - Create a two-file package like the
greet-cliexample in this lesson, with abinfield pointing at your script. Confirm you can run it withnpx .and that it correctly receives a command-line argument. - Install a small package globally (for example,
npm install -g nodemon), then usenpm root -gandnpm config get prefixto locate exactly where it was installed on your system. Uninstall it afterward withnpm uninstall -g nodemon.
Summary
npm install -g <package>puts a package on your system’sPATHso it’s available in every directory, but that also means every project on the machine shares one global version.npxruns a package’s bin on demand: it prefers a localnode_modules/.bincopy first, then a cached download, and only fetches from the registry when neither exists.- Downloaded packages are cached under
~/.npm/_npxand reused on later runs with the same version spec — they are not deleted after each use. - Passing
-y/--yesskips the install confirmation prompt, which is required in CI and other non-interactive environments. - Prefer
devDependenciesplusnpx(or npm scripts) over global installs for anything tied to a specific project, to keep tool versions reproducible across machines. - Give your own tools a
binentry inpackage.jsonsonpx .can run them directly during development.
