Keyv and Cacheable Chaindrop Worm artwork with the quote “When the worm comes, there is no escape. You ride or you die.” — Charlie Robbins

Keyv and Cacheable - Chaindrop Worm

TLDR

  • Keyv and Cacheable together do over 2 billion npm downloads a month and have about 41 million total dependencies.
  • I mistakenly left an old full-access GitHub token active on a machine that was infected.
  • On August 4, 2026, an attacker used it to push malicious code into Keyv, Cacheable, and Ecto.
  • Legitimate release workflows then published poisoned packages to npm, launching the first wave of a self-propagating, credential-stealing Shai-Hulud-family worm that StepSecurity named ChainDrop.
  • Provenance did not stop it. The real GitHub Actions and npm OIDC workflow built the poisoned source, so those packages carried valid provenance.
  • It spread beyond my projects to hundreds of package names. GitHub and npm stopped the propagation and removed the infected releases within about 7 hours. Known-clean versions were back on latest the same day, and the exploit was confirmed gone from every branch within 36 hours.
  • I have removed write-capable GitHub PATs and npm publishing tokens. I now develop in isolated, disposable environments with no secrets on the host, delay new packages for seven days, and require staged npm publishing plus artifact review before a release can go public. Those controls are being rolled out as a public Defense in Depth for Node.js baseline.

Leaving that credential active was my failure. I am sorry.

Table of Contents

The Mistake Was Mine

At the time of the attack, I had two GitHub personal access tokens. I created a read-only token within the last year. The other token was more than two years old and had full access.

I believed I had replaced the older full-access token with the read-only token. I had not. The old token remained active. Given that two-token inventory, I believe it was the credential used.

While we have not been able to be 100% sure how they got access to that token, I will confirm that my machine was infected and it had the token, my repos, and .env files on it.

Just to clarify, I was using passkeys and 2FA, moving releases to OIDC, and have been very careful on phishing scams around this. Even then, it happened.

To the people and teams who trusted Keyv, Cacheable, and the other packages I maintain: I am deeply sorry. I will do better.

Timeline

The unpublished GitHub git-access and API audit exports I reviewed are the primary source for the account activity below. I am not linking the raw files because they contain account and network metadata. I correlated them with the public commit and npm registry timelines published by StepSecurity and Socket. Entries marked “around” are approximate. All times are UTC on August 4, 2026.

  • 08:34:57 — The first API request associated with the attacker activity is a successful PAT-authenticated GET /user request.
  • 08:36:13 — The first Keyv git-access event labeled as a push appears from the same network.
  • 09:02:37 — The malicious Keyv release commit is timestamped at 09:02:37 and adds the install hook and payload files.
  • 09:04:30 — A second timestamped Keyv commit adds Claude Code session-start and VS Code folder-open execution hooks.
  • 09:18:49 — The first successful API write in the export creates a Keyv release.
  • 09:23:50 — A further timestamped Keyv commit removes the cover test associated with the earlier payload changes.
  • 09:25 — A sequence of Keyv release and tag deletion and recreation cycles begins in the API log and continues through 09:52.
  • 09:30–09:32 — Several @keyv/* adapters at version 6.0.0 publish before the executable hook is added to them. The cited public analysis found that these tarballs did not carry the executable payload.
  • 09:35:00 — The cited public registry analysis identifies [email protected] as the earliest confirmed carrier. It publishes through the legitimate GitHub Actions and npm OIDC workflow with valid provenance.
  • 09:38:13 — StepSecurity identifies @thiennq/[email protected] as the earliest second-wave package it observed outside my projects.
  • 10:06:57 and 10:07:01 — The logs record two Cacheable events labeled as pushes. A Cacheable release is created through the API at 10:07:17.
  • 10:09:44–10:14:41 — Following the Cacheable payload commit, nine confirmed Cacheable-family carrier versions publish in a five-minute burst.
  • 10:17:44 — The first public warning is opened in the Cacheable repository.
  • 10:26:51 and 10:26:54 — Two Ecto events labeled as pushes appear. An Ecto release is created through the API at 10:27:10.
  • 10:27:16–10:27:21 — Four Keyv issues are updated successfully through the API.
  • 10:28:01 — The cited public registry analysis records [email protected] as a carrier.
  • Around 10:39 — npm begins removing affected versions.
  • Around 11:15[email protected] is restored as the known-clean latest version.
  • 11:28 — The logs show another Keyv release and tag deletion and recreation cycle.
  • 11:35:21 — A failed Keyv job is successfully rerun through the API.
  • 11:46 — Another Keyv release and tag deletion and recreation cycle appears.
  • 11:46:45 — The final attacker-associated event labeled as a push appears.
  • 11:51:48 — A second failed Keyv job is successfully rerun through the API.
  • 12:00:19 — The final attacker-associated API activity authorizes and redirects a workflow job-log download.
  • By 18:10 — Public registry monitoring shows a known-clean version restored to npm's latest dist-tag for each of the eleven original package names.

During the confirmed attack window, the exports contain 607 PAT-authenticated API requests associated with the attacker activity. The git-access export contains 23 events labeled as pushes: 19 against Keyv, two against Cacheable, and two against Ecto. These are log events, not necessarily 23 distinct commits. Successful API operations created eight releases, deleted five Keyv releases and five Keyv tags, edited four Keyv issues, and reran two failed Keyv jobs.

Finally, I want to personally thank the engineers and support staff at npm and GitHub. They were able to stop the propagation and remove the infected releases within ~7 hours. Yes, we always want it to go faster but we need to realize the size and scale they deal with every day is insane. They are truly the heroes in this.

How the Worm Moved Through the Release Path

The malicious Keyv commit added the package lifecycle hook and payload files directly to main. At 09:35, the project's real GitHub Actions workflow used npm trusted publishing and OIDC to publish that source as [email protected]. It carried valid provenance because the legitimate workflow had faithfully built poisoned source.

That distinction matters. Provenance proved where and how the package was built. It did not prove that the source was safe or that the change was authorized.

The eleven confirmed first-wave carrier versions from my projects were:

The earlier @keyv/* adapter releases at version 6.0.0 did not contain the executable hook, according to the published analysis. The worm nevertheless spread beyond my projects to hundreds of package names and more than a thousand affected versions. Published counts varied as the investigation evolved.

The investigations from StepSecurity, Socket, Aikido, and SafeDep provide the deeper technical analysis. Socket also maintains a live list of affected packages and versions.

By the end of August 4, a known-clean version was back on npm's latest dist-tag for each of the eleven original package names. That does not undo an affected installation.

What We Did During the Incident

We immediately disconnected all of my corporate accounts and began rotating every secret that could have been exposed. As of publication, our investigation has found no evidence that those corporate accounts or systems were compromised.

During the outage, I purchased a new computer and started from scratch. Out of caution, I then wiped every other computer I used and rebuilt each one from a clean starting point. There is just too much risk in attempting to clean a machine and missing one thing.

We sent multiple messages to GitHub and npm support. After they had stopped the propagation from Keyv, Cacheable, and Ecto, they sent an account reset so I could gain access. After gaining access from a clean machine I removed all automated publishing out of precaution. Next, I started building a plan on how to clean the infected repos, audit all other repos, and move forward with a more secure way to work and handle these open source projects.

Within 36 hours, I had removed the exploit and worked with security experts to confirm that it was no longer present in any branch.

Development / Work Config Moving Forward

Given the targeted nature of these attacks, using a local machine for multiple projects and repositories creates too large a blast radius for me to accept.

I no longer develop directly on the host operating system. Each project runs in its own isolated, disposable environment with no secrets or .env files. I do not do shared mounts and also require Aikido Shim.

Checks requiring credentials run only in protected CI jobs after explicit maintainer approval, using least-privilege credentials. Forked or otherwise untrusted pull-request code must never receive secrets.

I have also configured pnpm to put explicit guardrails around which dependencies can be installed and which package scripts may run:

minimumReleaseAge: 10080
minimumReleaseAgeStrict: true
minimumReleaseAgeIgnoreMissingTime: false
blockExoticSubdeps: true
strictDepBuilds: true
dangerouslyAllowAllBuilds: false

These settings make packages ineligible for installation until seven days after publication and reject packages without verifiable publication dates. They also block transitive dependencies from non-registry sources and require lifecycle scripts to be explicitly approved before they can run. Any repository-specific exception must be narrow and reviewed in code.

The pnpm supply-chain security guide explains these controls, which reduce risk rather than eliminate it. With a compatible pnpm version, the seven-day delay would have outlasted these releases, and the unapproved preinstall hook would have stopped installation.

Defense in Depth Across My Open-Source Projects

Open source has always been my way to give back. That trust needs stronger release boundaries than the ones I had in place.

The lesson was not that one security tool failed. One compromised credential was able to cross too many trust boundaries: source, CI, and publication. I have turned the response into a public Defense in Depth for Node.js implementation manual. Its goal is to make a successful compromise require several independent failures and to limit the damage when any one layer fails.

I am rolling it out one scoped pull request at a time. This is an implementation in progress, not a claim that every repository already has every control. Each repository will track what is complete, in progress, and still missing in DEFENSE_IN_DEPTH.md. Its public SECURITY.md will describe only protections that are actually live. You can see some of the examples here:

The target state has several separate boundaries:

  • Development and dependency intake: Codespaces and Cursor Cloud Agents bootstrap Aikido Safe Chain from a pinned, checksum-verified installer before a frozen-lockfile install. pnpm adds the seven-day cooling period by default, blocks unreviewed lifecycle scripts and exotic transitive sources, and rejects a downgrade in publishing trust. Socket Firewall wraps package-manager installs in CI to screen packages at install time and block confirmed threats.
  • Source and CI: Repository rules are being separated into two boundaries. An owner-bypass-free baseline requires a pull request and named green checks, and blocks branch deletion and force-pushes. A second ruleset applies CODEOWNERS review to release, dependency, workflow, cloud-environment, and security-critical files. On a solo-maintained repository, the owner exception applies only to that review rule and only through a pull request—not to the baseline CI gate or a direct push. Where another trusted maintainer is available, that person reviews high-risk changes instead. Workflows use least-privilege permissions, full commit-SHA action pins, non-persisted checkout credentials, and no privileged pull_request_target execution of untrusted code. zizmor checks workflow changes, and runs from outside contributors require maintainer approval.
  • Independent security gates: Socket reviews every pull request that changes dependencies. Aikido scans every build, and its release scan must pass before the stage-publish job can run. A scanner finding is therefore a release condition, not only a dashboard notification.
  • Publication: CI packs the exact tarball and authenticates to npm with short-lived, stage-only OIDC credentials. It can stage a candidate but cannot make it public. Drydock reviews that same artifact, and a maintainer must promote it with npm 2FA. Releases are immutable: the reviewed tarball is never rebuilt or replaced, and any correction requires a new version. Long-lived npm publishing tokens are removed.
  • Repository and account authority: The default Actions token is read-only, tag creation is limited to repository administrators, secret scanning and push protection are enabled where available, and only approved classes of Actions can run. GitHub and npm accounts use phishing-resistant 2FA.

Passkeys and 2FA were already enabled before this incident. They protect account sign-in, but they did not reduce the authority of an existing full-access PAT. Since the incident, I have removed npm publishing tokens and write-capable PATs.

The only GitHub PAT I still use—for read-only testing and a dashboard—has been validated as read-only. I have a recurring access review scheduled every 90 days to verify that the inventory, permissions, need, and expiration of every remaining credential are still correct.

The lockdown audit is also being tightened. It will validate the effective branch and tag rules, exact bypass identities, actual CODEOWNERS coverage, mandatory check names and sources, and the exact set of permitted Actions. Applying the baseline must preserve stricter existing protections instead of replacing them with weaker settings. These controls and any bypass use will be checked again during the same 90-day review.

npm staged publishing adds a different boundary. With the trusted publisher restricted to staged publishing, CI can place a release candidate in npm's private staging area but cannot make that package public. A maintainer must inspect and approve it with 2FA. I am moving the trust relationship to stage-only publishing so the workflow's OIDC authorization cannot publish directly.

I am also using Drydock to review the exact artifact that would ship. It compares the candidate with the previous published version and calls attention to changes such as new lifecycle scripts, new files, network access, and process execution. In this incident, that release diff could have surfaced the new preinstall, setup.mjs, and Math_Symbol.js directly in front of the reviewer before approval. A review tool is not a guarantee, but it puts a human decision at the last useful checkpoint.

Jovi De Croock explains this boundary extremely well in Two Places to Stop a Bad Release. The principle is simple: review the artifact, not only the pull request or the build recipe.

The manual is a living baseline, not a guarantee. It will keep changing as the controls are tested and audited. The point is not to claim that compromise is impossible. It is to ensure that a stolen credential, malicious dependency, or compromised workflow encounters other boundaries before it can become a public release.

As always, feedback is more than welcome as I am not a security expert.

Special Thanks

I wanted to list some of the people who put time and effort into helping me. Being a maintainer is hard and this was a ton of work. Without these people it would have been a massive undertaking:

  • GitHub Support Team - While I don't know who you were directly I want to thank you for working the problem and getting the propagation under control. You truly are amazing.
  • npm Support Team - Like above, I cannot thank you enough and thanks so much for taking the infected releases down as fast as possible.
  • Ashish Kurmi & Varun Sharma @ Step Security - Thank you so much for reaching out and helping validate each of the repos with me. It is so helpful to have a second set of eyes.
  • Charlie Eriksen @ Aikido - Simply amazing the support you gave and that you personally contacted me to just help.
  • John Reed - Thanks for being so fast to respond and helping me understand what was going on.
  • Feross Aboukhadijeh @ Socket - Thanks for following up about this. You and your team have been so responsive and helpful.
  • Ahmad Nassri @ Socket - The way you responded and got me help shows just how amazing you and your team are.
  • Nick Hurley @ Socket - Thanks so much for getting on a video conference with me to simply explain to a "non security expert" how this worm works and allowing me to ask questions about what to do next.
  • Charlie Robbins - Thank you for all the support, reviewing things with me, going over and suggesting updates on my security approach. The hours that you spent on this are just remarkable and I owe you.

There were many others who helped broadcast, re-post, and such that I wouldn't be able to list all of you. Thanks for making me want to be part of this community. ❤️

Next
Today is an Amazing Day for Eric Branner and the Fons Team!