npm 12: Install Scripts Are Opt-In Now
On July 8, 2026, npm 12 reached general availability with the biggest change to npm install behaviour in years: lifecycle scripts — preinstall, install, and postinstall — no longer run automatically. They need explicit approval, and that approval is written into package.json where it gets reviewed in a pull request like any other code.
The problem: npm install meant "run their code on your machine"
Since the registry's early days, any package could declare an install script that executes arbitrary commands with the privileges of whoever typed npm install. No import required, no runtime entry point — installation alone was enough. That is precisely the path exploited repeatedly through 2026 in a run of registry compromises, including the AsyncAPI generator package compromise that Microsoft documented in July 2026.
The surface is not limited to direct dependencies either. A tree of 1,200 packages is 1,200 execution opportunities, most of them unread and published by people no one on the team has heard of.
What actually changed
Three new defaults land in npm 12:
allowScriptsnow defaults to off. Lifecycle scripts and implicitnode-gypbuilds do not run unless allowed.--allow-gitdefaults tonone. Git dependencies — direct or transitive — are no longer resolved without explicit permission.--allow-remotedefaults tonone. Dependencies from remote URLs, such as https tarballs, are blocked by default.
--allow-file and --allow-directory keep their previous defaults.
The practical migration path
The smart move is discovering the blast radius before CI turns red. Start on npm 11.16.0 or newer, which warns about uncovered scripts while still executing them:
npm i -g npm@latest
npm approve-scripts --allow-scripts-pending # lists pending packages, changes nothingThen approve only what genuinely needs it, package by package:
npm approve-scripts sharp esbuildApprovals are pinned to the installed version (pkg@1.2.3) unless --no-allow-scripts-pin is passed. To deny something explicitly:
npm deny-scripts some-packageAnd to enforce v12 behaviour early inside CI — failing the build instead of silently skipping scripts:
# .npmrc
strict-allow-scripts=trueWith that in place, npm ci becomes a real gate: any new dependency that tries to execute code at install time stops the pipeline and asks for a human review.
Real-world caveats
Binary packages feel it first. Anything that downloads a prebuilt executable or compiles a native addon depends on postinstall. Blocking it without approval produces a build that succeeds and then fails at runtime with a misleading error. Approve those deliberately, and record the reason in the PR description.
Version pinning creates noise. Every dependency bump invalidates the pinned approval, so automated PRs will carry extra review steps. That is not a defect — it is the point: a review every time the code that will execute changes.
Git dependencies break internal forks. Teams pointing at a patched fork over a Git URL now need --allow-git explicitly, or a move to a private registry.
There is a second date on the calendar. The hardening does not stop here. From early August 2026, granular access tokens configured to bypass 2FA lose the ability to perform sensitive operations, and from January 2027 they lose direct publishing entirely. The recommended replacements are trusted publishing via OIDC or staged publishing with a human 2FA approval.
Takeaway
npm 12 moves install-time code execution from "default and silent" to "explicit and reviewed". This is not cosmetic hardening — it closes the widest exploitation path in the JavaScript ecosystem and puts the decision where it belongs: in code review.
A team that goes through this transition today on npm 11.16 with strict-allow-scripts=true enters v12 with a clean, understood allowlist. A team that defers it will discover the list on the day the pipeline stops.
For more on riding the ecosystem's security-update wave, see the Next.js July 2026 security release.
Primary source: npm install-time security and GAT bypass2fa deprecation — GitHub Changelog