npm 12 shipped this month with a change that’s been years overdue: npm install no longer runs a package’s preinstall, install, or postinstall scripts unless you’ve explicitly said it can. The same default-deny now applies to git dependencies and remote tarball URLs. If you’ve been following the ChainDrop and Miasma npm worm incidents covered here before, this is the platform-level fix aimed squarely at the technique both attacks relied on.
Why This Matters
Lifecycle scripts run automatically, with no user interaction, the moment npm install resolves a package — before your application code, your linter, or your CI’s security scanner ever sees a line of the dependency. That made postinstall the single most abused primitive in npm supply-chain attacks: a compromised maintainer account publishes a new version with an innocuous-looking script block, and every downstream npm install silently executes it.
{
"name": "some-utility",
"version": "5.6.1",
"scripts": {
"preinstall": "node ./scripts/bootstrap.js"
}
}
Before npm 12, that script ran unconditionally on install. After npm 12, it doesn’t — unless the package is on your project’s allowlist.
The Old (Insecure) Default
# npm < 12: implicit trust
npm install some-utility
# preinstall/install/postinstall all run automatically,
# with full access to your filesystem, env vars, and network
Nothing in this workflow gave a developer or a CI pipeline a chance to review what the script actually did. --ignore-scripts existed as an opt-out, but almost nobody set it globally because it also breaks legitimate native-module builds (node-gyp, sharp, bcrypt, etc.).
The New (Secure) Default
# npm 12+: explicit allowlist
npm install some-utility
# scripts, git dependencies, and remote tarballs are skipped by default
# Review what a package's install scripts actually do, then approve it
npm approve-scripts some-utility
# Explicitly deny a package you don't trust
npm deny-scripts some-other-package
npm approve-scripts writes the decision into package.json under an allowScripts field, which you commit to source control:
{
"name": "my-app",
"allowScripts": {
"some-utility": true,
"some-other-package": false
}
}
Because the allowlist lives in a committed file, it’s reviewable in pull requests just like any other dependency change — a new entry appearing in allowScripts is a signal worth a second look during code review, the same way a new dependency in package.json is.
Migration Checklist
- Upgrade and audit first. Run
npm install --foreground-scriptson npm 11.16+ to surface which packages currently rely on lifecycle scripts before you’re forced to decide blind. - Approve deliberately, not in bulk. Resist the urge to
npm approve-scriptseverything just to make CI green again — that recreates the exact blanket trust npm 12 is trying to remove. Read what each script does first. - Commit the allowlist.
allowScriptsonly protects reproducible installs if it’s checked into version control and enforced vianpm ciin CI, not left as a local-only override. - Check for
ignore-scripts=truein your.npmrc. If your project or CI already sets this globally, the newallowScriptsfield is silently ignored — you’ll believe you’re protected when you’re not. Remove the blanket flag and use the allowlist instead. - Watch git and remote dependencies too.
--allow-gitand--allow-remotenow default tonone. If yourpackage.jsonpulls a dependency straight from a GitHub URL or a tarball link, it will stop resolving until you explicitly allow it — a good forcing function to move those onto the registry with pinned versions instead.
Where This Still Falls Short
allowScripts stops a compromised package from running code at install time — it does nothing about malicious code that executes at runtime, inside your application, the first time you require() or import the package. An attacker who can’t get a postinstall hook approved can still ship a backdoor in index.js that fires when the module is loaded. Lifecycle-script hardening is a meaningful reduction in blast radius, not a replacement for dependency pinning, lockfile integrity checks, SBOM review, and runtime monitoring.
Bottom Line
For years, one line in a package.json scripts block was enough to run arbitrary code on every machine that installed a package. npm 12 turns that from an implicit grant into an explicit, auditable decision. Pair it with the npm ci lockfile enforcement you should already have, and the install step of your pipeline gets meaningfully harder to weaponize — without giving up the native-module builds that legitimately need it.
Sources: InfoQ – npm 12 Released, The Hacker News – npm 12 Disables Install Scripts by Default, GitHub Community Discussion – Preparing for npm v12