npm & Package Management
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with
the differences of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out. The
version numbers were checked on 2026-10-03; every example was run on Node.js 26, and on 24 and 22 wherever the behaviour could
differ, plus npm 12.2.0 where the page says so. npm is a separate project that Node.js bundles, so the version you have depends on the Node.js line you installed:
Node.js 26.10.0 ships npm 11.19.1, Node.js 24.21.0 ships npm 11.19.0 and Node.js 22.23.3 ships npm 10.9.9. npm 12 (12.0.0 on
2026-07-08, latest 12.2.0 on 2026-09-30) is not bundled with any Node.js line yet; it is installed explicitly and supports
Node.js ^22.22.2 || ^24.15.0 || >=26.0.0. Unlike the Node.js API documentation, npm has no stability index: the commands
and configuration options below are either released (in the npm version stated) or not. Where a feature differs between npm 10,
11 and 12 the page says so, because the differences are largest in the security area, where npm 12 changes defaults. The
Bookshelf application that grows across this section uses almost no third-party code, which makes it a good place to learn
how little of it you should add and how to add it safely.
What npm is, and which one you have
npm is three things with one name: the public registry at registry.npmjs.org, the npm command line client, and the
package.json / package-lock.json file formats that describe a project and its dependency tree. The client is written in
JavaScript and runs on top of Node.js; the Node.js installers and release archives include a copy of it (see
Installing Node.js). The Learn article introduces npm install, the
--save-dev, --no-save and --save-optional flags and npm run; this page goes further. The article also notes that
Yarn and pnpm are alternatives to the npm client; they read the same registry. The
Workspaces & Publishing page compares them and covers monorepos.
| Node.js line | Bundled npm | Status | What this page means for you |
|---|---|---|---|
26 (26.10.0) |
11.19.1 |
Current; LTS from 2026-10-28 |
Install-script warnings, |
24 "Krypton" (24.21.0) |
11.19.0 |
Active LTS |
Same as Node.js 26 for everything on this page. |
22 "Jod" (22.23.3) |
10.9.9 |
Maintenance LTS |
No |
any supported line |
12.2.0 (installed explicitly) |
|
Dependency install scripts blocked by default; git and remote tarball dependencies refused by default. |
The release lines and their end-of-life dates are explained in Release Lines & LTS.
To see which npm you are running, and to try npm 12 without changing the npm that came with Node.js, install it into a
throwaway directory and run its binary from there (a global update with npm install -g npm@12 works too; npm itself prints
that hint when a newer version exists).
node --version
npm --version
mkdir npm12 && cd npm12
npm install npm@12.2.0
./node_modules/.bin/npm --version
On Node.js 26.10.0 this printed v26.10.0, 11.19.1 and 12.2.0; on Node.js 24.21.0 and 22.23.3 the middle line was 11.19.0
and 10.9.9. Every "npm 12" example below uses that locally installed binary (written npm12 in the prose).
A tour of the CLI
Most of the daily commands are verbs on the dependency tree. The tables and transcripts below come from a small scratch project; paths are shortened. Every command runs unchanged on npm 10, 11 and 12 unless stated.
npm install in its many forms
npm install (alias npm i) with no argument installs everything package.json and package-lock.json describe. With
arguments it adds packages, and the argument is a package spec: a name with an optional version, range or tag, an alias, a
folder, a tarball or a git location. Because npm 5 the command also records the dependency in package.json.
| Command | Entry written | Notes |
|---|---|---|
|
nothing |
Installs the whole tree from the lockfile (or resolves it if there is none). |
|
|
The latest version, saved with the |
|
|
A range or version you type is saved as typed, not widened. |
|
|
A dist-tag resolves to a concrete version first. |
|
|
No range at all; see SemVer ranges and |
|
|
An alias: two majors of one package side by side under different names. |
|
entry in |
|
|
entry in |
For libraries that must share the host’s copy. |
|
|
A folder is linked, a tarball is unpacked. Install scripts of a tarball run (see npm 12 and the supply-chain changes). |
|
git or URL specifier |
Refused by default on npm 12 ( |
|
nothing |
Installs without recording; |
|
nothing |
Prints |
|
lockfile only |
Updates |
Source: npm Docs: npm install
The table was produced by running the commands on npm 11.19.1 and spot-checked on 11.19.0 and 10.9.9. Two details save
confusion later. A version you type, such as 7.5, stays a loose range in package.json, so use --save-exact or an exact
version when you mean it. A GitHub shorthand such as github:user/repo resolves through git, which npm 12 refuses unless
allow-git is relaxed.
Looking at the tree: show, ls, explain
npm view (aliases show, info, v) reads registry metadata without installing anything. npm ls prints
what is installed, npm explain says why a package is there.
npm view semver version dist-tags.latest engines
npm ls
npm ls --all
npm explain semver
npm ls -g --depth=0
Source: npm Docs: npm view
version = '7.8.5'
dist-tags.latest = '7.8.5'
engines = { node: '>=10' }
demo@1.0.0 /path/to/demo
+-- prettier@3.3.3
`-- semver@7.8.5
semver@7.5.4 dev
node_modules/semver
dev semver@"7.5" from the root project
/path/to/nvm/versions/node/v26.10.0/lib
`-- npm@11.19.1
npm ls shows only direct dependencies unless you add --all; --omit=dev restricts it to what production installs; --json
gives machine-readable output; npm ls <name> shows every copy and the path that pulled it in. The last command lists the
global packages of the current Node.js version, here only the bundled npm itself. On npm 12, npm view --json always returns
an array, where npm 11 and 10 return an object for a single match.
outdated, update and uninstall
npm outdated compares installed, wanted and latest versions. Wanted is the highest version your range allows; latest is the
highest published. The command exits with code 1 when something is outdated, which makes it usable as a CI check (observed on npm 11.19 and 12.2).
npm install lodash@4.17.20
npm outdated
npm update lodash
npm ls lodash
npm uninstall lodash
Source: npm Docs: npm outdated
Package Current Wanted Latest Location Depended by
lodash 4.17.20 4.18.1 4.18.1 node_modules/lodash demo
prettier 3.3.3 3.3.3 3.9.9 node_modules/prettier demo
changed 1 package
demo@1.0.0 /path/to/demo
`-- lodash@4.18.1
The table explains two behaviours. prettier is pinned with --save-exact, so its wanted version equals the current one and
only a deliberate npm install prettier@latest moves it. npm update lodash moved the installed copy to the highest version the
range ^4.17.20 allows and rewrote the lockfile; as the documentation says, npm update does not change the ranges of direct
dependencies in package.json unless you add --save, so a fresh clone resolves the same version only through the lockfile. npm uninstall (aliases remove, rm, un, r, unlink) deletes the package and its entry. npm 12.1.0
made uninstall reject arguments that carry a version, such as npm uninstall lodash@4.
prune, dedupe and cache
npm prune removes packages from node_modules that package.json no longer asks for ("extraneous" packages); with
--omit=dev it also removes development dependencies. npm dedupe (alias ddp) flattens duplicate copies where ranges allow; npm find-dupes only
reports them.
npm install is-even
npm pkg delete dependencies.is-even
npm prune
Source: npm Docs: npm prune
The install added five packages (is-even and its transitive dependencies); after the entry was deleted from package.json,
npm prune printed removed 5 packages. npm pkg is the supported way to edit package.json from scripts: npm pkg get,
set and delete. On npm 12 its output is no longer forced to JSON, so a single value comes back bare.
npm downloads every tarball into a content-addressed cache (the cache setting; npm config get cache prints the folder) and
reuses it for later installs and for offline work.
npm cache ls
npm cache verify
npm install lodash --prefer-offline
Source: npm Docs: npm cache
npm cache ls lists entries, such as make-fetch-happen:request-cache:https://registry.npmjs.org/is-even; --prefer-offline uses cached metadata and tarballs without revalidating them; npm cache verify
checks integrity and garbage-collects unused content (it reported Content verified with the number of entries). The npm
documentation calls the cache self-healing, so npm cache clean --force is rarely the answer to an install problem. In CI, cache the
folder npm config get cache prints between runs and install with npm ci (see package-lock.json, npm ci and --omit=dev).
link: developing against a local copy
npm link makes a folder show up in another project’s node_modules as a symlink, which is how you try a library you are
editing in an application that uses it, without publishing.
cd appdemo
npm link ../libdemo
ls -l node_modules/@bookshelf
node --input-type=module -e "import { isbn } from '@bookshelf/isbn'; console.log(isbn())"
Source: npm Docs: npm link
The folder listing showed isbn → ../../../libdemo, the import printed 978-0000000000, and npm ls reported the link as
extraneous because linking does not write package.json. Running npm link without an argument inside the library links it
into the global folder instead, and npm unlink -g @bookshelf/isbn removes that. Remember that a linked package is resolved
through its real path, so a linked library that imports a peer dependency gets its own copy of it unless you take care. The
Workspaces & Publishing page shows workspaces, which cover the same need
more robustly inside one repository.
SemVer ranges and --save-exact
Semantic versioning (SemVer) gives the three numbers meaning: increment the first (MAJOR) for breaking changes, the middle
(MINOR) for backward-compatible features and the last (PATCH) for backward-compatible fixes. A range in package.json is a
promise about which of those you accept automatically. The npm documentation lists the common forms: patch releases 1.0,
1.0.x or ~1.0.4; minor releases 1, 1.x or ^1.0.4; and any release * or x.
semver package that npm itself usesimport semver from 'semver';
const published = ['1.2.3', '1.2.9', '1.3.0', '1.9.0', '2.0.0', '2.0.1-beta.1'];
for (const range of ['^1.2.3', '~1.2.3', '>=1.3.0 <2.0.0', '1.x', '1.2.3 - 2.0.0', '*']) {
console.log('picks', range.padEnd(16), '->', semver.maxSatisfying(published, range));
}
for (const range of ['^1.2.3', '^0.2.3', '^0.0.3', '~1.2.3']) {
console.log('range', range.padEnd(8), 'means', new semver.Range(range).range);
}
console.log(semver.satisfies('2.0.1-beta.1', '>=1.0.0'));
console.log(semver.satisfies('2.0.1-beta.1', '>=2.0.1-beta.0'));
picks ^1.2.3 -> 1.9.0
picks ~1.2.3 -> 1.2.9
picks >=1.3.0 <2.0.0 -> 1.9.0
picks 1.x -> 1.9.0
picks 1.2.3 - 2.0.0 -> 2.0.0
picks * -> 2.0.0
range ^1.2.3 means >=1.2.3 <2.0.0-0
range ^0.2.3 means >=0.2.3 <0.3.0-0
range ^0.0.3 means >=0.0.3 <0.0.4-0
range ~1.2.3 means >=1.2.3 <1.3.0-0
false
true
The output is identical on Node.js 26, 24 and 22. Three rules are worth memorising. The caret ^ allows everything that
does not change the left-most non-zero digit, so ^0.2.3 stays inside 0.2.x and ^0.0.3 accepts only 0.0.3: before
the first stable release, every minor release may break. The tilde ~ allows patch releases only. A prerelease such as 2.0.1-beta.1 does not match
a plain range like >=1.0.0; it matches only a range that names a prerelease on the same major.minor.patch, so * and ^
do not pull betas by accident. Remember that the range only describes what may be installed: the lockfile decides what is.
Two settings change what npm install <pkg> saves. --save-exact (-E) writes the exact version, and
--save-prefix='~' writes a tilde range; both can be made permanent in a project .npmrc with npm config set save-exact true
--location=project. The scratch run produced these entries on npm 11.19.1, 11.19.0 and 10.9.9:
{
"dependencies": {
"is-even": "~1.0.0",
"is-odd": "^3.0.1",
"semver": "7.8.5"
}
}
Exact pins give reproducibility for the one package you pinned, but not for its transitive dependencies; only the lockfile
fixes the whole tree. Pin directly when a tool must not move without review (formatters and code generators are typical), and
let the lockfile plus a regular npm outdated review handle the rest.
package-lock.json, npm ci and --omit=dev
package-lock.json is written automatically whenever npm changes node_modules or package.json. It records the exact
tree: every package location, version, registry URL (resolved), content hash (integrity) and whether it is a development or
optional dependency. Commit it for every application. lockfileVersion: 3 is what npm 9 and later write, and what the scratch
run produced. The excerpt below is the entry for one package (the hash is shortened):
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO75…",
"dev": true,
"license": "MIT",
"bin": { "prettier": "bin/prettier.cjs" },
"engines": { "node": ">=14" }
}
Source: npm Docs: package-lock.json
npm install treats the lockfile as a starting point: it may update it when package.json asks for something the lock does
not satisfy. npm ci ("clean install", aliases clean-install, ic) treats it as a contract. It requires a lockfile, fails
instead of changing it when package.json disagrees, deletes any existing node_modules first, can only install the whole
project, and never writes package.json or package-lock.json. That is what you want in CI, in Docker builds and on
deployment machines.
npm ci
npm ci --omit=dev
npm ls --omit=dev
npm pkg set dependencies.semver="^6.0.0"
npm ci
Source: npm Docs: npm ci
added 3 packages in 192ms
added 2 packages in 185ms
demo@1.0.0 /path/to/demo
+-- lodash@4.18.1
`-- semver@7.8.5
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Invalid: lock file's semver@7.8.5 does not satisfy semver@6.3.1
The first call installed all three packages, the second --omit=dev installed only the two production ones (the development
dependency prettier is gone), and the last call failed with exit code 1 because the edited range no longer matched the lock.
The same output was produced on Node.js 24 and 22. The --omit=dev flag is the current way to leave out development
dependencies; the older --production flag and --only=prod are documented as deprecated aliases for it. A Dockerfile
therefore installs with npm ci --omit=dev, as shown in Docker & Containers.
If you generate the lockfile with flags that change the tree shape, such as --legacy-peer-deps, pass the same flags to
npm ci, or store them in the project .npmrc.
npm ci also fixes the order in which scripts run, which matters for the Scripts: lifecycle, pre and post, --, .bin section: preinstall runs after the
lockfile has been validated but before dependencies are fetched, so it cannot rely on anything in node_modules. Two more
facts about lockfiles: node_modules/.package-lock.json is a "hidden lockfile" npm uses to avoid rescanning the folder (never
commit it), and as of npm 12 the file npm-shrinkwrap.json is no longer read or written (rename it to package-lock.json; a
dependency that ships one inside its tarball has it ignored, so publishers who need to ship a locked tree use
bundleDependencies).
Dependency types and overrides
package.json has several dependency maps, and the map decides when a package is installed and who is responsible for it.
The full file reference is the subject of package.json, exports & imports.
| Field | Installed when | Use it for |
|---|---|---|
|
always, including |
Code your package imports at run time. |
|
in your own project, but not with |
Test runners, linters, formatters, type checkers, build tools. |
|
always, but a failure to install does not abort the install |
Platform-specific accelerators; your code must cope when the import fails. |
|
npm 7 and later install them automatically unless that creates a conflict |
Plugins and libraries that must share one copy of a host package (a framework, a type definition package). |
|
packed into the tarball when you publish |
Shipping a fixed tree; the only way to ship locked versions on npm 12. |
Source: npm Docs: package.json
When two packages need incompatible versions of a third, npm installs both in nested node_modules folders. When you must
change what is installed deep in the tree, for instance to apply a security fix before the intermediate package releases one,
use overrides. It is honoured only in the root package.json of the project; overrides inside installed dependencies and
workspaces are ignored.
npm install is-odd@3.0.1
npm ls is-number
npm pkg set 'overrides.is-number'='7.0.0'
npm install
npm ls is-number
ov@1.0.0 /path/to/ov
`-- is-odd@3.0.1
`-- is-number@6.0.0
ov@1.0.0 /path/to/ov
`-- is-odd@3.0.1
`-- is-number@7.0.0 overridden
The first npm ls shows the version that is-odd asked for; after the override, the same position is 7.0.0 and npm ls
marks it overridden. An override can be a bare version (applies everywhere), a nested object ({ "is-odd": { "is-number":
"6.0.0" } } applies only below is-odd), a semver range, a dist-tag or a replacement spec such as npm: or file:. Overrides
are an escape hatch: they can pick a version the consuming package was never tested with, so remove them once the real
dependency catches up. Run npm explain <name> to see which override applies.
Scripts: lifecycle, pre and post, --, .bin
The scripts field maps names to shell commands. npm run <name> runs one; npm start, npm test, npm stop and
npm restart are shortcuts for the built-in names, and npm run with no name lists everything. npm runs the command
through /bin/sh on POSIX systems and cmd.exe on Windows (configurable with script-shell), always from the package
root, with an environment that includes npm_package_name, npm_package_version, npm_lifecycle_event and
npm_package_json. Since npm 7 most other package.json fields are no longer exported as variables; read the file or use
npm_package_json.
{
"name": "bookshelf-scripts",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"prebuild": "echo prebuild",
"build": "echo build",
"postbuild": "echo postbuild",
"pretest": "echo pretest",
"test": "node --test",
"posttest": "echo posttest",
"args": "node args.js",
"fmt": "prettier --version"
},
"devDependencies": { "prettier": "3.3.3" }
}
Source: npm Docs: Scripts
Two behaviours are visible in this file. Any script x can have prex and postx companions, which npm runs
automatically around it: npm run build printed prebuild, build, postbuild in that order, and npm test ran pretest,
the tests, then posttest. And fmt calls prettier with no path: npm adds the project’s node_modules/.bin (and those of
parent folders) to PATH for the script, so the locally installed version runs (3.3.3), whatever is or is not installed
globally. That is why scripts, not global tools, are the way to share commands in a repository.
import process from 'node:process';
console.log(process.argv.slice(2));
Source: npm Docs: npm run
npm run args --silent -- --port 3000 --verbose
npm run args --silent --port 3000
Source: npm Docs: npm run
[ '--port', '3000', '--verbose' ]
[ '3000' ]
Everything after a lone -- goes to the script; without it npm treats --port as its own option and passes only the
non-option 3000. On Node.js 26, 24 and 22 (npm 11 and 10) the second form also printed a warning that --port is an unknown
cli config that "will stop working in the next major version of npm", and npm 12 is that major version: it fails with
EUNKNOWNCONFIG and Unknown cli flag: --port. Always write the --. The same npm 12 release turned unknown keys in
.npmrc, abbreviated flags and single-hyphen multi-character shorthands into errors, so a typo is no longer silently ignored.
For running a script without npm at all, see node --run in
Running Scripts, the CLI & the REPL; it runs the same scripts
entries, adds node_modules/.bin to the path, but does not run pre and post companions.
Which scripts run when
Besides the arbitrary names, npm runs fixed lifecycle scripts around its own commands. The ones that matter for application developers:
| Command | Scripts, in order |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
A root package.json with echo scripts for each name printed preinstall install postinstall prepublish prepare postprepare
for a bare npm install on npm 10.9.9, 11.19.1 and 12.2.0 alike; npm install is-odd (with a package argument) ran none of
them. prepare is the right place for a build step that must happen before the package is used, for example compiling
TypeScript, while prepublish is deprecated in favour of prepare and prepublishOnly. npm no longer has uninstall
scripts: since npm 7 removal can happen for too many reasons to give a script enough context. The scripts of the dependencies you install are a different matter: their preinstall, install and postinstall are
exactly what the npm 12 policy in npm 12 and the supply-chain changes now blocks. Also, prepare scripts of workspaces run concurrently, and since npm 7
they run in the background; add --foreground-scripts to see their output.
npx and npm exec
npx runs a command from an npm package: one installed in the project, or one fetched on demand into the npm cache. It
shares its implementation with npm exec (since npm 7). Local executables win: with semver installed as a dependency,
npx semver uses that copy; otherwise the package is downloaded and a prompt asks first (suppress it with --yes or
--no).
npx --yes semver 1.2.3 -r '^1.0.0'
npx --yes semver 2.0.0 -r '^1.0.0'
npx --yes -p semver -c 'semver -i minor 1.2.3'
npm exec -- semver -i patch 1.2.3
npx --yes semver@6 --help
Source: npm Docs: npx
The first command printed 1.2.3 (the version satisfies the range), the second printed nothing and exited with code 1, the
third printed 1.3.0, the fourth 1.2.4, and the last printed the help of SemVer 6.3.1 rather than the 7.x copy: a package
name with a specifier only counts as the local one when name and version match exactly. When no prompt can be shown (no
terminal), npm 11 printed a warning that the package "was not found and will be installed" and went ahead, so be careful in
scripts that call npx on unreviewed names: that is a way to run code you have not locked in package-lock.json. The
option rule from the npm documentation: with npx, all flags must come before the first positional argument; with
npm exec, use -- to separate npm’s flags from the command’s. In a real terminal, name the package and its version
explicitly (npx --yes semver@7.8.5) when the command must be reproducible.
npm audit, signatures and supply-chain hygiene
npm audit submits a description of the dependencies configured in your project to the registry and prints the report of
known vulnerabilities it gets back. npm audit fix applies compatible updates, --audit-level=<level> sets the minimum severity that
makes the exit code non-zero, and --omit=dev limits the report to what production installs. npm audit signatures verifies
the registry signatures of the installed tarballs.
npm install minimist@1.2.0
npm audit
npm audit --audit-level=critical
npm audit fix
npm audit signatures
Source: npm Docs: npm audit
# npm audit report
minimist 1.0.0 - 1.2.5
Severity: critical
Prototype Pollution in minimist - https://github.com/advisories/GHSA-vh95-rmgr-6w4m
fix available via `npm audit fix`
node_modules/minimist
1 critical severity vulnerability
changed 1 package, and audited 2 packages in 564ms
found 0 vulnerabilities
1 package has a verified registry signature
npm audit exited with code 1 (it does whenever a vulnerability is found, as the documentation says), npm audit fix moved
minimist to 1.2.8 within its range, and the signature check passed. The output is a point-in-time snapshot of the advisory
database, so yours will differ. Audit is a floor, not a guarantee: it knows only published advisories, says nothing about
malicious packages published yesterday, and complains about development dependencies that never reach production (use
--omit=dev). The wider threat model (typosquatting, dependency confusion, compromised maintainers) and what to do about it is
the topic of Security Best Practices. The npm threats-and-mitigations page
is the official companion. Combine five habits: commit and review the lockfile, install with npm ci, keep the tree small,
apply the npm 12 script policy below, and consider a release-age cool-down (also below).
Global versus local installs
A local install puts packages in the project’s node_modules and lists them in package.json; anything the application
imports or runs belongs there. A global install (-g) puts a package in the folder of the current Node.js installation and
exists to put a command on your PATH. Because the global folder is per Node.js installation, switching versions with a
version manager such as nvm gives you a different set of global packages; the Installing Node.js
page covers the managers.
npm root
npm root -g
npm prefix -g
npm ls -g --depth=0
Source: npm Docs: folders
On the scratch machine the local root was the project’s node_modules, the global root was
<prefix>/lib/node_modules under the active Node.js installation, and the global list contained only npm@11.19.1. Prefer
local installs and npm exec or npx for tools: the version is recorded in package.json and the lockfile, so everyone,
including CI, uses the same one. Reserve global installs for tools that are not tied to a project, and note that npm 12 asks you
to provide install-script permissions for global installs and npx through the --allow-scripts flag, because there is no
project package.json in which to record them (npm 12 and the supply-chain changes). On Unix-like systems, do not use sudo npm install -g: install
Node.js with a version manager or into a user-owned prefix instead.
npm 12 and the supply-chain changes
npm 12.0.0 (2026-07-08) is a major release with defaults aimed at supply-chain attacks. The 12.0.0 release notes list the breaking changes verified here by running npm 12.2.0 on Node.js 26.10.0:
| Change | What happens |
|---|---|
Dependency lifecycle scripts blocked by default |
|
Git and remote tarball dependencies |
|
Root |
Runs before dependencies are fetched. |
Unknown configuration |
Unknown |
Removed or changed commands |
|
Supported Node.js |
`^22.22.2 |
^24.15.0 |
|
>=26.0.0`. |
Source: npm Docs: Changelog (12.0.0, breaking changes), npm Docs: config (allow-git, allow-remote), npm CLI release notes on GitHub: v12.0.0
Install scripts are blocked: allowScripts
A dependency’s install-time scripts run arbitrary code on your machine and your CI runner during npm install, before you have
imported a line of it. That is the delivery mechanism of most npm supply-chain malware, so npm 12 makes you opt in. The test
case: esbuild has a postinstall script (node install.js).
npm install esbuild
npm12 install esbuild
npm12 install-scripts ls
npm12 install-scripts approve esbuild
npm12 rebuild
Source: npm Docs: npm install-scripts
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn install-scripts esbuild@0.28.2 (postinstall: node install.js)
...
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts esbuild@0.28.2 (postinstall: node install.js)
...
1 package has install scripts blocked because they are not covered by allowScripts:
esbuild@0.28.2 (postinstall: node install.js)
Approved esbuild:
added esbuild@0.28.2
rebuilt dependencies successfully
Note the verbs. npm 11.19 (Node.js 26 and 24) warns that the script is "not yet covered" and still runs it, and it already
ships the npm install-scripts command; npm 12 says the script "had install scripts blocked" and does not run it; npm 10 (Node.js 22) says nothing and runs it. The experiment
that proved it used a local package whose postinstall writes a marker file: the marker existed after installing with npm 10
and npm 11, and was absent with npm 12 until the package was approved and npm rebuild ran. The approval is written to the
root package.json:
{
"dependencies": { "esbuild": "^0.28.2" },
"allowScripts": { "esbuild@0.28.2": true }
}
Source: npm Docs: npm install-scripts
The pinned form esbuild@0.28.2 approves only the version you reviewed; --no-allow-scripts-pin writes a name-only entry for
any version. npm install-scripts deny <pkg> writes a false entry that survives approve --all and keeps a package
blocked; npm install-scripts prune removes entries that no longer match anything. npm approve-scripts and
npm deny-scripts are aliases. After the approval, npm ci ran the script on a fresh node_modules (the policy in
package.json applies to every install). Four configuration options sharpen the policy:
| Option | Effect |
|---|---|
|
An unreviewed script becomes a hard error ( |
|
For one-off and global contexts ( |
|
Runs no scripts at all (including your own); it overrides every other setting. |
|
Bypasses the policy; documented as a migration escape hatch whose use is strongly discouraged. |
Source: npm Docs: Config
Treat the first run on npm 12 as a review: run npm install-scripts ls, read what each script does (most native
packages only download or compile a binary), approve the ones you trust, and commit the allowScripts field. Packages with native binaries, such as canvas or sharp (the
examples used in the npm documentation), are the usual entries. The policy also covers prepare of dependencies installed from git or from a folder or tarball; it never affects the
scripts of your own root project.
min-release-age: a cool-down for new versions
A cool-down gives the registry and the community time to detect and remove a malicious release before it reaches your machine.
The min-release-age option (added in npm 11.10.0, 2026-02-11, so
available on Node.js 24.21 and 26.10 but not on npm 10 in Node.js 22) makes npm build the tree only from versions that have been
published for more than the given number of days. It complements --before, which takes an exact date. If no version satisfies
the constraint, the install fails; min-release-age-exclude exempts package names or glob patterns, such as your own scope.
npm install semver --min-release-age=200
npm install semver --min-release-age=200 --min-release-age-exclude=semver
npm install semver --before=2026-01-01
npm install semver --min-release-age=100000
On 2026-10-03 these resolved to semver@7.7.4, semver@7.8.5 (the excluded package takes the newest), semver@7.7.3 and the
last one failed with ENOVERSIONS ("No versions available for semver"). With npm 12.2.0 and on Node.js 24 (npm 11.19.0) the first command gave the same
result; on Node.js 22 (npm 10.9.9) the option was ignored silently and 7.8.5 was installed, so a team that still runs
Node.js 22 should use --before or a registry proxy. Put the value in the project .npmrc (the npm documentation’s own example uses min-release-age=7) so that every developer and CI job gets it. The option also
applies to npm audit fix: when a patched version is newer than the window, npm keeps the vulnerable version and exits with a
non-zero code, so list the package in min-release-age-exclude to install an urgent fix. The option is not exported to
child processes.
What changed recently
| Version | Date | Change |
|---|---|---|
npm 11.10.0 |
2026-02-11 |
|
npm 11.19.x |
bundled with Node.js 26.10.0 and 24.21.0 |
|
npm 12.0.0 |
2026-07-08 |
Dependency scripts blocked by default; |
npm 12.1.0 |
2026-09-21 |
|
npm 12.2.0 |
2026-09-30 |
|
Earlier changes still worth knowing: the --production flag is deprecated in favour of --omit=dev; npm 7 made
peerDependencies install automatically and introduced lockfile version 2 and the hidden lockfile; npm 9 introduced
lockfileVersion: 3. Corepack is no longer part of the Node.js
distribution (see Installing Node.js), so pnpm and Yarn are installed like any
other tool.
Where the books are outdated
Efficient Node.js (Samer Buna, O’Reilly 2025) has a chapter on package management covering npm show, ls, link, cache,
the install forms, SemVer operators, --save-exact, update, outdated, prune, the lockfile, scripts and npx. Its
baseline is Node.js 22 with npm 10, so the concepts hold but three points need updating: use npm ci and --omit=dev where an
older workflow runs prune and install before tests or deploys, treat --production as deprecated, and read the install
section knowing that npm 12 blocks dependency scripts, refuses git and URL dependencies by default and errors on unknown flags.
Nothing in it covers allowScripts, npm install-scripts or min-release-age, which did not exist in npm 10.
Node.js Projects (Jonathan Wexler, O’Reilly 2025) uses npm init, package.json fields, SemVer ranges and, in its
appendices, Yarn and Corepack. The npm usage is still valid; the Corepack route is not, because Corepack is no longer bundled
with current Node.js. Its projects also install packages such as dotenv, nodemon and npm-run-all, for which Node.js now has
built-ins (--env-file, --watch and node --run, covered in
CLI Options, Environment & Config File), and a native add-on
installed from npm would need an allowScripts entry on npm 12. Both books were written before npm 12, so neither explains why
npm install on a fresh machine may now report blocked scripts.
For publishing a package to the registry, package.json fields such as files, exports and engines, and trusted
publishing, see JavaScript Tooling: Bundling, npm &
Publishing; this page deliberately covers only what that page lacks (lockfiles and npm ci, audit, npm 12, npx and
overrides). The Node.js-specific publishing topics and workspaces continue in
Workspaces & Publishing.
References
-
Node.js Learn: An introduction to the npm package manager, https://nodejs.org/learn/getting-started/an-introduction-to-the-npm-package-manager
-
npm Docs: About semantic versioning, https://docs.npmjs.com/about-semantic-versioning/
-
SemVer specification, https://semver.org/
-
npm Docs: Scripts, https://docs.npmjs.com/cli/v12/using-npm/scripts/
-
npm Docs: npm run, https://docs.npmjs.com/cli/v12/commands/npm-run/
-
npm Docs: npx, https://docs.npmjs.com/cli/v12/commands/npx/
-
npm Docs: npm exec, https://docs.npmjs.com/cli/v12/commands/npm-exec/
-
npm Docs: npm ci, https://docs.npmjs.com/cli/v12/commands/npm-ci/
-
npm Docs: package-lock.json, https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json/
-
npm Docs: package.json, https://docs.npmjs.com/cli/v12/configuring-npm/package-json/
-
npm Docs: npm install, https://docs.npmjs.com/cli/v12/commands/npm-install/
-
npm Docs: npm view, https://docs.npmjs.com/cli/v12/commands/npm-view/
-
npm Docs: npm ls, https://docs.npmjs.com/cli/v12/commands/npm-ls/
-
npm Docs: npm explain, https://docs.npmjs.com/cli/v12/commands/npm-explain/
-
npm Docs: npm outdated, https://docs.npmjs.com/cli/v12/commands/npm-outdated/
-
npm Docs: npm update, https://docs.npmjs.com/cli/v12/commands/npm-update/
-
npm Docs: npm uninstall, https://docs.npmjs.com/cli/v12/commands/npm-uninstall/
-
npm Docs: npm prune, https://docs.npmjs.com/cli/v12/commands/npm-prune/
-
npm Docs: npm dedupe, https://docs.npmjs.com/cli/v12/commands/npm-dedupe/
-
npm Docs: npm pkg, https://docs.npmjs.com/cli/v12/commands/npm-pkg/
-
npm Docs: npm link, https://docs.npmjs.com/cli/v12/commands/npm-link/
-
npm Docs: npm cache, https://docs.npmjs.com/cli/v12/commands/npm-cache/
-
npm Docs: npm audit, https://docs.npmjs.com/cli/v12/commands/npm-audit/
-
npm Docs: npm install-scripts, https://docs.npmjs.com/cli/v12/commands/npm-install-scripts/
-
npm Docs: Config (
min-release-age,allow-scripts,strict-allow-scripts), https://docs.npmjs.com/cli/v12/using-npm/config/ -
npm Docs: folders, https://docs.npmjs.com/cli/v12/configuring-npm/folders/
-
npm Docs: Threats and mitigations, https://docs.npmjs.com/threats-and-mitigations/
-
npm Docs: Changelog, https://docs.npmjs.com/cli/v12/using-npm/changelog/
-
npm CLI release notes: v12.0.0, https://github.com/npm/cli/releases/tag/v12.0.0
-
npm CLI release notes: v11.10.0, https://github.com/npm/cli/releases/tag/v11.10.0
-
Node.js: Previous releases, https://nodejs.org/en/about/previous-releases
-
O’Reilly: Efficient Node.js, Samer Buna, https://www.oreilly.com/library/view/efficient-node-js/9781098145187
-
O’Reilly: Node.js Projects, Jonathan Wexler, https://www.oreilly.com/library/view/node-js-projects/9781098173135/