Tech 11 min read

npm v12 blocks install scripts: approve-scripts and allowScripts

IkesanContents

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.

IncidentPublication or observation windowInstall-time entry pointWhat npm v12 blocks
Nx s1ngularityAugust 2025telemetry.js in postinstallIf unapproved, script execution stops after dependency download
Shai-Hulud 2.0November 2025Install-time payload that launches Bun and a secret scanner during setupIf unapproved, execution stops before the payload starts
axios compromiseMarch 2026postinstall in the fake dependency plain-crypto-jsIf plain-crypto-js is unapproved, execution stops before axios is imported
Mini Shai-Hulud TanStack waveMay 11, 2026 UTCpreinstall plus prepare on the Git dependency sideHits both allowScripts and --allow-git
Mini Shai-Hulud @antv waveMay 19, 2026 UTCpreinstall and Git dependencies across 314 packages and 637 malicious versionsNarrows registry scripts and Git dependency paths at the same time
Red Hat-linked Miasma npm waveDisclosed June 2, 2026preinstall across 32 packages and more than 90 versionsStops the entry point that downloads Bun and starts the second stage
Microsoft 73-repository disablementJune 5, 2026AI tool and IDE settings, not npmNot blocked by npm v12. Workspace settings still need separate review
Three malicious node-ipc versionsMay 14, 2026 UTCCommonJS execution when require("node-ipc") runsNot 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 typeWhat happens in v12Where to look first
Pure JavaScriptInstalls normally without running anythingNo extra work
Dependency that downloads a binary in postinstallSetup stops if unapprovedscripts.postinstall
Package with binding.gypImplicit node-gyp rebuild stopsbinding.gyp inside the tarball
prepare in Git, file, or link dependenciesPrepare stops if unapprovedLockfile and dependency spec

Dependencies that usually catch migrations tend to fall into these groups.

Package exampleWhat it does at install timeSymptom when blocked
esbuildPlaces the OS/CPU-specific binaryBundler startup cannot find the executable
sharpPrepares the native binding and libvips-related piecesImage processing fails while loading the module
canvas, bcrypt, better-sqlite3Builds C/C++ through node-gypNative binding is missing
Old node-sassDownloads or builds a native binarySass build fails after install
@prisma/clientGenerates the client or prepares enginesRuntime fails because Prisma Client was not generated
cypressDownloads the test binarycypress 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.

References