July 28, 2026
npm 12 Blocks Install Scripts by Default
Hello, I’m Lince. I’m building LiveReview, an AI code reviewer that keeps your code, infrastructure, and AI entirely under your control…

By Lince Mathew
5 min read
Hello, I'm Lince. I'm building LiveReview, an AI code reviewer that keeps your code, infrastructure, and AI entirely under your control while being cost-effective. Check it out here.
npm 12.0.0 was published on July 8, 2026. Two days later, 12.0.1 took over the latest tag, which means it now installs by default.
Nobody has to opt in. Pull a fresh Node image, run npm install -g npm, or onboard a new developer, and npm 12 arrives on its own.
For most projects the upgrade is uneventful.
For any project with a native module in its dependency tree, the first npm install after upgrading will finish successfully and leave you with a broken build.
That is the part worth understanding before it happens to you at an inconvenient moment.
Why npm made this change
For years, installing a package meant running its code.
Any dependency, at any depth in your tree, could declare a postinstall script that executed the moment you typed npm install, with the full privileges of whoever ran the command.
Your SSH keys, your cloud tokens, your CI secrets, all reachable.
That design was a known risk for a long time without much happening. Then 2026 happened.
The pattern repeated all year with small variations. An attacker takes over a maintainer's npm account through phishing or a stolen publish token. A script republishes malicious versions across every package that account controls, faster than any human review can react.
The malicious version carries an install hook that harvests credentials from whoever installs it, and in the worst cases uses those stolen credentials to republish from the next maintainer's account, which is what made the Shai-Hulud family of worms self-propagating.
The names stacked up. Axios was compromised in March, a package with roughly 100 million weekly downloads.
LiteLLM was hit on PyPI the same month. Mastra in June.
A June campaign against node-gyp that researchers named Phantom Gyp reached 57 packages and got its execution through a weaponized binding.gyp rather than a lifecycle hook, which shows how quickly the techniques were adapting.
Strip away the branding and every incident needed the same thing to work: code running automatically at install time on a machine that held credentials.
npm's earlier responses all addressed the publishing side. Mandatory 2FA, shorter-lived tokens, trusted publishing through OIDC.
Those raise the cost of account takeover, and they do nothing once a malicious version is already in the registry. npm 12 is the first change that addresses the consuming side, which is where you sit.
What actually changed
Three defaults flipped.
Dependency install scripts no longer run. This covers preinstall, install, postinstall, and prepare for non-registry sources. npm skips them, completes the install, and prints a summary of what it skipped.
Note that it skips rather than fails. Nothing errors, so the symptom appears later as a missing binary or a module that will not load.
If you upgrade npm and a build starts failing in a confusing way a day later, this is the first thing to check.
allow-git now defaults to none. Dependencies pointing at a git repository will not install unless you opt in.
allow-remote now defaults to none. The same applies to dependencies pointing at a tarball URL. One relief here: tarballs served from the same hostname as your configured registry still install normally, which covers npm itself, GitHub Packages, and most private registries.
How approvals work now
Approvals live in an allowScripts field in your package.json, and npm 12 adds a command to manage it.
Start read-only. This lists every package with install scripts that you have not yet approved or denied:
npm install-scripts lsnpm install-scripts lsThe list is usually shorter than people expect. Most projects need scripts for a handful of native modules and nothing else.
Approve what you need:
npm install-scripts approve sharp esbuildnpm install-scripts approve sharp esbuildBy default this writes pinned entries, so the approval applies to the exact version you reviewed:
{
"allowScripts": {
"sharp@0.33.4": true,
"esbuild@0.21.5": true,
"some-analytics-pkg": false
}
}{
"allowScripts": {
"sharp@0.33.4": true,
"esbuild@0.21.5": true,
"some-analytics-pkg": false
}
}A pin means the next version needs a fresh look, which is the entire point. --no-allow-scripts-pin writes name-only entries that allow any future version if you decide the pins are too much friction. I would keep them.
The false entry is an explicit denial. Denials survive npm install-scripts approve --all, so a decision to say no cannot be undone by a teammate taking the fast path later.
Two more subcommands:
npm install-scripts prune --dry-run # find stale entries
npm install-scripts prune # remove themnpm install-scripts prune --dry-run # find stale entries
npm install-scripts prune # remove themprune clears approvals for packages that are no longer installed or no longer have install scripts, which keeps allowScripts from turning into a junk drawer nobody trusts.
There is also approve --all, which approves everything currently in your tree without you reading any of it. Reasonable as a one-time migration step on an existing project. A bad habit afterwards.
Global installs and npx
npm install-scripts needs a project package.json and fails with EGLOBAL without one. For global installs and one-off runs, use the flag instead:
npm install -g --allow-scripts=canvas,sharpnpm install -g --allow-scripts=canvas,sharpOr persist it:
npm config set allow-scripts=canvas,sharp --location=usernpm config set allow-scripts=canvas,sharp --location=userPassing --allow-scripts during a project-scoped install, ci, update, or rebuild is an error. The two paths are kept separate deliberately, so a convenience flag for a global install cannot quietly weaken a project's policy.
Make it strict in CI
The default is a warning. That is right for local work and wrong for a pipeline, where a silently skipped script becomes a mystery failure three steps later.
strict-allow-scripts=truestrict-allow-scripts=trueWith this set, any unreviewed package with install scripts fails the install outright. Explicit denials stay silent, and optional dependencies that cannot run on the current platform are not flagged, so you only hear about packages that genuinely need a human decision.
Two flags override all of this: --ignore-scripts and --dangerously-allow-all-scripts. If you find the second one in a CI config, you have found your afternoon's work.
What else changed in npm 12
The same release cleaned house, and some of it will bite during the same upgrade:
npm shrinkwrapis gone, andnpm-shrinkwrap.jsonis no longer read. Rename it topackage-lock.json.npm adduseris gone. Usenpm login.- Unknown and abbreviated CLI flags now throw instead of warning. Unknown
.npmrckeys still warn, andstrict-npmrc=truepromotes those to errors too. - A root
preinstallscript now runs before dependencies are installed, which is what most people always assumed it did. - Node requirement is
^22.22.2 || ^24.15.0 || >=26.0.0.
One more setting is worth adding while you are already editing .npmrc:
min-release-age=3min-release-age=3That refuses to install any version published within the last three days. Malicious versions are usually caught and pulled within hours, so a short quarantine costs you almost nothing and closes the window these attacks depend on. It does make npm audit fix exit non-zero when a legitimate patch is too new, which you resolve with min-release-age-exclude.
What this does not fix
Worth being clear about the limits, because a default that feels like a solution is its own risk.
An approved package still runs whatever code it wants. If you approve sharp and sharp is compromised in a version your pin happens to cover, you get the same outcome as before. The allowlist is only as good as the review behind it, and approve --all is a review of nothing.
Blocking install scripts also does nothing about malicious code in a package's runtime path. A dependency that steals credentials when your application imports it is unaffected by any of this.
What the change actually removes is the automated, unattended execution path that made mass campaigns cheap. That is a real reduction in blast radius, and it is not the same as being safe.
The upgrade path
- Upgrade npm locally before you touch CI.
- Run
npm install-scripts lsand read the list. - Approve what you need by name, pinned. Deny anything you do not recognize.
- Set
strict-allow-scripts=truein CI once the list is clean. - Add
min-release-agewhile you are in.npmrc. - Grep your pipelines for
--ignore-scriptsand--dangerously-allow-all-scripts.
On a normal project this is about twenty minutes of work. It closes the exact door that every major npm compromise this year walked through, and it turns a category of silent risk into a short list you can actually read.