Shai-Hulud Took keyv — and the Malware Shipped With Valid Provenance
Confirmed and ongoing. On 4 August 2026, the GitHub account of the maintainer behind keyv — a key-value storage library with roughly 127 million weekly downloads — was compromised and used to publish credential-stealing malware across the maintainer's entire package family. Socket's Threat Research team and Aikido have both published analyses; Aikido reports the campaign spread to at least 868 further packages across 1,381 versions, together accounting for over 2 billion monthly installs. New malicious versions were still appearing as this was written. The worm is Shai-Hulud — the same family behind the 2025 waves, and the same one that reached Suno.
What Was Hit
The initial family shares one maintainer, which is why one account takeover reached all of it at once:
keyv— 619M monthly downloadsflat-cache— 565M/monthfile-entry-cache— 557M/monthcacheable-request— 133M/month@cacheable/utils— 34M/month;cacheable— 29M/month;@cacheable/memory— 28M/monthcache-manager— 16M/month;@cacheable/node-cache— 5.9M/month;ecto— 4K/month
Most teams reading this do not install keyv deliberately. They install something that installs something that installs it — these sit deep in dependency trees under common tooling, including ESLint. If you have a JavaScript project with a linter, assume you are in scope until you have checked. Most affected users never typed any of these names.
Socket names specific poisoned versions, which is what you actually need for an audit: keyv@6.0.0, cacheable@2.5.1, @cacheable/utils@2.5.1, @cacheable/memory@2.2.1, @cacheable/net@2.1.1, @cacheable/node-cache@3.1.2, flat-cache@6.1.24, file-entry-cache@11.1.6, cacheable-request@13.0.20 and cache-manager@7.2.10. Treat that list as a floor, not a ceiling — Socket is updating it as new variants land, and their campaign page is the live version.
Are you running an affected version?
Drop in your package-lock.json or yarn.lock. It is read in your browser and never uploaded — there is no endpoint to send it to. Checks 10 packages against 10 known-bad versions.
Known-bad list as published by Socket's live campaign list. It is being updated as new versions appear — for anything time-critical, check the source.
How It Worked
Malicious files were committed straight to main, and a release was cut immediately. Each affected package gained two files — setup.mjs, an obfuscated dropper, and Math_Symbol.js, a 728 KB payload — plus a "preinstall": "node setup.mjs" hook in package.json. A preinstall script runs automatically on npm install. No one has to import the package, call it, or ship it to production. Installing is executing.
The dropper downloads a standalone Bun runtime (v1.3.13) and executes the second stage under it. That is not a convenience — it is evasion. Running under Bun sidesteps the host Node version entirely, along with any Node-level monitoring, hooks or instrumentation you have in place. If your detection assumes malicious JavaScript runs under the Node you installed, it does not see this.
What it harvests is broader than a developer's laptop secrets. Reporting describes AWS, GCP and Azure keys, HashiCorp Vault tokens, Kubernetes service account tokens, GitHub Actions OIDC credentials, and npm publishing tokens, alongside .npmrc and GitHub CLI configuration. That list is a CI/CD environment, not a workstation — which tells you where this was designed to land.
Exfiltration runs over DNS and by committing stolen secrets into attacker-created GitHub repositories — this wave titled "Shai-Hulud: Here We Go Again". It then uses the stolen publishing credentials to poison packages belonging to other maintainers. That is the worm behaviour, and it is why the affected count kept climbing through the day.
The Part That Should Worry You: Provenance Held
The poisoned versions were published with valid provenance, signed by GitHub Actions. Nothing was forged. No signature failed. Any supply-chain control that asks "was this package built by the real CI pipeline from the real repository?" got back a truthful yes.
Because it was. The attacker did not defeat provenance — they stood upstream of it. They took the maintainer's account, committed to the real repository, and let the legitimate pipeline sign genuinely malicious code. Provenance attests to origin, not to intent, and this incident is the cleanest demonstration of that distinction we have seen.
If your dependency policy treats "signed with provenance" as a trust decision rather than a data point, this attack passes straight through it. Provenance answers "did this come from where it claims?" It has never answered "should you run it?"
Socket's analysis sharpens this further: the worm propagates by repackaging other maintainers' packages with the same hook and republishing them through npm OIDC trusted publishing — the mechanism specifically designed to remove long-lived publish tokens from CI. It is a genuine security improvement, and the worm rides it, because a workflow that is trusted to publish will publish whatever the repository contains. Hardening the credential does not help when the attacker owns the repository the credential is trusted for.
It Plants Itself in Your Editor
The payload writes autostart hooks into .claude and .vscode directories. Read the trigger condition carefully, because it is the worst detail in this incident: those hooks execute when a developer or an AI coding agent opens the cloned repository. No npm install is required.
That breaks the containment model most teams are working from. "Which machines ran an install?" is the wrong question — the right one is "which machines have cloned or opened this repository since the 4th?" A developer who pulls the repo to investigate the incident, or an agent that opens it to help with cleanup, executes the payload by doing so.
Cleanup misses this reliably. Teams rebuild node_modules, rotate every token, and consider it closed, while a project-local autostart hook sits in a dotfolder that git may well be ignoring. And because exfiltration destinations are resolved over DNS rather than hardcoded, blocklisting a domain does not necessarily cut the channel.
If you use Claude Code, Cursor, VS Code or any agent that executes project-local configuration, check those directories explicitly on every repository that touched an affected version. An agent that runs a hook on open is a persistence mechanism holding your developer's privileges.
What To Do Now
- Assume execution, not just exposure. A
preinstallhook fires on install — including in CI, including on builds that later failed. Any machine that rannpm installagainst an affected version is in scope, not just production hosts. - Then widen the question past installs. Because of the editor and agent hooks below, "who installed?" is not the boundary. Ask who has cloned or opened an affected repository since 4 August — including anyone who pulled it to help investigate.
- Revoke, don't rotate — and do npm and GitHub first. Rotation leaves the old credential valid until it expires; revocation kills it now, which is the difference that matters when a worm is actively publishing. npm and GitHub tokens go first because those are what spread it to other people. Then AWS/GCP/Azure keys, Vault tokens, Kubernetes service account tokens, and anything reachable from a host that ran install scripts.
- Block the scope at your registry proxy. The strongest available containment: deny
keyv,@keyvandcacheableentirely until the account is confirmed clean. Pinning is a race against a list that is still being updated; a scope block is not. - Pin by integrity hash, not by range. If you must keep installing, rebuild lockfiles against known-clean versions with no caret or tilde ranges, and no
npm update. A range is an instruction to fetch whatever is newest, which is precisely the failure mode here. - Rebuild clean.
npm cache clean --force, deletenode_modules, reinstall from a pinned lockfile. A cached tarball survives a version pin. - Check your CI logs before your laptops. Build agents install more often than developers do, hold better credentials, and nobody watches them.
- Grep for the persistence, not just the package. Look for
setup.mjsandMath_Symbol.jsin dependency trees, and inspect.claudeand.vscodedirectories in affected repositories for autostart hooks you did not write. - Hunt it in Defender. A community KQL query for Microsoft Defender for Endpoint has been published that checks estates against Socket's live list of compromised versions.
- Enforce phishing-resistant MFA on publishing accounts. This started as one account takeover. Every downstream consequence followed from that single fact.
The Uncomfortable Structural Point
A single maintainer's personal account security became the effective security control for two billion monthly installs. That person did nothing wrong that has been established publicly, and the ecosystem gave them no mechanism to be less load-bearing. Your attack surface is not your dependency list — it is every maintainer upstream of it, and every device those maintainers log in from.
You cannot audit that. You can only shorten credential lifetimes, so that a stolen token is a dead end rather than a foothold, and pre-decide what you do on the day this happens — because it is now happening more than once a year.
Confirmed by Socket and Aikido on 4 August 2026, and still active — Socket reports new malicious variants appearing in real time and is maintaining the authoritative package list. Figures here are as reported on that date and may be revised. We have not seen a statement from npm or the maintainer at the time of writing. One number worth holding onto: Socket detected affected artifacts an average of five minutes and eighteen seconds after publication — and it still reached this many packages. Detection speed is not the constraint; install speed is.
Socket's research report, with IoCs → · Aikido's technical write-up → · Wiz on the earlier Shai-Hulud wave → · CISA alert on the npm ecosystem compromise →
PlayCISO's War Room runs exactly this scenario: a transitive dependency you never chose, a credential rotation with an ordering problem, and a disclosure clock that started before you knew. Play a live incident free →
Ready to practise the decisions these articles describe?
Run a free War Room →