For sixteen years, npm install executed arbitrary code shipped by any
of your dependencies. Not the dependency you picked: any of the hundreds it
drags along behind it. The only defence was a flag, --ignore-scripts, that
I have almost never seen enabled in production.
Since 8 July 2026 and the release of npm v12, that era is over. The
preinstall, install and postinstall scripts of dependencies no longer run
unless explicitly allowed. It closes the most profitable attack vector in the
JavaScript ecosystem, the one used by Shai-Hulud, axios and the 57 Mastra
packages.
I wanted to check whether other ecosystems were following the same path. The answer surprised me: npm did not lead, it caught up with its competitor. pnpm made that decision eighteen months earlier.
What npm v12 actually blocks
npm v12 refuses to run install scripts coming from your dependencies, but
still runs your own. The distinction matters: your own build postinstall
keeps working, it is third-party code that is neutralised.
The block is broader than the headline suggests. It covers three things:
preinstall,installandpostinstallscripts of dependencies;- implicit node-gyp builds, triggered by the mere presence of a
binding.gypfile with no declared script; preparescripts of dependencies installed from Git or a local link.
npm v12 also blocks Git dependencies and remote URL dependencies by
default. The documented reason for Git dependencies is worth a detour: a
malicious package could ship its own .npmrc pointing at a fake git
executable.
To restore a legitimate script, npm provides a dedicated command that builds an allowlist you commit to the repository:
npm approve-scriptsWhy install scripts were the preferred vector
A postinstall script runs with the rights of the developer or the CI runner,
with no validation, no review, and often without anyone knowing it exists.
That is what makes it such an effective weapon: the victim did nothing beyond
installing dependencies.
The incident record is eloquent. The Shai-Hulud worm compromised dozens of AntV packages by stealing runner secrets from GitHub Actions. axios shipped a remote access trojan. On 17 June 2026, 57 Mastra packages and more than 286 malicious versions went through the same door.
The long-standing recommendation, --ignore-scripts, has changed status: it
is no longer advice, it is the default behaviour.
pnpm had an eighteen-month lead
Here is the fact that puts the announcement in perspective. I checked the publication dates directly in the npm registry, which timestamps them:
| Version | Published | What it brings |
|---|---|---|
| pnpm 10.0.0 | 7 January 2025 | Dependency scripts disabled by default |
| pnpm 11.0.0 | 28 April 2026 | 24-hour cooldown enabled by default |
| npm 12.0.0 | 8 July 2026 | Dependency scripts disabled by default |
pnpm disabled install scripts eighteen months before npm, and went further:
since pnpm 11, no version published less than 24 hours ago is installed, with no
configuration required. Bun, for its part, only runs scripts from packages listed
in trustedDependencies.
npm does offer an equivalent waiting period, min-release-age, shipped
in February 2026. But it is disabled by default, which for this kind of
protection amounts to not existing for the vast majority of projects.
PyPI has been setting the pace since 2023
The real metronome of registry hardening is PyPI, and that is rarely said. The order of dates leaves no ambiguity:
- 2023: PyPI ships OIDC trusted publishing, the first major registry to do so. crates.io followed in July 2025, npm in late July 2025, NuGet in September 2025.
- 1 January 2024: two-factor authentication becomes mandatory for every publishing account.
- November 2024: Sigstore-signed digital attestations arrive.
- 22 July 2026: a release refuses any new file after fourteen days, to prevent retroactive poisoning of old versions.
npm did in one year what PyPI built over three, but under duress: it is the only registry that revoked existing credentials en masse, deleting all classic tokens on 9 December 2025.
Other ecosystems do not have the same problem to solve
This is the trap of such a comparison, and I fell into it while preparing this piece: ranking ecosystems by virtue makes no sense when they do not share the same architecture. The execution model matters more than the date of the latest announcement.
| Ecosystem | Code at install time | Nature of the situation |
|---|---|---|
| npm | Blocked since July 2026 | Recent fix |
| pnpm / Bun | Blocked since January 2025 | Early fix |
| Composer (PHP) | Never run for dependencies | Design choice |
| Go | Never existed | Design choice |
| NuGet | Removed in 2017 with PackageReference | Long-standing fix |
| Maven Central | None, JARs are passive | Design choice |
| RubyGems | Still open (native extensions) | Genuine lag |
| PyPI | Possible through a source archive | Neutralised in practice by wheels |
Composer is instructive: its documentation states that only scripts defined in the root package run, and plugins require an allowlist. The problem npm just fixed never existed in PHP.
Go neither: modules are source code, go get executes nothing, and there
are not even publishing accounts to phish since everything comes from
repositories. Presenting that as an advantage earned in 2026 would be dishonest,
it is an original design decision.
What these protections cost
None of these measures is free, and staying silent about it would paint a false picture.
Waiting periods also delay fixes. Waiting 24 hours or seven days before installing a new version protects you from the package compromised an hour ago, but delays the patch for a critical flaw by just as long. Hence the escape hatches each tool provides, which become weaknesses in turn: a package on the exclusion list drops the protection exactly where it mattered.
PyPI's fourteen-day window has a side effect: you can no longer retroactively add a wheel built for a new Python version to an old release. You have to republish. PyPI's own impact analysis puts the friction at 56 projects out of the 15,000 most downloaded, which is low but not zero.
Trusted publishing concentrates risk. It only works with the large CI platforms today. Maintainers publishing from elsewhere stay on tokens, and the security of the ecosystem couples itself to two providers.
Key takeaways
- npm v12, released 8 July 2026, no longer runs dependency install scripts. The first break in that behaviour in sixteen years.
- Only dependency scripts are blocked, yours keep running.
- The block also covers implicit node-gyp builds, which will break projects with native modules in CI.
- pnpm made that decision eighteen months earlier, in January 2025, and has enforced a 24-hour waiting period by default since April 2026.
- npm has offered that delay since February 2026, but disabled by default.
- PyPI has led since 2023: trusted publishing first, mandatory 2FA from January 2024, Sigstore attestations, releases frozen after fourteen days.
- Composer, Go, Maven and NuGet do not have this problem, by design or for a long time. That is not a recent lead, it is a different architecture.
- Waiting periods also delay security fixes: the protection has a cost you should accept knowingly.