Engineering notesBy Daniel ChenLast verified 19 min read

Would npm 12 and a seven-day cooldown have stopped this year's npm attacks?

npm 12 blocks dependency install scripts by default, and every package manager now offers a cooldown on new releases. Replayed against all 12,693 malicious npm releases flagged in 2026, npm 12's defaults would have stopped almost every hijack of an established package, and a seven-day cooldown would have skipped two in three releases overall. The price is security fixes that arrive 2.5 days later on average.

  • npm
  • supply-chain
  • security
  • dependencies

npm 12 stopped running dependency install scripts by default in July 2026. Most package managers now also offer a cooldown: a rule that new versions are not installed until they are a set number of days old. Both are sold as defences against the year's run of malicious npm releases. This note checks the claim against the record. I took every malicious npm package flagged in the OSV database in 2026, worked out how each one ran and how long it stayed on the registry, and asked which defence would have stopped it. Then I measured the main cost of a cooldown: how much later you get security fixes.

TL;DR

  • Hijacked popular packages: of 1,063 malicious releases of established packages (the Shai-Hulud worms, the TanStack compromise and others), 96% of those with a known execution path ran only through install-time code or a git dependency, both of which npm 12 blocks by default. 97% of them were flagged or removed within seven days, so a seven-day cooldown would very probably have skipped them too. Only 4 of 854 got past both.
  • All 12,693 malicious releases: a seven-day cooldown would have skipped 67% (95% interval 66%–68%), because npm had removed them by then. A one-day cooldown, pnpm's default, would have skipped 53%. One in three was still installable after a week, and 675 flagged releases are still on the registry today.
  • npm 12 on its own: 53% of the releases whose advisory says how the code runs used install scripts and nothing else. Most advisories (54%) don't say, so that share covers less than half the data.
  • Together: where both the execution path and the outcome are known, npm 12's defaults plus a seven-day cooldown would have stopped 85%. 15% would have got through both. Most of those run when your code imports the package, and they stayed on the registry for more than a week.
  • The cost: with a seven-day cooldown, you would have got 51% of security fixes in the 1,000 most-used packages no later than before, but the rest late. On average fixes arrived 2.5 days later, and 38% arrived more than three days late. A one-day cooldown cost an average of 0.2 days.
  • pnpm's one-hour claim doesn't hold for 2026: its docs say malicious releases are usually removed "within an hour". Here, 29% were.

Tested with

Malicious packages
OSV npm dump (all.zip, last modified 8 October 2026 16:48 UTC): every MAL- record published in 2026, 12,856 records, of which 163 were withdrawn
Registry data
npm registry metadata (packuments) for all 12,692 affected packages, fetched on 8 October 2026. Metadata only: no package tarball was downloaded, unpacked or run
Security fixes
4,442 GitHub-reviewed npm advisory rows (advisory, package, first fixed version) published 1 October 2025 to 30 September 2026; package popularity from ecosyste.ms
Tools
npm 12.2.0 on Node.js 24.21.0; pnpm 12.10.1 for the cooldown check; Python 3 for the analysis
App run
10 open-source apps installed with npm 12 in a bubblewrap sandbox with no credentials (secondary, see below)
Spend
US$0: no paid APIs or models were used

Why measure this at all

Many of 2026's big npm incidents worked the same way. An attacker got publish rights to a real package, often by stealing a maintainer's token or abusing a CI publishing workflow. They pushed a version with a preinstall hook, and that hook stole credentials from every machine that installed it. Worms such as Shai-Hulud then used those credentials to publish more poisoned packages.

The package managers responded in two ways. npm 12.0.0 (8 July 2026) changed the default: "Dependency lifecycle scripts are now blocked by default unless allowed by the root package's allowScripts policy" (npm CLI changelog). It also stopped fetching git dependencies unless you opt in (allow-git now defaults to none). Separately, every major tool has gained a cooldown:

  • npm: min-release-age, in days, off by default (added in npm 11.10.0).
  • pnpm: minimumReleaseAge, in minutes, on by default since pnpm 11 at 1,440 minutes (one day).
  • Yarn: npmMinimalAgeGate.
  • Bun: install.minimumReleaseAge.
  • Renovate: minimumReleaseAge.
  • Dependabot: a default three-day cooldown for version updates, which "does not apply to security updates".

pnpm's documentation gives the reasoning: "In most cases, malicious releases are discovered and removed from the registry within an hour" (pnpm settings).

Neither defence is free. Blocking install scripts breaks packages that really need them, such as native modules and binary downloaders, until someone approves each one. A cooldown delays every new version, including the one that fixes a vulnerability. The question here is whether the protection is worth that cost, using what actually happened this year rather than the vendors' examples.

The setup

The attacks: every malicious npm release flagged in 2026

OSV gathers malicious-package reports from GitHub's malware advisories, Amazon Inspector, OpenSSF Package Analysis, ReversingLabs, Google and others. I took every npm MAL- record that OSV published between 1 January and 8 October 2026 and left out the 163 that had been withdrawn. That left 12,693 records in 12,692 packages. Just over half (6,619) cover the whole package, meaning the package itself was malicious from the start, as typosquats and dependency-confusion packages are. The rest list specific bad versions of a package.

For each record I fetched the package's registry metadata and read three times from it. Publication is the time of the first malicious version (for whole-package records, when the package was created). Removal is when npm unpublished it or replaced it with a -security placeholder. The flag is when it first appeared in OSV or one of its sources. Package tarballs were never fetched: the fetch script refuses any URL that could point at one.

What counts as "stopped"

  • npm 12's defaults stop a release if its advisory says the malicious code runs through an install-time hook (preinstall, install, postinstall, or a binding.gyp that node-gyp runs during install), or arrives through a git dependency, and through nothing else. If the code also runs when your application imports the package, blocking the hook does not help. I checked in npm 12.2.0's source that binding.gyp builds go through the same allowScripts gate. A rule-based classifier reads each advisory. Where an advisory does not say how the code runs, I count the execution path as unknown. I never assume it either way.
  • A cooldown of N days stops a release if npm had removed it less than N days after it was published. By the time the cooldown would have let it in, it was gone. Releases still on the registry count as not stopped.
  • A cooldown plus a malware feed is a more generous second scenario. The release also counts as stopped if it was publicly flagged within N days, as it would be if your tooling refused flagged versions. npm does not do this on its own.

Hijacked established packages

Most records are throwaway packages that few people install. The incidents that hurt were hijacks of real packages. I define a hijacked established package as a record that lists specific bad versions of a package that already had at least five earlier versions, the first of them published at least 90 days before the bad one. That gives 1,063 records. Six campaigns account for 733 of them. For those I coded the execution path by hand from the advisories and, for the TanStack compromise, from the maintainers' postmortem.

The cost: security fixes

For every GitHub-reviewed npm advisory published between October 2025 and September 2026, I took the first fixed version, the time npm published it and the time the advisory became public. Without a cooldown, you can install the fix once both exist. With an N-day cooldown, you also have to wait until the fix is N days old. The extra wait is the gap between the two. The main set is the 1,000 npm packages with the most dependent packages, according to ecosyste.ms.

The questions, definitions and success criteria were written down before the full analysis. One change came after seeing the data, and it is disclosed under Limitations.

Results

How long malicious releases stayed installable

The median malicious release was removed 10.8 hours after publication. The spread is very wide: a quarter were gone within about five minutes, and a quarter were still up after 13 days. The chart shows the share still installable at each point in time.

Line chart of the share of malicious npm releases still installable over time since publication, on a log scale from 15 minutes to 30 days. All releases, counted until removal: about 71% after 1 hour, 47% after 1 day and 33% after 7 days. All releases, counted until removed or publicly flagged: 32% after 1 day and 17% after 7 days. Hijacked established packages, counted until removed or flagged: above 75% for the first three hours, then a steep drop to 13% after 1 day and 3% after 7 days.
Share of malicious releases still available, by time since publication. Kaplan–Meier estimates. All releases: 11,073 records with a publication time and either a removal time or still listed. Hijacked established packages: 1,063 records, counted until removed or flagged (see Limitations for why).
Share of 2026's malicious npm releases a cooldown would have skipped, by cooldown length. "Removed" counts a release as stopped only if npm had removed it before the cooldown ended; 95% Wilson intervals; about 11,060 records per row. "Removed or flagged" also counts releases publicly flagged by then.
CooldownRemoved in timeRemoved or flagged in timeHijacked established packages, removed or flagged
1 hour29% (28%–30%)39%8%
1 day53% (52%–54%)68%87%
3 days61% (60%–62%)78%95%
7 days67% (66%–68%)83%97%
14 days71% (70%–72%)86%98%

Two things stand out. First, the gain flattens after a few days. Going from one day to seven adds 14 points; going from seven to fourteen adds four more. Second, a long tail never goes away. 675 releases that OSV has flagged as malicious are still listed on the registry, 6% of the releases with usable timing. The median one has been listed for 84 days. I report counts only and do not name them. A cooldown of any length does nothing about these. You need a tool that reads malware advisories.

What npm 12's script block covers

5,822 of the 12,693 advisories (46%) say how the malicious code runs. Of those, 3,089 (53%, 95% interval 52%–54%) run only through install-time scripts, so npm 12's default would have stopped them. Another 639 use an install script plus another route, usually code that runs on import. 2,094 don't use install scripts at all, so blocking scripts makes no difference to them. Across all 12,693 records, that means at least 24% would have been stopped by npm 12 alone. The true share for the 6,871 silent records is unknown.

I checked the classifier by hand on 60 records it had labelled. It matched my reading exactly on 51 and agreed on the question that matters here (install script only, or not) on 53. I also read 30 records it called unclear. In 28 the advisory really doesn't say how the code runs, and none of them hid an install script it had missed.

Both defences together

Stacked bar chart of which defence would have stopped each malicious release. All 2026 releases that state how they run (4,999): both npm 12 and a 7-day cooldown 30%, npm 12 only 19%, cooldown only 36%, neither 15%. Hijacked established packages (854): both 96%, cooldown only 3%, npm 12 only 1%, neither under 1%.
Which defence would have stopped each release: npm 12's script block, a seven-day cooldown, both or neither. Top: records whose advisory states the execution path and whose seven-day outcome is known, with the cooldown counted against registry removal. Bottom: hijacked established packages with a known execution path (campaigns hand-coded), with the cooldown counted against removal or flagging.

The two defences overlap less than you might expect. Across all releases with a known path and outcome, 30% would have been stopped by both, 19% only by npm 12, 36% only by the cooldown and 15% by neither. Each catches a different kind of attack. Script blocking catches quick smash-and-grab releases, whatever their lifetime. A cooldown catches anything npm removes quickly, however it runs. What gets past both is mostly code that runs on require or import, in packages that stayed on the registry for more than a week.

A cruder version of the same sum keeps every record with a known outcome and assumes that any record without a stated execution path gets past npm 12. Then a seven-day cooldown plus npm 12 stops 76%, and a one-day cooldown plus npm 12 stops 64%. Treat these as floors.

Hijacked popular packages: npm 12 alone would have covered almost all

For the attacks that made the news, the picture is much clearer. Of the 854 hijacked-package releases whose execution path is known, 824 (96%, 95% interval 95%–98%) ran only at install time or through a git dependency. npm 12's defaults would have stopped them before any of their code ran. The hand-coded campaigns:

The largest 2026 campaigns in the data, matched by advisory text. Records include all of a campaign's packages, not only those that meet the established-package definition. Hours: median time from publication to removal or public flag, whichever came first.
CampaignRecordsHow it ranMedian hours to flag or removalFlagged or removed within 1 day
Shai-Hulud "Here We Go Again" worm (August, began with keyv and cacheable)444preinstall hook4.899.8%
Mini Shai-Hulud, compromised atool account (@antv and others, May)317preinstall hook3.799.7%
"Mini Shai-Hulud is back", starting with TanStack Router and Start (May)177git dependency with a prepare script (blocked by the allow-git default)5.0100%
Mini Shai-Hulud, Red Hat Cloud Services trusted publisher (June)32preinstall hook8.9100%

Two smaller ones fit the same pattern: the Miasma worm on 20 LeoPlatform packages (a binding.gyp trick that "bypasses lifecycle script scanners" but not npm 12's gate) and four SAP CAP packages (preinstall). The TanStack attack arrived through a git dependency, which npm 12 no longer fetches unless you opt in. In my app run, npm did run a git dependency's prepare step once git dependencies were allowed, so for this kind of attack allow-git is the setting that matters.

These hijacks were also caught fast. The median was 3.7 hours from publication to removal or public flag. 87% were flagged or removed within a day and 97% within seven. Only 4 of the 854 would have got past both npm 12 and a seven-day cooldown, and 12 past npm 12 plus a one-day cooldown. A caveat matters here. For hijacked packages the registry's removal time is often unreliable, so these figures count the public flag as the end of the danger. Counted by recorded removal alone, a seven-day cooldown stops at least 43% (see Limitations). The TanStack postmortem gives a direct check: npm removed the tarballs 2 hours 53 minutes to 4 hours 35 minutes after publication (TanStack postmortem).

Checking pnpm's one-hour claim

pnpm's docs say most malicious releases are removed within an hour. In this data, 29% were (95% interval 28%–30%; 31% if removal times inferred from the last-modified date are left out). For hijacked established packages, only 8% had even been flagged within an hour. The claim may reflect earlier years or a different definition. For 2026 it overstates how quickly the registry cleans up, which is an argument for pnpm's one-day default rather than against it.

The cost: security fixes arrive later

Stacked bar chart of the extra wait for security fixes in the 1,000 most-depended-on npm packages, by cooldown length. 1 day: 76% no extra wait, mean 0.2 days. 3 days: 64% no extra wait, mean 0.8 days. 7 days: 51% no extra wait, 38% more than 3 days late, mean 2.5 days. 14 days: 42% no extra wait, 53% more than 3 days late, mean 6.3 days.
Extra wait for the first fixed version, compared with no cooldown: 761 advisory rows in the 1,000 npm packages with the most dependents, October 2025 to September 2026.

A cooldown costs less than it sounds, because 96% of fixes in the top 1,000 packages were published before their advisory went public. Maintainers usually ship the fix and announce afterwards, so the cooldown clock is often already running by the time you hear about the vulnerability. Even so, longer cooldowns cost real time:

Extra wait for security fixes caused by a cooldown, 761 advisory rows (advisory, package, fixed version) in the 1,000 most-depended-on npm packages. "When late" is the median extra wait among fixes that were delayed at all.
CooldownNo extra waitMore than 3 days lateMean extra waitWhen late, median
1 day76%0%0.2 days0.9 days
3 days64%0%0.8 days2.6 days
7 days51%38%2.5 days6.0 days
14 days42%53%6.3 days12.8 days

A few packages dominate the advisory list. Electron and Next.js account for 254 of the 761 rows, because Electron ships the same fix on several release lines. If each package counts equally, the share with no extra wait at seven days falls to 41%. Without those two packages it is 47%. Across all 4,181 npm advisory rows with a fix, the seven-day figure is 47% with a mean of 2.5 days, and the same holds for critical and high severity alone (47%). Severity made little difference.

You can avoid most of this cost. npm's min-release-age-exclude exempts named packages from the cooldown. When a cooldown blocks npm audit fix, npm 12 warns and exits with an error rather than failing silently. A one-off --min-release-age=0 install of the fixed version also works. Dependabot already skips its cooldown for security updates. The cost only bites if nobody is watching advisories, which is exactly when a cooldown is most tempting to rely on.

Day-to-day friction: ten real apps under npm 12

This part is secondary and small. I installed ten open-source apps with npm 12.2.0 at their current default-branch commits, each in a bubblewrap sandbox with no credentials. Three could not be tested for reasons that have nothing to do with npm 12's new defaults:

  • TypeORM requires pnpm.
  • video.js has no lockfile.
  • Nest's lockfile hit a peer-dependency conflict. Without an npm 11 run for comparison, I can't say whether npm 11 fails the same way.

Of the other seven:

  • Two failed at once on the new git-dependency default: Insomnia and Mattermost Desktop. npm 12 prints a clear EALLOWGIT error, and --allow-git=all gets past it. After that, both stopped on problems in my sandbox that I did not chase (a missing make, and a git dependency's build step).
  • Four installed with some scripts blocked: Bitwarden (15 packages), LibreChat (7), Parse Server (4) and Jellyfin Web (3). Mattermost Desktop had 7 more once git dependencies were allowed. The blocked packages are native modules, binary downloaders and a few post-install messages, such as esbuild, @swc/core, protobufjs and core-js.
  • Two already ship an allowScripts list: Uptime Kuma (17 entries) and Insomnia (9). Uptime Kuma installed with nothing blocked.
  • Builds: all three apps with a top-level build script built successfully with the scripts still blocked.
  • Approving: npm install-scripts approve --all followed by npm rebuild worked for all four apps.

Approvals are pinned to exact versions by default, so expect to re-approve when those packages update. I didn't run test suites, so the build results are a lower bound on breakage.

What I would do with this

  1. Move to npm 12 and keep its defaults. For the hijacks that hurt most this year, the install-script block is the single most effective control. It works whether or not anyone has spotted the attack yet. Approve scripts one package at a time with npm install-scripts approve, review the list in code review, and leave allow-git at none unless you need it.
  2. Set a cooldown of a few days, not weeks. One to three days takes most of the benefit (53% to 61% of all malicious releases removed in time, and 87% to 95% of hijacks flagged or removed) for well under a day's average delay on fixes. Seven days buys a few more points at a cost of 2.5 days on average.
  3. Give security fixes a fast path. Use min-release-age-exclude or a one-off --min-release-age=0 install for an advisory you need to act on, and keep Dependabot or Renovate security updates outside the cooldown.
  4. Add a malware feed for the long tail. 675 flagged releases are still installable, and neither defence handles code that runs on import and stays up for weeks. That needs a scanner or registry proxy that reads OSV or GitHub malware advisories.
  5. Install from a lockfile in CI (npm ci), so a cooldown or approval list is applied once, by a person, rather than at every build.
# .npmrc (npm 12)
min-release-age=3
# packages you maintain yourself, or a fix you need today
min-release-age-exclude[]=@your-org/*
allow-git=none

Limitations

  • OSV is not every attack. It holds what its sources reported. Malicious releases that nobody caught are missing, and those are exactly the ones a cooldown cannot help with. Campaigns and bulk reporting also make the count lumpy: August alone has 4,078 records. By month, the share a seven-day cooldown would have stopped ranges from 49% (May) to 83% (January).
  • Removal times are reconstructed from metadata. I used the recorded unpublish time where there was one (3,222 records) and the -security placeholder time otherwise (6,537). For 639 records the only signal was the package's last-modified date, which is an upper bound. No removal time could be recovered for 672 removed releases, and 948 records had no registry metadata at all. Those are left out of the timing shares. Across the robustness checks, the seven-day figure ranges from 58% to 69%; the low end keeps only records that have their own version publication time.
  • One change after seeing the data. For hijacked established packages, 483 of the 495 recorded removal times came from that last-modified fallback. Spot checks showed it can be months late for such packages. One Mini Shai-Hulud release shows 125 days, although its first public report came less than four hours after publication. TanStack's packages show no removal time at all, although the maintainers report removal within five hours. So for hijacks only, the headline figures count the public flag as the end of exposure. Removal alone gives a lower bound of 43% at seven days. The pre-registration file records this amendment and when it was made.
  • Flag times are imperfect. 823 OSV records carry a date-only publication time. For those I used the end of that day, unless a source reported earlier, which is conservative. 205 records were flagged before their recorded publication time and count as flagged at publication.
  • Execution paths come from advisory text, not from running the code, and 54% of advisories don't say. The hand checks (51 of 60 exact matches; 53 of 60 on the npm 12 question) are a small sample. I coded the "Mini Shai-Hulud is back" campaign from the TanStack postmortem's description and applied that to the whole wave, which is an assumption.
  • "Stopped" assumes the defaults stay on. If a project approves every script, or sets dangerously-allow-all-scripts, npm 12 protects nothing. Approvals are pinned to exact versions by default (allow-scripts-pin), so a hijacked new version of an approved package still needs approving. Turn pinning off, or approve updates without reading them, and that protection is gone. Stolen tokens and later pivots are out of scope.
  • The fix-delay measure is a model. It assumes you would install a fix as soon as both the fix and the advisory exist. Teams that update weekly already wait longer than any short cooldown, so for them the extra cost is smaller. It counts first fixed versions per release line, so packages with many lines weigh more. I report package-weighted figures for that reason.
  • The app run is small and rough. 7 usable apps, one commit each, builds but not tests. There was no npm 11 baseline: "approve all" stood in for the old behaviour. It ran in a rootless bubblewrap sandbox on a shared machine, not a container. The sandbox had read-only system directories, a private /tmp, no access to home or project directories, and a cleared environment (HOME set to a throwaway directory, no tokens or keys, and the analysis API key unset). No sudo was used and no system packages were installed, so build steps that need tools such as make failed. That is my environment, not npm 12.

Methods note

Everything is a set of Python scripts over public metadata. One script parses the OSV dump and classifies each advisory's execution path with negation-aware rules, so "declares no preinstall hook" is not read as a hook. A second fetches each package's registry document over HTTPS, keeps a trimmed copy (times, version list, dist-tags; no tarball links), and works out publication, removal and flag times. A third computes the shares, Wilson intervals and Kaplan–Meier curves. A fourth checks every number in this note against the results files. The fix-delay script joins GitHub-reviewed advisories from the same OSV dump to registry publication times. The cooldown settings were checked on a real package: with min-release-age=7, npm 12.2.0 resolved vite to 8.3.2 instead of that day's 8.3.4. pnpm 12.10.1 resolved 8.3.3 by default and 8.3.2 at 10,080 minutes.

Safety was a hard rule. No known-malicious package or tarball was downloaded, unpacked or run. Only advisory text and registry metadata were read, and the fetcher refuses tarball URLs. The benign apps were installed only inside the sandbox described above.

How this was made

Research, the scripts and the analysis were done with AI assistance. Every number comes from the scripts and the results files, and a check script compares the figures in this note against them. Daniel Chen reviewed the design, the results and the text, and is responsible for what it says. Nothing here was paid for or reviewed by npm, GitHub, pnpm or any other vendor.

Sources

  1. OSV, npm ecosystem data dump. osv-vulnerabilities.storage.googleapis.com/npm/all.zip (downloaded 8 October 2026).
  2. npm CLI changelog, v11.10.0 to v12.2.0. github.com/npm/cli (read 8 October 2026).
  3. npm Docs, "config" (npm 12): min-release-age, min-release-age-exclude, allow-git. docs.npmjs.com (read 8 October 2026).
  4. pnpm, "Settings": minimumReleaseAge. pnpm.io/settings (read 8 October 2026).
  5. Yarn, "Settings (.yarnrc.yml)": npmMinimalAgeGate. yarnpkg.com (read 8 October 2026).
  6. Bun, "bunfig.toml": install.minimumReleaseAge. bun.com (read 8 October 2026).
  7. Renovate, "Configuration options": minimumReleaseAge. docs.renovatebot.com (read 8 October 2026).
  8. GitHub Docs, "Dependabot options reference": cooldown. docs.github.com (read 8 October 2026).
  9. T. Linsley, "Postmortem: TanStack npm supply-chain compromise", 11 May 2026 (updated 15 May). tanstack.com.
  10. ecosyste.ms, npm packages ranked by dependent packages. packages.ecosyste.ms (fetched 8 October 2026).

Changelog

  • : first published.
All engineering notes