Dawloom
All posts

The 15-day cooldown that would have stopped this week's npm attack

Dawloom engineering5 min read

On the morning of August 5, we opened dawloom.com’s package-lock.json and searched it for one word: keyv. It wasn’t there, and that was mostly luck. Twenty four hours earlier, someone had taken over the GitHub account of Jared Wray, maintainer of keyv and cacheable, and published malicious versions of ten widely used caching packages under his name.

Was this a small incident? No. According to Wiz, Snyk, and Socket, the compromised releases self-propagated using stolen npm tokens until they reached at least 434 packages across 1,381 versions. Chainguard put the combined monthly download count for the affected ecosystem north of 2 billion. That’s not a niche dependency three levels deep in a tree nobody opens. That’s most of the JavaScript ecosystem, once a week.

What actually ran on people’s machines

The mechanism is worth understanding because it isn’t exotic. It’s a standard npm feature almost every project already has switched on. A preinstall hook in package.json ran a script called setup.mjs the moment npm install touched one of the trojanized versions. That script downloaded a standalone Bun runtime, then handed off to an obfuscated second stage that harvested npm tokens, GitHub credentials, cloud keys, and CI secrets, according to Snyk’s writeup. The stolen npm tokens are what let the payload republish itself into other maintainers’ packages, which is how ten compromised releases became hundreds.

The detail that should worry you more than the malware itself: keyv 6.0.0 carried a valid OIDC provenance attestation and an SLSA signature when it landed on the registry. Every cryptographic signal the ecosystem spent years building said this release was legitimate. The attacker didn’t forge anything. They ran the real release pipeline with a stolen account. Provenance proves who ran the workflow. It doesn’t prove the workflow wasn’t hijacked.

Timeline showing a malicious package version published on day 0, the ecosystem detecting and pulling it within days, and a 15-day install window opening after that

The fix is a number in a YAML file

pnpm has a setting called minimumReleaseAge, and it does something almost embarrassingly simple: it refuses to install a package version until that version has been publicly available for a set number of minutes. In pnpm-workspace.yaml:

minimumReleaseAge: 21600

That’s 15 days. Socket’s own scanners flagged the keyv payload about six minutes after it was published, which sounds reassuring until you remember that automated detection and an actual takedown across 434 packages run on very different clocks. Malicious npm versions in incidents like this one typically get found and pulled within hours to days once the wider security community starts pulling the thread, well inside a 15-day window. Set the number that high and your install runs after the poison has already been found, not during the window when it’s still live.

You don’t have to freeze every dependency for two weeks to get this. minimumReleaseAgeExclude lets you name specific packages, or patterns like @yourorg/*, that should always install the newest version immediately. That’s the escape hatch for anything you update constantly and trust.

pnpm’s second default matters just as much here. Since pnpm 10, dependency lifecycle scripts, preinstall, postinstall, install, don’t run automatically. A package has to be explicitly approved before its build scripts execute, through pnpm approve-builds, which writes the decision into an allow list in pnpm-workspace.yaml. The keyv attack ran entirely through a preinstall hook. On pnpm, with no approval on file for keyv, that hook simply never fires. One default would have stopped the entire chain at the first step.

The honest tradeoff

A 15-day cooldown also delays legitimate security patches, and we’re not going to pretend otherwise. If a real vulnerability gets fixed in a library you depend on, you wait 15 days for that fix too, unless you’ve excluded it or pinned the patched version yourself the moment it ships. For most teams that’s still the right trade. Same-day critical patches you genuinely can’t wait two weeks for are rare. Compromised maintainer accounts shipping something worse than the bug they’re replacing are, this month, not rare at all. Pin the exceptions, exclude what you watch closely, and let the default cover everything else.

We checked our own dependency tree against the published list of affected packages the day the story broke, and it was clean. On August 5 we migrated dawloom.com from npm to pnpm with a 15-day minimumReleaseAge. We’d point out that we’re a dev shop recommending a dev tool here, so take that endorsement for what it’s worth, but the setting is free and the yaml is public, so check it yourself. It’s now the default we carry into the Node projects we build for clients too, alongside the rest of what’s written up on our tools and stack page. The same instinct is behind why this site itself loads in under a second: fewer moving parts you didn’t choose on purpose, fewer places for someone else’s bad day to become yours.

Paste this into Claude Code or any coding agent

Migrate this project from npm or yarn to pnpm and harden it against npm supply chain attacks:

1. Install pnpm if missing, run `pnpm import` to convert the existing lockfile, delete package-lock.json or yarn.lock, then run `pnpm install`.
2. Create or update pnpm-workspace.yaml with:
   minimumReleaseAge: 21600
   This is 15 days in minutes: package versions younger than 15 days will not install.
3. Add a "packageManager" field for pnpm to package.json.
4. Update anything in this repo that calls npm install or npm run (CI configs, docs, scripts) to use pnpm.
5. Run the project's build and tests to confirm nothing broke. Do not upgrade any dependency versions while migrating.
6. Append this standing rule to my global Claude configuration file (~/.claude/CLAUDE.md, or C:\Users\<username>\.claude\CLAUDE.md on Windows): "Use pnpm instead of npm for all Node.js projects. Always set minimumReleaseAge: 21600 in pnpm-workspace.yaml so newly published package versions wait 15 days before installing. Never run npm install in a project with a pnpm lockfile."

Run it, then read the diff before you approve anything a build script wants to do on your behalf. That part doesn’t get automated away.

The keyv attack was never really about JavaScript, or npm, or even one maintainer’s stolen credentials. It’s about which decisions in your supply chain get made by “has this been out long enough for someone to notice if it’s bad” and which ones get made by “it built, so it shipped.” If your dependency installer can’t answer the first question, that’s the actual gap, whatever language you write in.

If you want a second opinion on your own dependency tree, or want to talk through hardening a project before something like this reaches it, get in touch or read more about how we build web projects.

Got something to build?

Tell us what you need. An engineer replies, not a sales team.

Search the whole site