Skip to content
Back to blog
Development

npm v12 stopped running install scripts: what changes in your pipeline

Wilson Vargas Martínez 10 minSeptember 21, 2026

After a year of self-replicating worms in the registry, npm turned off install scripts, Git dependencies and remote URLs by default. No Node.js line ships that npm yet: you have to ask for it.

Since 8 July 2026, an npm install running npm v12 does not execute your dependencies’ install scripts, does not resolve Git dependencies and does not download tarballs from remote URLs unless you explicitly allow it. CLI version 12.0.0 shipped that day, and 12.0.2 on 27 July. It is the most aggressive change of defaults in the registry’s history, and it arrives after twelve months in which a self-replicating worm compromised first 500 packages and then more than 1,300 versions.

The detail that catches teams off guard: no Node.js line bundles that npm yet. Upgrading Node does not hand you the new defaults. You have to ask for them, and you have to prepare the repository first, because the first visible effect is a broken build.

What happened in npm between September 2025 and August 2026?

Two waves of the same worm, a year apart and an order of magnitude bigger. In the first, CISA issued an alert on 23 September 2025 over more than 500 compromised packages hit by malware named Shai-Hulud: it scanned the environment for GitHub personal access tokens and API keys for AWS, GCP and Azure, exfiltrated them to a public repository through the GitHub API, and then authenticated to the registry as the compromised developer to publish infected versions of their other packages. That last step is what makes it a worm.

The second wave is from August 2026. Advisory AD-2026-009 from Singapore’s cyber security agency, dated 6 August, puts the ChainDrop variant at more than 1,300 compromised package versions, with roughly 2 billion combined monthly downloads. The affected list explains the reach: keyv 6.0.0, flat-cache 6.1.24, file-entry-cache 11.1.6, cacheable-request 13.0.20, cache-manager 7.2.10. Nobody installs those on purpose; they arrive three or four levels down, under ESLint or an HTTP client.

How do attackers get in, if nobody installed a strange package?

Through a legitimate maintainer’s publish token, and 2026 produced three distinct ways of getting one, all documented by the victims.

  • Out of the CI runner’s memory. On 11 May an attacker published 84 malicious versions across 42 TanStack packages. The method, per the postmortem: they located the GitHub Actions Runner.Worker process and read /proc/<pid>/maps and /proc/<pid>/mem to extract the OIDC token the runner held in memory because the workflow declared id-token: write. An outside researcher caught it 20 to 26 minutes later; the versions were deprecated within one hour and 43 minutes.
  • Out of the developer’s editor. The compromise Red Hat documents in RHSB-2026-006 started on 29 May with a GitHub account taken over through a VS Code extension carrying malware, and ended with 32 packages published under the @redhat-cloud-services namespace. No Red Hat product was built with those versions, but the vector matters: the laptop’s editor is part of the supply chain.
  • Without compromising anyone, through name confusion. On 28 May an actor published 14 malicious packages inside a four-hour window, with spoofed repository metadata and inflated version numbers — 1.0.7265 — to fake a mature history. The second stage was a roughly 195 KB Bun-compiled binary hunting for AWS credentials in Secrets Manager across 16-plus regions, HashiCorp Vault tokens, npm publish tokens and GitHub Actions secrets.

That third case is what explains npm’s change. The theft happened during installation, through the package lifecycle hooks. The victim’s code never had to call anything: someone typing npm install was enough.

What exactly changed in npm v12?

Three automatic code-execution paths became opt-in, per the 9 June announcement:

  1. Install scripts. A dependency’s preinstall, install and postinstall do not run unless the package is approved in the allowScripts field of your package.json.
  2. Git dependencies. --allow-git defaults to none: neither direct nor transitive, which closes a code-execution path that could be triggered from an .npmrc.
  3. Remote URLs. --allow-remote defaults to none: HTTPS tarballs outside the registry need authorisation too.

The same release carries other breaking changes worth reviewing before you upgrade: npm view --json now always returns an array, support for npm-shrinkwrap.json is gone, and npm adduser plus the star, stars and unstar commands were removed. The one that breaks pipelines quietly is the --json change, if any script parses that output with jq.

Do I already have npm v12 if I upgraded Node.js?

No. As of today, the Node.js distribution index shows the 26 line bundling npm 11.19.1, the 24 line — the active LTS, per the support schedule — bundling npm 11.19.0, and 22 still on npm 10.9.8. No line carries 12, so the new version is installed separately: npm install -g npm@12, or by pinning it in whatever manager you use.

This has a concrete operational consequence. If your CI uses a node:24 image and trusts the npm it brings, you are running the old defaults while your team believes otherwise. Our practice is to pin the exact npm version in the pipeline and treat it as one more dependency, never inherit it from the base image.

How do I migrate a repository without breaking the build?

By preparing it under npm 11 before touching npm 12. All of the new behaviour is available behind warnings from npm 11.16.0, so the migration can be rehearsed without changing major version:

  1. npm install as usual, so the CLI records which packages carry scripts.
  2. npm approve-scripts --allow-scripts-pending, which only lists the pending packages and approves nothing.
  3. Review that list, approve what makes sense with npm approve-scripts, deny the rest with npm deny-scripts, and commit the resulting package.json.
  4. Run the full pipeline with --strict-allow-scripts, which applies v12 behaviour while still on npm 11. If it passes, the major upgrade stops being a leap in the dark.

There is npm approve-scripts --all to approve everything pending at once. It works as an initial snapshot in a monorepo with hundreds of dependencies, and beats postponing the migration for six months, but it leaves the job half done: what it approves is today’s state, not a decision. If you use it, keep the list in package.json and review its growth in every pull request — that is where a new package asking to run code at install time becomes visible.

What about native packages like sharp or better-sqlite3?

They are blocked too, through a path that never appears in package.json. A package shipping a binding.gyp triggers an implicit node-gyp rebuild even with no install script declared, so npm treats it as a script and stops it. sharp, better-sqlite3 and canvas are the first cases to surface in any real project.

The failure is loud, which is the good part: the build breaks in the pipeline, not in production. Two reasonable exits. Approve those packages explicitly, leaving a record in the repository of which dependencies are allowed to compile; or prefer distributions with prebuilt binaries where the project offers them, which also shortens image build time.

What if the attacker comes in through the publish token?

npm closed that side in parallel, on dates that are already running. Granular access tokens with the two-factor bypass option lost the ability to perform sensitive account, package and organisation actions from August 2026 — creating or deleting tokens, changing passwords, configuring trusted publishing, managing teams — and from January 2027 they lose direct publishing: they will be limited to reading private packages and staging a publish that waits for human approval with a second factor.

The rest of the pieces appear in the summary GitHub published on 28 July: staged publishing since 22 May, trusted publishing over OIDC extended to CircleCI since 6 April, high-impact accounts held read-only for 72 hours after an email change or the use of a two-factor recovery code since 25 June, and a self-service tool to revoke credentials since 24 June.

The TanStack case tempers the enthusiasm for OIDC: the token was short-lived and they still lifted it out of process memory. Trusted publishing removes the long-lived secret, which is a lot, but the job issuing that token has to be isolated. In the pipelines we build, the publishing job runs no third-party code, external actions are pinned by SHA, and id-token: write lives only in that job, never at workflow level.

How do I avoid installing a compromised version the day it ships?

By putting time between publication and your install. Since 14 July, Dependabot applies a three-day cooldown by default on version updates, while security updates still arrive immediately. That asymmetry is the right one: the TanStack window lasted under two hours, and the cooldown would have covered all of it without delaying a real security patch.

The rest is hygiene you should already have and rarely do in full: npm ci in every pipeline instead of npm install, the lockfile committed and reviewed in each pull request, and no floating tags in production. A ^ range in package.json is harmless while the lockfile rules; it stops being harmless the day someone runs npm install on the server.

What do I do if I already installed a compromised version?

Treat the machine as compromised and rotate from a different one. CISA’s recommendations and the Singapore advisory agree on the order: remove the affected versions from development and CI environments, rotate GitHub personal access tokens, SSH keys, cloud credentials and npm publish tokens, review repositories for commits or new public repositories nobody created, and block outbound traffic to the domains used for exfiltration, with webhook.site at the top.

Two details teams skip, at a high price. Rotating credentials from the same infected machine hands the new ones to the attacker. And secret scanning with branch protection enabled is what turns the next attempt into an alert instead of an incident; CISA asks for it explicitly, alongside phishing-resistant multi-factor authentication.

Where we would start this week

  1. Inventory the scripts. Run npm approve-scripts --allow-scripts-pending in every repository and count how many packages ask to run code at install time. The number is surprising.
  2. Pin the npm version in CI instead of inheriting it from the base image, and rehearse with --strict-allow-scripts before jumping to v12.
  3. Move publishing to OIDC for internal packages, with the publishing job isolated and third-party actions pinned by SHA.
  4. Turn on the Dependabot cooldown and confirm security updates still arrive without delay.
  5. Rehearse a rotation with a stopwatch: how long it takes today to rotate every publish token and cloud credential a team holds. That number is your real response time.

How we help at Athrun Data Intelligence

A 30-minute call to review your repositories’ supply chain: which dependencies run code at install time, which long-lived tokens are still alive, and how exposed your publishing job is. If it fits, we leave the npm v12 migration done with the script inventory committed to the repository, publishing isolated over OIDC, and a rotation procedure rehearsed against a stopwatch rather than written in a document.

Sources

Who wrote it

Next step

Sound familiar? Request the diagnostic.

You tell us the challenge and within 24 business hours we tell you in writing whether it is viable and where to start. No commitment.

Related articles

  1. 01

    Stack to launch a SaaS in 90 days without piling up tech debt

    Cristian Agudelo · 7 min
  2. 02

    Shopify vs headless commerce: when each one makes sense

    Nodier Solano · 6 min