On August 4, 2026, the maintainer of the library keyv woke up to a day unlike the one he went to sleep with: his GitHub account had for hours been publishing infected package versions, downloaded two billion times a month. This is not a “token leak by a no-name” — it’s keyv, cache-manager, flat-cache, file-entry-cache. They sit in the dependencies of half of the planet’s Node projects, most often transitively: you don’t know about them, but npm install pulls them in.
Let’s examine this incident not as a horror story but minute by minute — because the mechanism is more important than the headline. The Shai-Hulud worm (a reference to the sandworms from Dune) is designed so that an infected machine itself becomes a spreader. And if you run a self-hosted stack, CI runner, or simply build the frontend on your own server — the attack vector goes straight through your build.
Chronology: how the day unfolded
All timestamps are from Wiz and Aikido Security; the investigation at the time of publication was still ongoing, figures were being clarified within 24 hours.
~09:00 UTC. The maintainer’s GitHub account for the keyv/cacheable ecosystem was compromised. Malicious files were added to the main branches of repositories, and a release was rolled out immediately. The key detail: infected versions were published in npm with valid provenance from GitHub Actions. In other words, the badge of “built by a trusted pipeline”, something many rely on, did not guarantee anything here — the attacker acted from inside a legitimate process.
First wave — specific versions. According to Aikido, these included, among others:
keyv 6.0.0 (~604 M downloads/month)
flat-cache 6.1.24 (~580 M)
file-entry-cache 11.1.6 (~571 M)
cacheable-request 13.0.20 (~137 M)
cache-manager 7.2.10 (~16 M)
cacheable 2.5.1
@cacheable/utils 2.5.1
ecto 5.0.1
~13:37 CEST. The scale was different: infected at least 434 packages in 1381 versions, totaling over 2 billion installations per month. The worm reproduced itself — not by the attacker’s hands.
If between 9 AM and the evening of August 4 you ran npm install or npm ci without locked versions (or with an updated lockfile) — consider that you may have pulled an infected version. Check not only direct dependencies but the entire package-lock.json.
Anatomy of the infection: what the payload does
The mechanism is two-step and worth understanding — the backbone of the entire “epidemic” effect.
Step 1. Installation hook. In every infected package, setup.mjs and Math_Symbol.js were added, and "preinstall": "node setup.mjs" was defined in package.json. The hook fires automatically on installation — no application startup needed.
Step 2. Dropper pulls Bun. setup.mjs downloads the Bun runtime and runs the main payload bypassing the usual Node path — a file Math_Symbol.js of about 728 KB with obfuscated code stealing secrets. Network logs show this via User-Agent Bun/1.3.13 and temporary directories /tmp/bun-dl-*/.
Step 3. Total exfiltration of secrets. A scanner traverses the filesystem with ~200 glob patterns (in 64 parallel threads) and pulls basically everything within reach:
What is leaked to the attacker on a successful infection:
- npm tokens from
~/.npmrc(validated directly in the registry); - GitHub tokens: classic PAT, OAuth, GitHub App; on CI runners — OIDC tokens for publishing;
- AWS: keys from config files, environment variables, EC2 metadata, ECS endpoints, and Secrets Manager;
- Kubernetes secrets (service account tokens, secrets via API) and HashiCorp Vault;
- Stripe and Slack tokens;
- Wiz data — and also AI tool storage (Claude, OpenAI, Cursor, Gemini) and crypto wallets (Solana, Monero, Foundry);
- SSH keys,
.env, Terraform state, Docker configs, IDE settings.
Step 4. Propagation. The worm then infects everything it can with stolen rights:
- via npm: with the stolen token, it finds accessible packages, bumps the patch version, reinserts the same hook, and republishes. The second generation is identifiable by the name
math_init.jsinstead ofMath_Symbol.js; - via GitHub: server-to-server GitHub App tokens commit to up to 50 repository branches, adding malicious hooks to
.claude/settings.jsonand.vscode/tasks.json— so that code runs the next time the IDE is opened. Commits are signed by the author “claude”.
Step 5. Data exfiltration. Collected secrets are encrypted with RSA (only the private key owner can decrypt) and uploaded to public GitHub repositories with the description “Shai-Hulud: Here We Go Again” — about 1300 were counted. If upload failed, a backup channel exists npm-cache[.]com:443/router, and C2 addresses are pulled from a smart contract on the Ethereum network. This is notable: hard-coded domains can be blocked, but an address on the blockchain cannot be blocked or removed via abuse reports.
One detail that saves you: preinstall in npm 12
There is an important fork worth noting, because it changes your exposure.
According to JFrog, starting with npm 12 the preinstall hooks do not run by default. If your builds are already on npm 12+, the automatic infection via this vector no longer occurs — the payload simply does not start during installation. This is not a reason to relax (the files still end up in node_modules, and running the script manually can trigger them), but there is a huge difference between “infected at the first npm ci” and “you need to do something else to trigger it.”
Hence the first practical takeaway for today: check the npm version on all build agents and development machines and upgrade it. Also, adopt a habit of installing dependencies with --ignore-scripts where possible (many packages work fine without postinstall hooks).
What to do right now
Procedural steps if you or your team touches the Node ecosystem:
- Find infected versions. Run through the list of known packages and versions in
package-lock.json(full lists are maintained by Wiz and Aikido). Pay attention to more than just direct dependencies. - Search for on-disk artifacts: files
setup.mjs,Math_Symbol.js,math_init.js; directories/tmp/bun-dl-*; unexpected"preinstall"inpackage.jsonof dependencies. - Check Git history and branches for commits from “claude” and edits to
.vscode/tasks.json/.claude/settings.json. - Rotate anything that could have leaked: npm tokens, GitHub PAT/OAuth, AWS keys, kube tokens, Vault secrets, Stripe/Slack keys, SSH keys. Not “for precautionary suspicion,” but everything that was accessible from the infected machine.
- **Rebuild potentially compromised runners from scratch, not just “clean up.”
- For the future — lock dependencies, enable allowlisting of packages and integrity checks.
Separately for CI: runners are the most valuable loot. They hold the richest secrets (publishing keys, cloud access), and npm ci runs there automatically and often with broad permissions. If you have even the slightest suspicion that a poisoned version passed through the pipeline, start rotating CI secrets first, before everything else.
What’s important in all this, and what’s noise
Honestly: panicking over the fact that there’s malware in npm again is pointless — this happens regularly. The important thing is different. First, the attack showed that provenance from GitHub Actions is not equal to security if the maintainer himself is compromised — you cannot rely on that badge as a final guarantee. Second, the combo “self-spread + theft of CI secrets + C2 in the blockchain” is no longer a one-off blast, but a pattern that will be repeated. Third, the salvation turned out to be painfully simple: fresh npm and --ignore-scripts cut off the main vector.
What not to do yet — don’t take the final numbers of the “affected” as fixed. At the time of publication the investigation was ongoing, and estimates (200+, 434+, 868 packages from different researchers) vary because they measure differently and at different times. The scale is confirmed independently by several teams; the exact number is still being clarified.
Sources
- Aikido Security — Keyv and friends compromised in npm supply chain attack
- Wiz — keyv and cacheable npm supply chain attack (+ IoC list on GitHub)
- JFrog Security Research — Shai-Hulud is back
Forum-related: Linux closed staging for neural-network patches.
How is your protection against this on build agents organized: is --ignore-scripts on by default, private proxy registry, package allowlist, something else? And have you already checked your lock files for this incident?
