← Notes & write-ups

Integrity is not immunity: the keyv worm was properly signed, and that was the point

5 August 2026· 9 min read

At 09:35 UTC on 4 August 2026, a package that had been installed roughly 620 million times in the previous month published a new major version. It was built by the project's own GitHub Actions workflow. It carried genuine npm provenance. The signed attestation binding that release to that repository at that tag is still sitting in a public transparency log, still accurate and still verifiable. It was also a worm (Snyk, Datadog Security Labs).

The mechanics are quick to state. The first malicious commit landed at 09:02:37; keyv@6.0.0 went out 33 minutes later with a single added line, a preinstall hook. Public warnings appeared around 10:18. By then the payload was already republishing other people's packages with stolen tokens at roughly one package per second per namespace, and eight organisations fell inside a 34-minute burst. The final tally: 2,234 poisoned versions across 444 package names in 12 organisations, with more than two billion monthly installs behind them (SafeDep, Aikido). My previous write-ups were all about trust boundaries that turned out to be assumptions. This one is about the machinery we built to prove trust, and what that machinery does when the attacker is standing inside it.

Start with the part that should be uncomfortable for everyone who has ever recommended provenance, including me. Nothing here was forged. npm provenance asserts a narrow, precise claim: this artifact was built by that workflow, from that repository, at that commit. Every word of that was true. The attacker had the repository, so the CI faithfully built and attested exactly what the repository said to build, and the resulting record is genuine. SafeDep put the consequence more bluntly than any vendor blog usually does: do not read valid provenance on a future release as evidence that the release is clean, because the provenance on 6.0.0 was itself genuine. In principle we all knew a signature proves origin, not intent. In practice we shipped that distinction to millions of developers as a green check mark, and a check mark is read as safety.

Then look at where trusted publishing moved the credential. Its whole purpose is to eliminate long-lived human tokens by handing publish authority to CI, and CI derives its authority from the repository. So the design hands the keys to precisely the class of attacker who compromises a maintainer's push access, which is the class that showed up here. We did not remove the credential; we relocated it from a human who might have hesitated at an unexpected 2FA prompt to a machine that never asks and never sleeps. The worm collected the leftovers too: tokens carrying 2FA-bypass, plus secrets recovered from the Actions runner's own process memory, and, in a detail I keep re-reading, a workflow it wrote itself whose only job was to dump the entire secrets context into a log it could then read (Datadog).

Here is the reframing that changed how I read the whole incident. Epidemiologists do not describe an outbreak by asking whether a pathogen is authentic. They use a reproduction number: contact rate, multiplied by transmission probability, multiplied by infectious period. Our industry has spent roughly a decade on transmission probability: signing, 2FA, attestation, reproducible builds. That is the one term a pathogen can route around simply by being genuine. Meanwhile we maximised contact rate on purpose and called it developer experience: one credential that can republish an entire namespace, automation that fires without a human present, dependency graphs with enormous fan-in. And the infectious period is still bounded by human reaction time. We spent a decade driving the first term down, we drove the second one up on purpose and called it maturity, and we left the third to whoever happened to be awake. On 4 August the third term was 34 minutes, which was long enough for 2,234 poisoned versions.

Which leads somewhere nobody says out loud: a mature release pipeline is a better host than an amateur one. A maintainer who still types the publish command on a laptop and answers a 2FA prompt is, epidemiologically, a dead end: low contact rate, a human in the loop, no automatic forwarding. The projects that did everything we told them to do, meaning automated releases, CI-held credentials and whole-namespace tooling, are the ones that conduct. We have spent ten years turning a sparse network of hand-published packages into a dense one with automatic forwarding, and we measured our progress in signatures.

None of this is a first, either. The same worm family has now run three times: the original Shai-Hulud in September 2025 (Unit 42), a second wave that November which reached 492 packages with around 132 million monthly downloads (eSentire), and now this one, which researchers tie to the same lineage and which tags the repositories it infects with the line “Shai-Hulud: Here We Go Again” across roughly 1,300 of them (Expel, nulltap). Three outbreaks, the same fundamental design, each one arriving after another round of integrity improvements. A pathogen that keeps working against a population you have been hardening for a year is telling you which term you hardened.

The second propagation channel is the one that should worry defenders most, because no supply-chain control in existence looks at it. Alongside the registry path, the worm used stolen GitHub credentials to write files across up to 50 branches per repository, including a VS Code task configured to run when the folder is opened and a Claude Code hook configured to run when a session starts (Snyk). Nothing is published. There is no version, no lockfile entry, no digest to pin, no registry to scan, no SBOM line item. Execution begins when a human opens a folder or starts an agent, and the payload travels in-band with source, through git, into branches nobody reviewed. The timing is its own commentary: npm v12, announced a month earlier on 8 July 2026, disables dependency lifecycle scripts by default (GitHub Changelog). The ecosystem shipped the correct fix for the install channel, and within a month the worm was spreading through a channel npm does not govern: the editor and the agent.

Now the asymmetry I cannot stop thinking about. Two ledgers were load-bearing in this attack, and both are append-only. Ours: a transparency log entry that will forever state, correctly, that the malicious release is authentic keyv, built by keyv's own pipeline. Append-only means we cannot un-attest it; the most we can do is publish more records next to it. Theirs: the command-and-control domain list, stored in an Ethereum mainnet contract and read through roughly 75 public RPC endpoints. There is no registrar to notify, no host to suspend and no takedown surface at all (Upwind). Both sides now anchor durability in immutable public infrastructure. But notice which assets are still revocable: dist-tags, tokens, repositories, domains, credentials. Every one of them sits on our side of the line. And the worm's persistence includes a monitor watching the stolen GitHub token, which means our own incident response is an observable event for the attacker. We made trust permanent and defence perishable.

The same inversion shows up in its fallback exfiltration: if the C2 is unreachable, the payload creates a public repository in the victim's own GitHub account and commits the loot there, with the victim's token embedded in the commit message. Your account is the attacker's content delivery network. Three write-ups ago I learned that my own telemetry could be an input; this one says my own infrastructure can be the exfil path, and my own signature can be the passport.

What actually bounds an outbreak is the third term. There is exactly one control in npm's published roadmap that shortens the infectious period rather than the transmission probability: from January 2027, 2FA-bypass tokens will no longer be able to publish directly. They will only be able to stage a publish that a human must approve. That is, in my reading, the most epidemiologically significant line in the entire npm security programme, and it arrives last, after years of integrity work. If the reproduction-number framing is right, the ordering is backwards. Integrity reduces the chance that a hostile artifact is believed. Staged publication reduces how far a believed one can travel. Only the second one ends an epidemic.

Registry-anomaly detection does exist. Socket, Phylum and OpenSSF’s package-analysis all watch publishing behaviour. But they watch the artifact: they need the tarball, and they need to look inside it. What I have not found is anyone treating the provenance feed itself as the sensor, where the worm’s shape is visible without downloading anything at all. We already run a real-time, tamper-proof, ecosystem-wide feed of who published what, from which workflow, at what second. We call it a transparency log and we file it away like a receipt drawer. A worm's shape in that feed is unmistakable: dozens of packages, across unrelated namespaces, patch versions incrementing, inter-arrival times under a second, sharing a workflow-identity pattern. That is a shape, not a signature. It needs no payload, no reverse engineering, no vendor feed, and no cooperation from the attacker, who cannot publish without writing to it. Read provenance as epidemiology rather than as receipts, and the very infrastructure that certified this worm becomes the fastest way to see the next one, plausibly minutes after the first republish and hours before any blog post. Two smaller asks follow from the same logic: publish blast-radius metadata per package, so a consumer can prefer dependencies that cannot be mass-republished by one credential; and treat repository-local agent and editor configuration as executable content: trusted per diff, never per folder, and never auto-run when HEAD has moved since the last human look.

The arc of these notes has been a slow retreat from things I thought were boundaries. Localhost was a topology, not a wall. Defence was structural, not vigilant. My own logs were an input. Finding bugs stopped being expensive. This one is the least comfortable, because it takes away the artifact I would have used to reassure myself: a valid signature and a green provenance badge were not what failed the ecosystem. They were what carried the worm. The question I now write at the top of my notes is no longer can I verify this? It is: if this is authentic and hostile, how far does it get in thirty-four minutes?

Sources / Nguồn:
- Snyk — “Inside the keyv npm supply chain compromise”: snyk.io
- Datadog Security Labs — “Worm compromises hundreds of popular npm packages”: securitylabs.datadoghq.com
- SafeDep — 2,234 poisoned versions across 444 packages and 12 organisations: safedep.io
- Upwind — the Ethereum smart-contract C2 channel: upwind.io
- Aikido — keyv and friends, propagation and scope: aikido.dev
- GitHub Changelog — npm v12 install-time security and 2FA-bypass token deprecation (8 July 2026): github.blog
- The Hacker News — worm plants Claude Code and VS Code hooks: thehackernews.com
- Expel — “ChainDrop”: this wave placed in the Shai-Hulud worm lineage: expel.com
- Unit 42 — the original Shai-Hulud npm compromise (September 2025): unit42.paloaltonetworks.com