A worm walked into npm through the front door: valid provenance, SLSA attestation, signed by the project's own build workflows. Then it stopped needing you to install anything. Opening the folder is enough.

No npm install. No new dependency. No CVE.

Just a configuration file, checked into the repository, that your editor obeys without asking.


Thirty-two minutes and twenty-three seconds

keyv is a caching library. cacheable, flat-cache and cache-manager are its neighbors: the kind of dependency nobody picks and everybody has, dragged in three levels down by something else. Of the eleven that fell, keyv alone carries 604 million installs a month.

On August 4, 2026, at 09:02:37 UTC, an unsigned commit landed straight on keyv's main branch. At 09:04, a verified commit — signed, green check and all — added the Claude and VS Code execution hooks. At 09:23, another removed the preinstall test. At 09:35:00.763, keyv@6.0.0 shipped to npm through trusted publishing over OIDC, with a valid SLSA attestation.

Thirty-two minutes and twenty-three seconds from first commit to signed package in the registry. Nobody reviewed anything: the changes went straight to main and the release followed them out.

Eleven seed packages in the first hour. Then the worm let itself loose: 433 more packages and 2,201 versions by 13:20. At the 18:10 cut, 444 and 2,212, with more than two billion installs a month.


What happens when it finds a token

The entry vector is a legitimate npm feature: a preinstall, which runs arbitrary commands before a package is installed. What hangs off it is not legitimate at all.

setup.mjs is a loader that pulls the legitimate Bun v1.3.13 binary from the official GitHub downloads — flawless traffic, flawless domain — to run the second stage underneath it: 728 KB of JavaScript obfuscated with control-flow flattening and Base91-encoded strings.

That stage sweeps the disk: 290 patterns on Linux, 129 on macOS, 50 on Windows against SSH, Git, npm, Docker, Kubernetes and cloud material. And it hunts eleven files earlier waves never wanted: the credential files of Claude, Codex, Cursor, OpenAI, Gemini. The haul is no longer just your cloud. It's your assistant.

The fact that defines this case arrives when the payload finds a ghs_ token. Aikido puts it plainly: "It adds malicious hooks to .claude/settings.json and .vscode/tasks.json so that the payload executes automatically the next time any developer opens the repository in VS Code or starts a Claude Code session inside it, with no npm install required."

The commits go out authored as claude, with the message chore: update config.

And the two files are wired into each other. Chainguard spells it out: the agent's SessionStart calls .vscode/setup.mjs, and the VS Code task — labeled "Environment Setup", with runOn: folderOpen — calls .claude/setup.mjs. Cycode warns what that means at cleanup time: they are "cross-wired, each invoking the other's setup.mjs, so cleaning only one leaves a live path". Of the three triggers, two never touch node_modules.

The C2 doesn't live where you would look for it either. It lives on Ethereum: the worm queries a contract with an eth_call, rotating through dozens of public RPC servers until one answers, and gets back whichever exfiltration domain is currently in force. On August 4 a single transaction moved it to a freshly registered one. There is no server to take down. You would have to talk Ethereum out of it. And in case you rotate your keys, a dead man's switch stays behind: a monitor installed as a user service that runs an attacker payload the moment the stolen token is revoked.


The file no scanner reads

In March we covered how a pip install of LiteLLM executed code without anyone writing import litellm: a .pth file merely existing in the environment was enough. Same move, one floor up. It used to be the Python interpreter starting things nobody had classified as code. Now it's the agent.

The day before the attack, Datadog had published the inventory: a project can drive code execution inside an agent "through hooks, skills, MCP servers, editor tasks, development-container settings, environment variables, runtime startup files, and ordinary executables." Of that list, MCP servers already have a campaign of their own. ChainDrop took two. And the precedent was already catalogued as MAL-2026-3648: three npm packages installing SessionStart hooks.

There's the blind spot. A dependency scanner reads package.json and package-lock.json, walks node_modules. It does not read .claude/settings.json. It does not read .vscode/tasks.json. Nobody put them on the list of executable things because they end in .json, and a .json is data. Until somebody runs it.

None of this happens without a click, and the honest version is worse than the invisible-attack fantasy. The click is right there: the folder trust dialog. Zero Hunt says it better than anyone — it's "the workspace-trust prompt they've long since learned to click through". What you approve there doesn't say "run this command"; it says "I trust this folder." Accept it once and the project's hooks run at session start without asking again. How many times did you click that today?

There was a window when not even the click was needed: CVE-2025-59536 described SessionStart hooks firing before the trust dialog rendered, and Anthropic fixed it. ChainDrop did not use that path. It came in through the legitimate one, the post-click one, the one that works exactly as documented. A hole gets patched. This isn't a hole.


The signature was valid

On September 22, 2025, after the original Shai-Hulud, GitHub published its plan for a more secure npm: mandatory 2FA, granular tokens with a seven-day life, trusted publishing. The recommended path. The same one ChainDrop used, ten months and thirteen days later, to sign the worm.

StepSecurity condenses it into two sentences that belong engraved over the registry door: "Provenance proves which commit was built. It cannot prove the commit was authorized." Snyk explains the mechanism: the malicious source was already sitting in the tagged repository state, so the legitimate workflow built and attested the malicious artifact. What does a correctly issued signature guarantee about a commit nobody authorized?

There's no CVE either. Phoenix says it whole: "There is no CVE for this incident, and provenance signatures on the poisoned releases were valid, so neither a CVE feed nor an SLSA badge would have flagged it."

Socket flagged keyv@6.0.0 roughly six minutes later. npm started unpublishing at 10:39. At 11:16 — one hour and forty-one minutes after publication — Snyk's registry snapshot still found eight malicious releases on the latest tag. Caught in six minutes. Served for a hundred and one.

The cleanest measure of the blast radius came from reading somebody else's CI: StepSecurity scanned around 44,000 public workflow runs over eight hours and got fifteen hits. Five were its own detonation repository. The other ten "were all from a single real project, backstage/backstage": a CNCF project pulling fresh dependencies outside the lockfile. PAILA in its own CI.

On August 6, Singapore issued advisory AD-2026-009: remove the affected versions, treat the systems as compromised, rotate credentials. Seven actions. Not one names .claude/settings.json, or .vscode/tasks.json, or coding agents at all. The official document describes an npm attack. The attack had already stopped being only an npm attack.


Attribution

Perpetrator: unnamed. Unit 42 is explicit: ChainDrop belongs to the Shai-Hulud code lineage, but "we cannot yet say whether it's operated by the group known as TeamPCP, or by another group adapting the published toolkit for their own purposes." The crew we mapped in March is a suspect, not a defendant. The only firm thing is the preparation: the C2 contract went live seventy-one days earlier, deployed from the same wallet that later moved the domain. And the maintainer is not a suspect, but the victim of an account takeover.

Accomplices: preinstall, which npm runs without asking. The trust click the industry trained everyone to give unread. And a detection rule that would have been enough: alert when a package adds or changes a preinstall, postinstall or prepare script between versions. Phoenix claims that "The lifecycle-script delta rule alone would have caught every Shai-Hulud wave to date." One rule. Every wave.

Systemic failure: we asked the ecosystem for cryptographic proof of origin and it gave us one, working exactly as designed, on top of a stolen account. Meanwhile we added to every repository a file that tells a machine which commands to run on open, and put it on no list at all. The supply chain stopped ending at the package: it ends at the configuration of the assistant you installed to review suspicious code.


We spent years learning to verify where the code we install comes from. ChainDrop forged none of those signatures: every one was genuine, every one verified. It just waited for you to open the folder.

How many other things, in the repository you just cloned, are waiting for your editor to ask them what to do?