npm v12 blocks install scripts: approve-scripts and allowScripts
Contents
TL;DR
Change In npm v12, dependency preinstall, install, postinstall, some prepare, and implicit node-gyp rebuild execution are blocked by default. Only packages approved through allowScripts run
Preparation Run a normal install on npm 11.16.0 or later, then use npm approve-scripts --allow-scripts-pending to list dependencies that would be blocked. Approve only trusted packages with npm approve-scripts; record the rest with npm deny-scripts
Difference min-release-age lowers the chance of installing a freshly published compromised version. npm v12 blocks install-time code execution after the package has already been resolved
npm v12 changes the safer default for npm install.
GitHub’s announcement says npm v12 is expected in July 2026, and that install-time scripts in dependency packages will no longer run by default.
Aikido frames this as a change that closes the recurring postinstall-style entry point seen in Nx, Shai-Hulud, axios, Mini Shai-Hulud, and Red Hat-linked Miasma.
The earlier post on pnpm 11, Yarn 4.10, and npm v11.10 release-age gates was about not picking up newly published versions immediately.
npm v12 is one step away from that.
Even after a package is downloaded and unpacked into node_modules, unapproved install-time code does not run.
Where past incidents line up with the change
npm v12 helps at places where code runs automatically during dependency resolution.
Looking across earlier posts, the entries split into paths npm v12 can block and paths it cannot.
| Incident | Publication or observation window | Install-time entry point | What npm v12 blocks |
|---|---|---|---|
| Nx s1ngularity | August 2025 | telemetry.js in postinstall | If unapproved, script execution stops after dependency download |
| Shai-Hulud 2.0 | November 2025 | Install-time payload that launches Bun and a secret scanner during setup | If unapproved, execution stops before the payload starts |
| axios compromise | March 2026 | postinstall in the fake dependency plain-crypto-js | If plain-crypto-js is unapproved, execution stops before axios is imported |
| Mini Shai-Hulud TanStack wave | May 11, 2026 UTC | preinstall plus prepare on the Git dependency side | Hits both allowScripts and --allow-git |
| Mini Shai-Hulud @antv wave | May 19, 2026 UTC | preinstall and Git dependencies across 314 packages and 637 malicious versions | Narrows registry scripts and Git dependency paths at the same time |
| Red Hat-linked Miasma npm wave | Disclosed June 2, 2026 | preinstall across 32 packages and more than 90 versions | Stops the entry point that downloads Bun and starts the second stage |
| Microsoft 73-repository disablement | June 5, 2026 | AI tool and IDE settings, not npm | Not blocked by npm v12. Workspace settings still need separate review |
| Three malicious node-ipc versions | May 14, 2026 UTC | CommonJS execution when require("node-ipc") runs | Not stopped by npm install alone. Check the lockfile and runtime logs |
The first six rows use automatic execution during npm install as an entry point.
The last two rows trigger elsewhere: opening a repository with an AI tool, opening it in an IDE, or letting an app or test load a package.
npm v12 closes a major entry point, but protecting credentials on developer machines and CI runners remains a separate job.
From automatic execution to project-level approval
With current npm behavior, any package in the dependency tree that has preinstall, install, or postinstall can run during npm install.
It does not matter whether the application imports that package.
As soon as dependency resolution brings it in, code runs with the permissions of the developer machine or CI runner.
In npm v12, that execution becomes approval-based.
GitHub’s announcement says the allowlist is created with npm approve-scripts, while rejected scripts are recorded with npm deny-scripts.
The result goes into allowScripts in package.json, then gets committed like other project configuration.
npm install
npm approve-scripts --allow-scripts-pending
npm approve-scripts sharp esbuild
npm deny-scripts some-package
npm docs describe --allow-scripts-pending as read-only.
It only lists packages with unreviewed install scripts; it does not change package.json.
The practical flow is to print that list in CI and locally first, then approve only dependencies that truly need native builds or setup.
By default, approve-scripts writes version-pinned approvals such as pkg@1.2.3.
Adding --no-allow-scripts-pin writes only the package name, but pinning is easier to reason about as a supply-chain defense.
It records that this specific version’s install script was reviewed.
package.json ends up with a record shaped like this.
{
"allowScripts": {
"sharp@0.33.5": true,
"esbuild@0.25.0": true,
"some-package": false
}
}
The actual versions are whatever your local resolution produced.
false remains package-name based and blocks that package’s install script, including future versions.
npm docs also note that npm approve-scripts and npm deny-scripts are not workspace-aware. In a monorepo, check individual workspaces as well as the root for dependencies with install scripts.
binding.gyp is also blocked as an implicit build
The easy-to-miss part of this change is that packages with no scripts field can still be blocked.
GitHub and Aikido both write that npm’s implicit node-gyp rebuild for packages containing binding.gyp is treated like an install script.
That means reading package.json and seeing no postinstall is not enough.
Packages with native addons can still build during install even without an explicit script.
In npm v12, that build stops unless the package is on the allowlist.
The fragile area is likely to include sharp, canvas, bcrypt, better-sqlite3, old node-sass, and internal native addons.
These are often legitimate build steps rather than malicious code.
| Dependency type | What happens in v12 | Where to look first |
|---|---|---|
| Pure JavaScript | Installs normally without running anything | No extra work |
Dependency that downloads a binary in postinstall | Setup stops if unapproved | scripts.postinstall |
Package with binding.gyp | Implicit node-gyp rebuild stops | binding.gyp inside the tarball |
prepare in Git, file, or link dependencies | Prepare stops if unapproved | Lockfile and dependency spec |
Dependencies that usually catch migrations tend to fall into these groups.
| Package example | What it does at install time | Symptom when blocked |
|---|---|---|
esbuild | Places the OS/CPU-specific binary | Bundler startup cannot find the executable |
sharp | Prepares the native binding and libvips-related pieces | Image processing fails while loading the module |
canvas, bcrypt, better-sqlite3 | Builds C/C++ through node-gyp | Native binding is missing |
Old node-sass | Downloads or builds a native binary | Sass build fails after install |
@prisma/client | Generates the client or prepares engines | Runtime fails because Prisma Client was not generated |
cypress | Downloads the test binary | cypress run fails with a binary-not-found error |
This table is a list of approval candidates, not a safety verdict.
Even for practical dependencies like sharp or esbuild, approving them means allowing that version’s install-time code execution.
Review allowScripts diffs in PRs the same way you review lockfile changes.
Git dependencies and remote URLs also close by default
npm v12 closes two other entry points besides install scripts.
Git dependencies will not resolve unless --allow-git is passed.
This exists because a Git dependency’s .npmrc could swap the Git executable and lead to code execution even when --ignore-scripts was used.
The change is already available in npm 11.10.0 and later.
Remote URL dependencies will not resolve unless --allow-remote is passed.
Specs such as https://.../package.tgz sit outside normal registry version history and removal flows, leaving room for an attacker to change the external URL’s contents.
This flag is available in npm 11.15.0 and later.
The github:antvis/G2#... style Git dependency covered in the earlier Mini Shai-Hulud @antv wave lands exactly here.
Release-age gates look at publication time on the npm registry, but Git dependencies arrive by commit hash rather than by npm version number.
npm v12’s --allow-git closes that separate route by default.
axios and Miasma trigger right after install
In the axios takeover, the RAT (remote access malware) was dropped not by the legitimate package body, but by postinstall in the added fake dependency plain-crypto-js.
As covered in the axios post, victims hit the payload before importing axios.
It triggered during npm install.
The Red Hat-linked Miasma wave used the same firing point.
In the Microsoft 73-repository disablement post, I summarized the Red Hat npm wave as using preinstall to download Bun and run the second-stage malicious code.
npm v12’s default block directly intersects that install-time execution.
The Microsoft repository-disablement wave, however, is a different path even though it belongs to the same Miasma cluster.
That one did not start with npm install. It started when Claude Code, Gemini CLI, Cursor, or VS Code loaded workspace configuration.
Even when npm ci becomes safer by default, configuration files read as soon as a repository is opened in an AI tool or IDE remain outside npm’s control.
Find breaking dependencies on 11.16.0 first
You do not need to wait for v12 to start the migration.
Both GitHub and Aikido say npm 11.16.0 and later can surface the three changes as warnings.
For now, run your normal install with npm 11.16.0 or later and inspect the warnings plus approve-scripts output.
npm -v
npm install
npm approve-scripts --allow-scripts-pending
The resulting list is the set of candidates that will stop after moving to v12.
Separate dependencies that need native builds in CI, dependencies that are only needed locally, and dependencies that are no longer used.
Approving everything with --all makes the v12 change much less useful.
If you want to see v12-like failures early in CI, add a separate npm 11.16.0 job with strict-allow-scripts=true.
npm docs explain that this setting turns unreviewed install scripts from warnings into install failures.
# .npmrc
strict-allow-scripts=true
There is also an escape hatch, dangerously-allow-all-scripts=true, for temporarily letting everything through.
Do not keep this as a standing CI setting: it bypasses the allowScripts policy, including denied packages.
Use it only on a short-lived branch or locally when you need to isolate a migration failure.
After review, commit only the packages you approved into allowScripts.
Denied packages remain as false, and a later approve-scripts run will not silently approve them.
npm docs describe this as a rule where an existing false entry always wins.
In CI, keep the environment that shows v12-style breakage separate from the environment where you are doing the staged migration.
It is easier to catch native dependency setup failures in a dedicated job before they reach the production build.
Commands without a project package.json, such as npm exec, npx, and npm install -g, are handled separately.
npm docs say those cases use the allow-scripts config area rather than the allowScripts field.
Globally installed CLIs, especially AI coding tools and browser automation tools, often run with more power than project dependencies, so do not mix local machine approvals with project approvals.
Not the same job as release-age gates
The release-age gate post from May was about lowering the odds of resolving malicious versions that disappear quickly after publication.
It can keep short-lived compromised versions such as Mini Shai-Hulud, axios, or debug/chalk outside the resolver window.
But it does not remove versions already in a lockfile, and it does not solve malicious versions that stay available for a long time.
npm v12’s default allowScripts block stops automatic execution after download.
Even if a malicious version lands in the lockfile, an unapproved install-time script stops there.
On the other hand, if an approved package is compromised later, behavior depends on approval granularity.
If the approval is pinned as pkg@version, a new version does not automatically inherit execution permission.
These two controls do not replace each other.
Use both: one setting delays freshly published package resolution, and the other makes post-resolution execution approval-based.
Add explicit approval for Git dependencies and remote URLs, and the npm-family entry points visible across 2025 and 2026 become much narrower.