On June 9, GitHub published a short changelog entry that rewrites the default behavior of the most-installed tool in JavaScript. Starting with npm v12, estimated for July 2026, npm install will no longer execute install scripts from your dependencies. Not preinstall, not install, not postinstall. Native node-gyp builds are included in the block.

Two more defaults flip in the same release. Git dependencies stop resolving unless you pass --allow-git, and remote tarball URLs stop resolving unless you pass --allow-remote. Three separate install-time code paths, all moving from allow-everything to deny-by-default in one major version.

If you use pnpm, Bun, or a current Yarn release, you have lived with this model for a while. npm was the last major JavaScript package manager that still ran arbitrary code from the registry the moment you typed install. That era now has an end date, and the details of how npm is closing it decide whether July lands as a security win or as a month of broken CI.

Three defaults flip at once

The headline change is a new install-script policy named allowScripts. Once v12 lands, npm install skips preinstall, install, and postinstall scripts from dependencies unless the package is explicitly allowed. prepare scripts from git, file, and link dependencies are blocked as well. The same policy covers node-gyp compilation, which npm has historically triggered implicitly for any dependency shipping a binding.gyp file.

Approval is managed by two new subcommands. npm approve-scripts records which dependencies may run their install scripts, and npm deny-scripts records an explicit block. The decisions land in an allowScripts field in your package.json, which matters more than it sounds: the allowlist is committed, reviewed in pull requests, and inherited by CI without any extra wiring.

By default, approvals are pinned to the exact installed version, written as entries like pkg@1.2.3. A --no-allow-scripts-pin flag writes name-only entries instead. Pinning is the right default even though it adds churn to every dependency bump. A name-only entry trusts whatever the package publishes next, and a hijacked future version running a fresh install script is precisely the scenario this feature exists to stop.

None of this requires waiting for v12. npm 11.16.0, released May 27, ships the whole machinery behind warnings: installs still run scripts, but every script that v12 would block gets flagged. The npm approve-scripts --allow-scripts-pending command lists every package whose install scripts are not yet covered by your allowlist, which makes it a dry run for July.

The timeline is further along than the announcement suggests. A v12.0.0-pre.0.0 prerelease has been on the npm/cli releases page since May 20, a week before 11.16.0 shipped the warnings. The defaults are decided and implemented; the remaining month is for the ecosystem, not the code.

The hole --ignore-scripts never closed

Security-conscious teams have run npm install --ignore-scripts for years and assumed it ended install-time code execution. It did not. The v12 changelog is unusually direct about why git dependencies get their own switch: blocking them "closes a code-execution path where a Git dependency's .npmrc could override the Git executable, even with --ignore-scripts". A dependency pointing at a git URL could ship configuration that swaps the git binary npm invokes for something else entirely. Script blocking never touched that path.

So v12 changes what npm will resolve at all. The allow-git and allow-remote settings both move from all to none, which blocks git dependencies and https tarball URLs, direct or transitive, unless you opt back in. The flags already exist: allow-git shipped in npm 11.10.0 after a February announcement, and allow-remote arrived in 11.15.0 alongside allow-file and allow-directory (those last two keep their permissive defaults in v12).

Each setting accepts all, none, or root, and works from the CLI, .npmrc, or package.json. The middle value is the pragmatic one for most teams: it lets the project's own manifest keep a git dependency it deliberately chose while refusing the same trick from something five levels down the tree.

# .npmrc - opt back in deliberately, not globally
allow-git=root
allow-remote=none
allow-scripts=sharp,canvas

Two worms and a cryptominer

npm is not doing this in a vacuum. The proximate history starts in late 2024, when a compromised Rspack release distributed cryptomining malware through a postinstall script. pnpm answered within weeks: pnpm 10 shipped in January 2025 with dependency lifecycle scripts blocked by default. Lead maintainer Zoltan Kochan was blunt about the motivation: "After each of such incidents we get a tsunami of requests to do something in order to make it harder to run lifecycle scripts."

Then September 2025 made the case unanswerable. The Shai-Hulud worm infected hundreds of npm packages with a postinstall hook that ran a bundled script on every install. The payload hunted for npm, GitHub, AWS, and GCP credentials, pulled down trufflehog to scan the filesystem for more, then used any valid npm token it found to republish that maintainer's own packages with the same hook. The install script was not incidental to the attack. It was the propagation mechanism, the thing that turned one compromised package into a self-replicating event. A later campaign in the same family moved the trick into GitHub Actions, but the original ran entirely on npm's execute-by-default install path.

Vector illustration of a segmented worm cloning into translucent crates

Seen from 2026, npm's response reads as a staged campaign rather than a single patch. February brought trusted publishing improvements and the first install-time controls. May 22 added staged publishing, where "a human maintainer with a 2FA challenge is required to approve a staged package before it is released to the registry", plus the full set of allow-* resolution flags. June 9 announced the default flips. Publish path first, install path second. v12 is the moment the install side stops being advisory.

Staged publishing attacks the worm problem from the other end. Shai-Hulud spread by publishing packages with stolen tokens, automatically and at machine speed. A registry that holds a publish in a queue until a human with a second factor approves it breaks that automation even when the token is valid. Workflows have to switch from npm publish to npm stage publish on CLI 11.15.0 or newer to get the protection, so adoption is opt-in for now. The install-side changes in v12 are not.

Every package manager reached the same conclusion

With v12, all four major JavaScript package managers converge on the same default: dependency code does not run at install time unless a human said so. They differ in mechanism, and the differences matter if you work across repos.

Package managerDependency scripts by defaultAllowlist mechanismSince
npmBlocked in v12allowScripts via approve-scriptsv12, estimated July 2026
pnpmBlockedpnpm.onlyBuiltDependenciesv10, January 2025
BunNever executedtrustedDependencies plus a curated default listAlways
Yarn BerryBlocked (enableScripts: false)enableScripts with dependenciesMeta overridesCurrent releases

Bun's documentation states the position plainly: "unlike other npm clients, Bun does not execute arbitrary lifecycle scripts by default." Bun also ships the most opinionated variant of the allowlist, a curated set of popular packages whose scripts run without configuration. There is a sharp edge in the semantics, though. Declaring your own trustedDependencies replaces the default list rather than extending it, so adding one package can silently stop another's scripts from running. The trade-off space between the runtimes is bigger than script policy; our Bun vs Node.js decision framework covers the rest of it.

Yarn Berry's current configuration defaults enableScripts to false, with per-package re-enabling through dependenciesMeta. Your own workspaces still run their scripts, on the reasonable theory that code you wrote is code you trust. pnpm's approach lives in a pnpm.onlyBuiltDependencies field that looks a lot like what npm just adopted, and pnpm keeps moving baselines elsewhere too - its catalogs feature followed a similar path from optional to expected.

Four teams with different architectures and different politics landed on the same default. That is about as close to consensus as the JavaScript ecosystem gets.

What actually breaks in July

The packages that need attention are the ones doing real work at install time: native modules compiling against node-gyp, and packages that fetch a prebuilt binary for your platform in a postinstall step. Image codecs, SQLite bindings, file watchers, headless browser installers. For these, a skipped script does not fail the install. The failure shows up later, where skipped scripts have always surfaced: at require time, when the compiled artifact is missing.

Minimalist schematic diagram of a halted robotic arm over engine

The migration is mechanical if you start before the major version forces it. Upgrade to 11.16.0 or newer, install as usual, and read the warnings. Then review the pending list and approve only what you can justify:

# see every package whose install scripts are not yet covered
npm approve-scripts --allow-scripts-pending

# approve specific packages (pinned to installed versions by default)
npm approve-scripts canvas sharp

# approve by name instead of pinned version - weaker, occasionally justified
npm approve-scripts --no-allow-scripts-pin canvas

# explicitly block a package's scripts
npm deny-scripts left-pad

There is also an npm approve-scripts --all escape hatch that approves every package with unreviewed install scripts in one go. Resist it. Approving everything reproduces the pre-v12 posture with an extra file in the repo, and it converts a one-time review you should do into a review nobody will ever do. The pending list for a typical application is shorter than you expect; most packages never needed install scripts in the first place.

Monorepos and CI need no special handling precisely because the allowlist lives in package.json. The annoying cases are indirect ones: a transitive dependency deep in the tree whose script silently stops running, or a Dockerfile pinning an old npm that behaves differently from local machines. The warning period exists to surface exactly these, which is the argument for upgrading this month rather than during the v12 firefight.

What this does not fix

The criticism worth taking seriously, raised repeatedly in the Hacker News discussion of the announcement, is that install scripts are only one of the places dependency code runs. Malicious code moved into a package's entry module executes the moment something imports it, with the same privileges it would have had in a postinstall hook. Build tooling widens the gap further: bundler plugins and PostCSS transforms run during every build with full filesystem access, and no install-time policy touches them.

Both points are true, and neither makes the change cosmetic. Install-time execution is zero-click: it fires on npm install, before any of your code runs, in every environment that touches the lockfile, including the laptop of someone who cloned the repo just to read it. Import-time execution at least requires the package to be loaded. More concretely, Shai-Hulud's propagation loop depended on automatic execution at install. Take that away and a worm needs its victims to actually run the infected code, which slows the spread from hours to something defenders can react to.

The fair summary is narrower than the announcement's framing but still substantial: v12 does not stop malicious dependencies, it removes their cheapest and fastest execution trigger. Anyone treating an approved allowlist as a security audit is holding it wrong. It is an inventory, and before this change npm did not even have the inventory.

Defaults are the product

Features move the developers who opt in. Defaults move everyone. pnpm proved eighteen months ago that deny-by-default install scripts are livable at scale, Bun proved an ecosystem can grow up without ever allowing them, and npm's flip makes the model universal for JavaScript. The interesting open question is whether npm follows Bun toward a curated default trust list to soften the long tail of native packages. The announcement says nothing about one, and the pinned-allowlist design suggests the team prefers explicit decisions over curated convenience.

What to do with this is unusually concrete for ecosystem news. Upgrade to npm 11.16.0 somewhere low-stakes, run npm approve-scripts --allow-scripts-pending, and look at the list. If it is empty, July costs you nothing. If it is not, you just found out which of your dependencies run code on every install, and after the year the registry has had, that is a list worth reading either way.