
Published August 4, 2026, within hours of first disclosure. This is an active incident, and the numbers below are a snapshot, not a final count.
keyv and cacheable pushed a credential stealer directly to main and cut releases across the whole package family.preinstall hook that downloads the Bun runtime and runs a 728 KB obfuscated stealer targeting .npmrc tokens, GitHub CLI tokens, AWS credentials, Vault tokens, Kubernetes configs, and crypto wallets.keyv@6.0.0. The releases that install silently are cacheable-request@13.0.20, cache-manager@7.2.10, and @cacheable/utils@2.5.1, all patch bumps inside live caret ranges.An attacker took over the GitHub account of the maintainer who owns keyv, cacheable, flat-cache, and file-entry-cache, then used that access to publish malicious versions across the entire family. Wiz Research puts the start of visible activity at around 09:00 UTC, when the attacker first planted IDE persistence payloads in the keyv repository, and then published a new keyv release carrying the stealer.
The publishing method matters more than the initial access. Rather than stealing an npm token and pushing tarballs from a laptop, the attacker pushed malicious files straight to the main branch and triggered a normal release. The release workflow did what it always does. It built the packages, requested an OIDC token, and published to npm with signed provenance.
So the tarballs on the registry right now are cryptographically genuine. They really were built by that repository, in that workflow, on that commit. The attestation is correct. The code is malware.
Wiz attributes the payload to the “Mini” Shai-Hulud family, with similarities to the TeamPCP and @antv campaigns from earlier this year. No confirmed attribution yet, and given how much of the Shai-Hulud toolchain is now public, copycat activity is likely.
These are the versions with published numbers as of this writing. Treat any release from this family published on August 4, 2026 as suspect until you have verified the tarball contents yourself.
| Package | Malicious version | Install volume |
|---|---|---|
keyv | 6.0.0 | ~127M weekly |
cacheable-request | 13.0.20 | ~133M monthly |
cache-manager | 7.2.10 | ~16M monthly |
@cacheable/utils | 2.5.1 | ~34M monthly |
flat-cache | see note | ~565M monthly |
file-entry-cache | see note | ~557M monthly |
cacheable | see note | ~29M monthly |
@cacheable/memory | see note | ~28M monthly |
@cacheable/node-cache | see note | ~5.9M monthly |
Every writeup on this incident leads with keyv and its 127 million weekly downloads. We installed the affected chains and checked which malicious versions the declared ranges actually accept. The answer is not the one the headline implies.
$ npm i eslint got cache-manager && npm ls keyv
eslint@10.8.0
└─ file-entry-cache@8.0.0 → flat-cache@4.0.1 → keyv@4.5.4
got@15.1.0
└─ cacheable-request@13.0.19 (got declares ^13.0.18) → keyv@5.6.0
cache-manager@7.2.9
└─ @cacheable/utils@2.5.0 (declares ^2.5.0) → keyv@5.6.0
Now run the ranges against the malicious versions:
| Consumer and declared range | Malicious version | Resolves? |
|---|---|---|
| got → cacheable-request ^13.0.18 | 13.0.20 | pulls it |
| cache-manager top level ^7.2.9 | 7.2.10 | pulls it |
| cache-manager → @cacheable/utils ^2.5.0 | 2.5.1 | pulls it |
| flat-cache → keyv ^4.5.4 | 6.0.0 | blocked |
| got → keyv ^5.6.0 | 6.0.0 | blocked |
| cacheable-request → keyv ^5.6.0 | 6.0.0 | blocked |
keyv@6.0.0 cannot reach you transitively. Nothing in the tree declares ^6, so a caret range on v4 or v5 refuses it. The only people who pulled malicious keyv are those who ran npm i keyv@latest or pinned to 6.
The versions that install silently are cacheable-request@13.0.20, cache-manager@7.2.10, and @cacheable/utils@2.5.1. All three are patch bumps sitting inside live caret ranges. got@15.1.0 declares ^13.0.18, which happily accepts 13.0.20.
Worth noting on the keyv release itself: the keyv README states v5 is in maintenance mode with no new majors planned. A surprise 6.0.0 from a project that publicly stopped shipping majors is an anomaly a human reviewer catches in about four seconds. It’s also, ironically, why that particular release was the least dangerous one in the set.
If you were treating semver ranges as a safety boundary, this is where the assumption breaks. Not because ranges failed, but because they protected you from the package everyone is talking about and did nothing about the three nobody is.
Because almost nobody installs these packages on purpose. They’re plumbing.
flat-cache and file-entry-cache are how ESLint caches lint results between runs. If your repository has an .eslintcache file, those two are in your tree. cacheable-request sits under got, which sits under thousands of HTTP clients and SDKs. keyv sits under cacheable-request. cache-manager shows up under NestJS caching setups.
Run this and see for yourself:
npm ls keyv cacheable-request flat-cache file-entry-cache cache-manager
Most teams get a result. Very few of them have ever heard of the maintainer.
That depth is the whole point of the target selection. A compromise at the framework layer gets noticed in minutes because thousands of developers are watching the repo. A compromise four levels down in a lint cache utility gets noticed when a vendor’s telemetry catches it. Nobody is reading changelogs for file-entry-cache.
This is the same structural problem we wrote about after the TanStack npm supply chain attack in May and the axios compromise before it. The difference is depth. TanStack hit packages developers chose. This one hit packages developers inherited.
Two files land in every compromised package, plus one line in package.json:
"scripts": {
"preinstall": "node setup.mjs"
}
preinstall runs before installation completes. There’s no opportunity to inspect anything first. If you ran npm install against an affected version, setup.mjs executed.
setup.mjs is a small obfuscated dropper with exactly one job: silently fetch the Bun runtime from github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/ and use it to execute the real payload.
execFileSync(<bun binary>, ['<script_dir>/Math_Symbol.js'], {
stdio: 'inherit',
cwd: <script_dir>
})
Downloading Bun is a deliberate evasion choice. The traffic goes to github.com, which every corporate proxy allows and every CI runner already talks to. Endpoint tooling watching for curl to an unknown host sees nothing. And running the payload under Bun rather than Node sidesteps Node-specific hooks and monitoring.
Math_Symbol.js is the actual stealer: 728 KB of obfuscated JavaScript, dropped as node_modules/keyv/Math_Symbol.js.
Everything a developer machine or CI runner keeps lying around. The payload runs a set of extractors, each aimed at a specific secret store:
.npmrc~/.aws/credentialsVAULT_TOKEN, with an HTTP fallback if the variable isn’t setWiz adds that the malware harvests secrets from CI/CD environments, fingerprints build runners, and enumerates cloud environments to widen access from whatever it finds.
Two details are worth calling out because they change how you should respond.
First, stolen tokens get validated before exfiltration. GitHub tokens are checked against the live API to confirm they’re active. This isn’t a spray-and-pray dump, it’s a filtered set of working credentials, which means whoever receives it already knows what still opens doors.
Second, the command and control has changed. Alongside a conventional exfiltration domain (npm-cache[.]com) and the Shai-Hulud pattern of committing stolen secrets to public GitHub repos, this variant uses Ethereum RPC endpoints as part of its C2 channel. Three of them: eth-mainnet.nodereal[.]io, go.getblock[.]io, and eth.llamarpc[.]com.
Blockchain RPC as C2 is a real escalation for defenders. You cannot sinkhole it, you cannot take down the infrastructure, and the traffic looks like any other web3 API call. If your egress policy allows public Ethereum RPC endpoints, and most do by default, this channel works.
The exfiltration repos carry the description Shai-Hulud: Here We Go Again, and initial commits include the string IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients.
That’s a bluff aimed at GitHub’s abuse team, not a technical claim.
Because provenance answers “where did this build come from” and never claimed to answer “is this code safe.”
An npm provenance attestation binds a tarball to a repository, a commit, and a workflow run, signed via Sigstore. That’s genuinely useful against token theft, where an attacker publishes from a machine that isn’t your CI. It does nothing when the attacker has write access to the repository, because then the malicious commit is the source of truth and the signature over it is accurate.
Here’s the uncomfortable comparison:
| Metric | axios (Mar 2026) | TanStack (May 2026) | keyv / cacheable (Aug 2026) |
|---|---|---|---|
| Initial access | maintainer account | CI workflow abuse via pull_request_target | maintainer GitHub account |
| Publish path | stolen npm token | OIDC token extracted from runner memory | normal release workflow on main |
| Valid provenance? | no | yes | yes |
| Execution stage | install script | install script | preinstall |
| C2 channel | HTTP | HTTP | HTTP plus Ethereum RPC |
Three compromises, three different initial-access techniques, and the trend line on provenance is going the wrong way. A green provenance badge tells you the build was reproducible. It cannot tell you the maintainer’s session cookie wasn’t stolen an hour earlier.
If your software composition analysis gate treats “has valid provenance” as a pass condition, it just passed this.
Work in this order. The sequence matters because rotating credentials from an infected machine hands the attacker your new tokens.
1. Find out whether you’re exposed.
npm ls keyv cacheable-request flat-cache file-entry-cache cache-manager cacheable
grep -rn "Math_Symbol" node_modules/ 2>/dev/null
ls -d /tmp/bun-dl-* 2>/dev/null
Also check your CI logs for preinstall output and any outbound requests to the IOC domains below.
2. Assume the machine is compromised, and treat it that way.
If a poisoned version installed, the host had arbitrary code execution as your user. Removing the package does not undo that. Rebuild developer workstations and CI runners rather than cleaning them. This is the step teams skip, and it’s the one that determines whether you’re still compromised next week.
3. Rotate from a clean machine, in dependency order.
npm tokens first, then GitHub PATs and OIDC trust relationships, then AWS keys and instance roles, then Vault tokens, then Kubernetes service account tokens, then SSH keys. Rotating AWS before GitHub is pointless if the attacker still holds a GitHub token that can read your Actions secrets.
One caution on revocation. The Mini Shai-Hulud variant behind the TanStack attack included a destructive daemon that wiped the user’s home directory when it detected token revocation. There’s no confirmation of that behavior in this variant yet, and absence of evidence this early means nothing. Rotate from a separate machine and treat the infected host as evidence, not as a workstation.
4. Hunt for what the attacker did with the credentials.
Review CloudTrail, GitHub audit logs, and npm publish history for the window starting 09:00 UTC on August 4. Look for unfamiliar public repositories under your org, new self-hosted Actions runners, workflow file additions you didn’t author, and package publishes nobody remembers making.
5. Check whether you became a spreader.
If a developer with publish rights was infected, your own packages may already be carrying the payload. That’s how @ornikar/* and @qlik/* ended up on the affected list. Diff your latest published tarballs against your source tree.
Domains
npm-cache[.]com (exfiltration, 104.21.35[.]216, Cloudflare)eth-mainnet.nodereal[.]io (Ethereum RPC C2, 35.175.164[.]77)go.getblock[.]io (Ethereum RPC C2, 185.44.207[.]215)eth.llamarpc[.]com (Ethereum RPC C2, 172.67.167[.]200, Cloudflare)File artifacts
/tmp/bun-dl-*/node_modules/keyv/Math_Symbol.jssetup.mjs alongside a preinstall hook in any cache-family packageStrings
Shai-Hulud: Here We Go Again (exfiltration repo description)IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClientsThere will be a next one. Since September 2025, the interval between major npm compromises has been measured in weeks, and the toolchain is public now. Plan for recurrence, not prevention.
Turn off install scripts. npm config set ignore-scripts true breaks a small number of packages with native builds, and it would have stopped this attack cold. Allowlist the handful of packages that actually need lifecycle scripts.
Use npm ci, never npm install, in CI. npm ci installs exactly what the lockfile pins. npm install resolves ranges, which is how cacheable-request@13.0.20 reached machines whose owners never asked for it.
Add a minimum release age. pnpm 11 ships a one-day cooldown by default and blocks exotic subdependencies. A 24-hour delay would have made this incident a non-event for anyone using it, because detection happened within hours. This is the single highest-leverage control available today and it costs you one day of freshness.
Stop treating provenance as an integrity check. It’s a build-origin attestation. Keep verifying it, but pair it with behavioral analysis of what the package does at install time.
Know your dependency tree before you need to. The teams that answered “are we affected” quickly today had an SBOM they could query. The ones that didn’t spent the morning running npm ls across forty repositories. If you don’t have that inventory, start with an SBOM.
Treat developer machines as production. Every credential this payload stole was sitting in a home directory or an environment variable. The workstation is where cloud access actually lives, and most vulnerability management programs still don’t look there. ASPM closes that gap by connecting what’s in your code to what’s exposed in your cloud, and open source security scanning in the dev workflow catches malicious dependencies before they reach a laptop.
Strobes tracks npm and PyPI supply chain incidents in Strobes VI, with affected package versions, IOCs, and linked advisories per incident.
Probably yes. keyv, flat-cache, file-entry-cache, and cacheable-request are almost always transitive dependencies. ESLint pulls in the cache packages, and got pulls in cacheable-request. Run npm ls against the package names rather than checking your package.json.
Only if you install from it. npm ci respects the lockfile exactly. npm install can resolve a new patch version inside an existing caret range, which is precisely how cacheable-request@13.0.20 and cache-manager@7.2.10 spread. If your CI runs npm install, the lockfile did not protect you.
No. Provenance proves a tarball was built by a specific repository and workflow. When the attacker controls that repository, the attestation is accurate and the code is still malicious. This is the second confirmed npm attack in three months to ship malware with valid attestations.
No. preinstall already ran arbitrary code as your user. Clear the package manager cache, then rebuild the host and rotate every credential it could reach. Reinstalling clean packages removes the delivery mechanism, not the compromise.
Revoke it, but do it from a different machine. A related Mini Shai-Hulud variant destroyed the user’s home directory when it detected revocation. That behavior isn’t confirmed here, but the infected host is not where you want to find out.
Related, not identical. Wiz assesses this payload as a descendant of the “Mini” Shai-Hulud family, with overlap against the TeamPCP and @antv campaigns. The Ethereum RPC C2 channel is new to this variant.
The count is moving. Public tracking showed 868 packages across 1,381 versions as of 13:37 CEST on August 4, representing over 2 billion combined monthly installs. That includes worm propagation into unrelated namespaces, and it will be higher by the time you read this.