npm v12 Disables Install Scripts: Migration Guide
npm v12 shipped July 8, 2026 with install scripts off by default. Here's what silently breaks in CI, and the migration steps to fix it.
npm v12 shipped on July 8, 2026, and it flips three long-standing defaults from
allow to deny: dependency lifecycle scripts (preinstall, install,
postinstall, prepare), git dependencies, and dependencies fetched from
remote URLs no longer run or resolve automatically. Each now needs an explicit
allowlist entry, and the change is retroactive — it applies the moment a
project upgrades, not just to newly added dependencies.
What actually flipped
Three separate defaults changed, each shipped on its own timeline before converging in v12:
allowScriptsdefaults to off.preinstall,install,postinstallandpreparescripts from dependencies — including implicitnode-gyprebuilds for native modules — are skipped unless the package has a matching entry inpackage.json'sallowScriptsfield.--allow-gitdefaults tonone. Git dependencies, direct or transitive, no longer resolve without explicit approval. This one was announced back on February 18, 2026 and has been available since npm 11.10.0.--allow-remotedefaults tonone. Dependencies pulled from remote URLs, such as HTTPS tarballs, need the same explicit allowance. Available since npm 11.15.0.
--allow-file and --allow-directory are unchanged — local, filesystem-based
dependencies still work the way they always did.
The trap: CI stays green with nothing installed
The change that catches teams off guard isn't the new default itself, it's
what happens when a blocked script is silently skipped instead of causing a
failure. By default, npm ci prints a warning and moves on — exit code 0 —
even when a dependency's postinstall step was supposed to compile a native
binary that your application needs at runtime. The build looks successful.
The binary simply isn't there.
npm ci
# npm warn Skipping install scripts for "sharp" (no allowScripts entry)
# ✓ added 412 packages in 8s
# exit code: 0 — but sharp's native binary was never builtnpm ships a fix for exactly this: the strict-allow-scripts config converts
skipped scripts into hard errors instead of warnings. It's off by default —
turning it on is a config change teams have to make deliberately, in CI
specifically, since a hard failure during local development is more
disruptive than useful.
Migrating without guessing
The safest path is to do the discovery work before v12 becomes the default in your project, using the warnings that npm 11.16.0 already prints:
npm install -g npm@11.16.0
npm install # surfaces every script v12 would block
npm approve-scripts --allow-scripts-pending # lists pending packagesReview the list, then approve what you trust and record the rest as denied so the decision is explicit and versioned:
npm approve-scripts sharp # approve one package, pinned to its version
npm approve-scripts --all # approve everything currently pending
npm deny-scripts fsevents # explicitly block a package's scriptsnpm approve-scripts is an alias for npm install-scripts approve, and by
default it writes version-pinned entries (sharp@0.34.2) rather than
name-only ones — a script that was safe in one version doesn't stay
auto-approved after an upgrade, which is the intended behavior for a security
control like this rather than a rough edge to work around.
{
"allowScripts": {
"sharp@0.34.2": true,
"fsevents": false
}
}That block lives in package.json and travels with the repository, so the
next developer — or the next CI run — inherits the same allowlist instead of
re-discovering it.
Watch optional, platform-specific dependencies
One gap worth testing for directly: an npm/cli issue reported npm ci --strict-allow-scripts rejecting fsevents, a macOS-only optional
dependency, even though npm approve-scripts --allow-scripts-pending never
flagged it as needing review on the Linux machine where the check ran.
Optional dependencies that only install on specific platforms can fall
through the gap between what the approval tool sees and what strict
enforcement checks at install time. If your pipeline runs on more than one
operating system, verify the allowlist behavior on each of them rather than
assuming a Linux CI run covers a package that only installs on macOS.
Why now
The timing isn't arbitrary. Install-time script execution has been the
delivery mechanism for a run of JavaScript ecosystem supply-chain attacks
over the past year, precisely because a postinstall script runs
automatically, with the same permissions as the developer or CI job running
npm install, before anyone reviews what the package actually does. Removing
that automatic execution path doesn't stop a malicious package from being
published, but it does stop it from running unattended the moment someone
installs it — the same shift toward reviewed, versioned trust that's shown up
elsewhere in the ecosystem, like Next.js's move to scheduled, disclosed
security releases
instead of ad hoc patches.
Before your team upgrades
- Upgrade to npm 11.16.0+ first, not straight to v12, so you see the warnings without the enforcement.
- Run
npm approve-scripts --allow-scripts-pendingin every package and workspace — the command is workspace-unaware, so it needs to run in each one individually. - Commit the resulting
allowScriptsblock inpackage.jsonso the allowlist is reviewed in the same pull request as the dependency that needs it. - Turn on
strict-allow-scriptsin CI only, after the allowlist is populated, so a missing entry fails the build loudly instead of shipping a broken artifact quietly. - Test the allowlist on every OS your pipeline runs on before relying on strict mode, given the platform-specific dependency gap above.
Teams already tracking dependency and CVE hygiene as part of a monthly patch
cadence — the kind of process GitHub Models' retirement
notice also rewarded by
giving weeks of brownout warning instead of a surprise cutoff — will find
this migration is mostly a matter of doing the npm approve-scripts pass
early rather than discovering the gap in a failed production deploy.
Frequently asked questions
What exactly changed in npm v12?
Three defaults flipped from allow to deny: allowScripts (preinstall, install, postinstall and prepare scripts, plus implicit node-gyp rebuilds), --allow-git (git dependencies), and --allow-remote (dependencies fetched from remote URLs like HTTPS tarballs). All three now require an explicit allowlist entry instead of running automatically.
Why does my CI stay green even though a native module never got compiled?
By default, npm skips a blocked lifecycle script with a warning rather than failing the install, so `npm ci` exits 0 even when a package's postinstall build step never ran. Setting the `strict-allow-scripts` config turns those skips into hard errors, which is what npm's own guidance recommends for CI.
How do I migrate without guessing which packages need scripts?
Upgrade to npm 11.16.0 or later first — it prints the same blocking warnings v12 enforces, without actually blocking anything yet. Run your normal install, then `npm approve-scripts --allow-scripts-pending` to list every package with a pending script, review each one, and commit the resulting `allowScripts` entries in package.json.
Do optional, platform-specific dependencies need special handling?
They can be a gap in strict enforcement. An npm/cli issue reported `npm ci --strict-allow-scripts` rejecting the macOS-only `fsevents` package even though `npm approve-scripts --allow-scripts-pending` never flagged it as needing review, since the tools can disagree on optional dependencies that only install on some platforms. Test your strict CI config on every OS your pipeline actually runs on, not just the one you developed against.