Supply Chain npm Active Exploit

A worm walked into keyv wearing a valid provenance seal

Aug 7, 2026·11 min read·Chris Boker, Founder, CVEasy AI
A left column of ten stacked green npm package tiles labeled by shape for keyv, cacheable, flat-cache, and the rest, a gold Sigstore provenance seal pressed onto the top tile with a mint check mark still valid, a rust preinstall hook arrow curving out of the tile into a large black Bun runtime gear in the middle wrapped by a mint credential harvesting loop that lifts small gold key tokens up out of a right side surface stack of CI runner, developer laptop, and Kubernetes secret boxes, and a rust self-propagation ribbon exiting the top of the gear and curving back down into more npm tiles at the far right marked with dashed outlines to imply the worm is not finished

On Tuesday, August 4, 2026, at 09:02 UTC, an attacker holding Jared Wray's GitHub credentials pushed a commit straight to the main branch of keyv. Two minutes later, persistence files landed in .vscode and .claude. At 09:23 a stray test file that would have given the game away was quietly deleted, and at 09:35 the first trojanized keyv version hit the npm registry. The last poisoned release went out at 10:28 UTC. In roughly an hour, ten packages anchoring Node.js caching had shipped a self-propagating credential-stealing worm through a maintainer account whose Sigstore provenance signatures still validated on every mirror that checked them (Datadog Security Labs).

Datadog and Chainguard call this Mini Shai-Hulud, a follow-on to the earlier wave that hit TanStack, Mistral AI, UiPath, and OpenSearch packages. The compromised set is keyv, cacheable, cacheable-request, cache-manager, @cacheable/utils, @cacheable/memory, @cacheable/node-cache, flat-cache, file-entry-cache, and ecto, with over two billion combined monthly installs per Armorcode's telemetry and Phoenix Security. If any of your CI runners or laptops ran a fresh npm install during the Aug 4 window, treat that host as compromised.

What actually happened on August 4

The compromise was not clever. The payload was. According to Datadog's reversing, the attacker rewrote the scripts block of each package.json so its only entry was {"preinstall":"node setup.mjs"}, then published trojanized versions including keyv@6.0.0, cacheable@2.5.1, flat-cache@6.1.24, file-entry-cache@11.1.6, and cacheable-request@13.0.20, with the full poisoned set catalogued in Datadog's IOC list. The preinstall hook is npm's own lifecycle contract: it runs before any application code, before any user prompt, before transitive resolution completes.

The preinstall hook that runs Bun before your code does

The 11 KB setup.mjs loader is a lightly obfuscated Node script whose only job is to bootstrap a second stage under a different runtime. It shells out to bun --version; if Bun is absent it fetches Bun v1.3.13 from the official GitHub release archive, unpacks it with system unzip (no signature check, no size limit), and runs the next stage. The second stage, math_init.js in Datadog's write-up, ships a 4,613 entry Base91 string table and 73 alphabet permutations to slow anyone reading it. Outside CI it respawns detached, holds a lock at tmp.dpkg_14527.lock, and sets _NODE_RUNTIME_INIT=1 as a recursion guard. Bun was picked because it sidesteps Node hardening and brings its own TLS stack, so the payload runs even in a sandbox that only whitelisted node.

Where the credentials were kept and how the worm found them

The stealer targets what a build environment actually holds. Datadog counted 290 Linux, 129 macOS, and 50 Windows file patterns covering SSH keys, shell history, npm and Git config, cloud config, cryptocurrency wallets, and /etc/shadow. AWS credentials come through the standard chain (web identity, ECS metadata, EC2 IMDSv2), followed by 17 hardcoded commercial regions against Secrets Manager and Systems Manager. Kubernetes gets the in-cluster API and Base64 decoded Secrets. On GitHub Actions Linux runners it pipes Python into sudo python3 to read Runner.Worker memory directly, lifting secrets that never touched a file. With a workflow-write token it takes a quieter path: temporary branch, injected workflow emitting ${{ toJSON(secrets) }}, artifact upload and download, deletion of both branch and run. The audit log shows a branch that briefly existed. That is all.

The whole build environment was in scope. If any developer laptop or CI runner ran a fresh install against these packages during the Aug 4 window, assume npm tokens, GitHub PATs, AWS keys, Vault tokens, Kubernetes service account tokens, and every env var in that process are attacker-known. Rotation is mandatory, not conditional on egress log evidence.

How provenance became a signature the attacker could get

npm's provenance uses Sigstore: an ephemeral P-256 key, a short-lived Fulcio certificate binding that key to who and where the build came from, a Rekor transparency log entry, and an OIDC publish token. The attestation says a specific commit in a specific repository, running a specific workflow, produced this tarball. It does not say the commit is trustworthy. Mini Shai-Hulud includes a branch that only fires when GITHUB_REPOSITORY contains /opensearch-js and the workflow is release-drafter.yml; on that path it walks the whole Sigstore ceremony correctly. The attestation is genuine. It faithfully binds an attacker-controlled source tree and an attacker-controlled build to the tarball, and every provenance client accepts it because that is exactly what the standard promises.

The self-propagation loop: writable tokens, IDE hooks, and one Ethereum contract

The worm keeps going in two directions. Toward npm, it queries the tokens API for anything marked bypass_2fa: true with package-write permissions, drops the second stage plus setup.mjs into the target tarball, rewrites scripts, bumps the patch version, and republishes with rejectUnauthorized: false. Toward developer machines, it walks up to 50 branches per repository and writes .vscode/tasks.json, .vscode/setup.mjs, .claude/setup.mjs, .claude/settings.json, and .claude/math_init.js. The VS Code task fires on folder open; the Claude Code SessionStart hook fires on session start. As Cycode noted, no install is required on the developer's part.

C2 runs through an Ethereum smart contract at 0xE1f2395ee43e45A1556EC6438a88c31B83493103 called with selector 0x53ed5143. The malware iterates 73 public RPC endpoints, treats HTTP 400 and 404 as healthy, and decodes the return value to a rotating domain list. Exfil is AES-256-GCM under a 4,096 bit RSA-OAEP key; a 200 response with an executable body runs as eval(responseBody.code) unsandboxed. Fallback is a GitHub commit search for the marker thebeautifulmarchoftime; last resort is a public repo named "Shai-Hulud: Here We Go Again" holding 30 MiB Base64 shards.

Why a normal scanner mis-ranks this even now

There is no CVE identifier for this campaign; registries pulled the malicious versions once found, and SCA feeds catch up hours or days later. A scanner on your lockfile will not raise anything today if package-lock.json was already pinned to a clean release before Aug 4, even though your CI could have run a fresh npm ci during the poisoned window. A CVSS first queue has nothing to sort by. What matters is whether your runner touched any of the ten packages between 09:35 and 10:28 UTC on Aug 4, whether that runner had credential material in scope, and whether any token still in circulation was one the worm could have exfiltrated. None of that is a CVSS number.

TRIS on this event: the layers that push it to ACT

TRIS is the Threat and Risk Intelligence Scoring engine inside CVEasy AI. It scores an event, not a CVE, and its layers exist so the same finding lands in different priority bands depending on your environment.

Exploitation status. Confirmed active exploitation in the wild. Datadog, Aikido, and Cycode observed live self-propagation to more than 440 downstream packages within the first hour. Top band on this layer.

Blast radius. A dependency tree that transitively pulls keyv or cacheable is a fair chunk of the Node.js code shipped in the last three years, over two billion combined monthly installs per Chainguard.

Real exposure. Where the same finding scores differently by box. A build runner with production AWS write access, a workflow-write GitHub PAT, and a Vault token in the environment sits at ACT. A sandboxed reproduction container with no outbound egress and no ambient credentials scores far lower. TRIS does not rank the CVE; it ranks your instance of the CVE against the inventory it already has on your hardware.

Persistence exposure. If your developers use VS Code or Claude Code and pulled these packages, TRIS flags the IDE persistence layer as its own finding. Cleaning node_modules does not remove .vscode/setup.mjs or .claude/settings.json. The next folder open re-detonates the loader.

What to patch, what to rotate, what to hunt

Rotate first, then investigate. The malware ran under the identity of every process that installed a poisoned tarball. Any secret those processes could read is compromised, whether or not you find exfil in your egress logs. Rotate before you audit; the audit is slower than the worm's second hop.

1. Pin to a clean release. Pin every affected package to the last known good version documented in the Datadog and Phoenix Security advisories. Drop caret and tilde ranges on these packages until the namespaces are re-audited.

2. Rotate every credential in scope of an affected install. npm publish tokens, GitHub PATs, AWS keys, GCP service account keys, Azure client secrets, Vault tokens, Kubernetes service account tokens, SSH keys, and any secret written into a CI env var. Do not filter by exfiltration evidence.

3. Hunt IDE persistence and runtime indicators. Grep every developer repository and CI checkout for .vscode/setup.mjs, .claude/setup.mjs, .claude/math_init.js, .claude/settings.json, and any .vscode/tasks.json calling node .claude/setup.mjs. Alert on env var _NODE_RUNTIME_INIT=1, lock file tmp.dpkg_14527.lock, outbound to awqhnjewqjkl[.]icu, pypi-get[.]com, js-mirror[.]com, npm-cache[.]com, and any sudo python3 reading Runner.Worker memory.

4. Audit your own npm namespaces. If a publish token you own was ever reachable from a compromised process, review the publish history for every package it can write to. Yours may already be the worm's next hop.

How CVEasy AI surfaces a worm without a CVE

CVEasy AI is the number one local-first CTEM platform. It ingests supply chain compromise feeds and researcher advisories from Datadog, Chainguard, Aikido, Cycode, and Phoenix Security alongside NVD, scores each confirmed event against your inventory on your own hardware, and answers the concrete questions your morning needs: which runners installed these packages during the Aug 4 window, which credentials were reachable, and which of your own npm namespaces sit downstream of a token the worm could have taken. Your package inventory, runner logs, and credential map never leave your environment, which is the whole point when the finding is that another vendor's cloud read your secrets last week.

Sources: Datadog Security Labs, Chainguard, Phoenix Security, Armorcode, Cycode, Aikido Security, NHS Digital Cyber Alert CC-4781