keyv npm Compromise: A Credential-Stealing Worm That Shipped With Valid Provenance
By pico picu · 3 Sept 2026 · 18 min read
Threat Actor: Unattributed (Shai-Hulud tradecraft assessed) · Target: npm — the keyv and cacheable namespaces and everything downstream of them
Source: snyk.io
Executive Summary
At 09:35 UTC on 4 August 2026, keyv@6.0.0 was published to npm. It carried a preinstall hook, a credential-stealing payload, and valid SLSA provenance issued through GitHub Actions OIDC trusted publishing.
That last part is the finding. The provenance was not forged, stolen or bypassed. It was correct. The attacker had compromised the maintainer account behind the keyv and cacheable namespaces, committed the payload into the repository, and then let the project's own legitimate release workflow build and attest it. Snyk's analysis puts it plainly: "the malicious source was present in the tagged repository state, so the legitimate workflow built and attested the malicious artifact."
Software supply chain guidance for the last five years has converged on provenance attestation as the answer to exactly this class of attack. This incident shows what that answer actually promises. Provenance proves an artefact came from a particular source at a particular commit through a particular workflow. It says nothing about whether that source was trustworthy. When the compromise happens upstream of the build, an honest pipeline produces an honest attestation of malicious code — and every downstream verifier passes it.
This is the same lesson as the Codecov compromise, escalated one layer. There, a valid TLS certificate and a validly signed vendor domain proved the bytes came from Codecov, while saying nothing about whether Codecov's build had been tampered with. Here, provenance proves the build was faithful, while saying nothing about whether the source was.
Eleven malicious releases went out across the maintainer's packages in two waves, roughly forty minutes apart. The payload downloaded a standalone Bun runtime, executed an obfuscated second stage, and harvested GitHub and npm tokens, cloud credentials, private keys, database connection strings, Vault tokens and Kubernetes service account tokens — and read GitHub Actions runner memory. It then used those stolen tokens, and the same OIDC trusted publishing mechanism, to republish trojanised versions of other packages. The wider worm reached 2,234 poisoned versions across 444 package names using credentials from twelve organisations.
The second notable element is where else it arranged to run. Beyond npm's preinstall hook, the commit added a SessionStart command to .claude/settings.json and a folderOpen task to .vscode/tasks.json. A developer who never installed the package — who merely opened the project folder in their editor, or started an AI coding session in it — executed the payload. These are configuration files that dependency scanners do not read, in a part of the repository nobody thinks of as executable.
Neither npm nor GitHub had published a statement in the days after disclosure, though npm rolled back latest tags on at least nine seed packages. The maintainer is a victim; the initial access route to their account has not been established.
Verification of Claims
Claim: Malicious versions of keyv and related packages were published on 4 August 2026.
→ Verified — Documented independently by Snyk, Socket, Wiz, Datadog, Aikido and Chainguard. Snyk records keyv@6.0.0 at 09:35 UTC; Socket places a first wave at 09:30–09:35 UTC and a second at 10:09–10:14 UTC.
Claim: The malicious keyv release carried valid SLSA provenance from GitHub Actions trusted publishing.
→ Verified — The npm registry identified GitHub Actions as the trusted publisher for keyv@6.0.0 with an accompanying attestation. Snyk's analysis concludes that "provenance can faithfully attest a build whose source or workflow context has already been compromised."
Claim: SLSA provenance was bypassed or defeated. → False — Nothing about the provenance mechanism failed. It performed exactly its specified function: it attested, truthfully, that a particular artefact was built by a particular workflow from a particular commit. All of that was true. The compromise was upstream of the build, in the source that workflow compiled. Describing this as a bypass misstates what provenance claims to establish and implies a defect where there is none — which matters, because the corrective action is different. There is no patch for this; there is a gap between what provenance proves and what people assumed it proved.
Claim: The payload executed without the developer installing or importing the package.
→ Verified — Two independent execution paths are documented. The preinstall lifecycle hook runs during npm install regardless of whether the package is imported. Separately, commit d8c850c7 added a SessionStart command in .claude/settings.json and a folderOpen task in .vscode/tasks.json, both of which execute on opening the project rather than on installing anything. Snyk: "A developer does not need to import keyv, start an application, or call a vulnerable API."
Claim: Roughly 450 million weekly downloads were affected.
→ Unverified — The figure describes the combined download volume of keyv, flat-cache and file-entry-cache, and Snyk states directly that "those numbers overlap heavily because the packages depend on one another" and represent ecosystem reach rather than confirmed compromised hosts. The malicious versions were available for a window measured in hours, not the month those download counts cover, and installs during that window that actually executed the payload were never quantified. The number measures the packages' importance, not the incident's blast radius.
Claim: The attack is the work of the Shai-Hulud operators. → Assessed, not confirmed — Socket records that "the tradecraft closely matches the techniques seen in the Shai-Hulud npm worm activity," citing credential harvesting, package enumeration and GitHub repository staging, while acknowledging that unrecovered identifying markers prevent definitive attribution. Chainguard frames the activity as part of a "Mini Shai-Hulud" campaign. No vendor asserts confirmed attribution and no government has issued one.
Claim: The maintainer's account was compromised. → Assessed, not confirmed — Socket states "the evidence indicates the maintainer account (jaredwray) was compromised" but does not establish how. The initial access vector remains unpublished. The maintainer is a victim of the incident, not a party to it.
Claim: npm or GitHub issued a public statement.
→ False — As of the reporting in the days following disclosure, neither had published a statement, although npm had begun rolling back latest tags on at least nine affected packages. The response that reached developers came from commercial security vendors.
Timeline
| Time (UTC) | Actor | Event | Source |
|---|---|---|---|
| Before 04 Aug | Unattributed | Maintainer account for the keyv/cacheable namespaces compromised; vector unpublished | Socket |
| 2026-08-04 | Attacker | Commit ee2681a9 adds the lifecycle hook and payload files to the repository |
Snyk |
| 2026-08-04 | Attacker | Commit f97eabcd removes the preinstall test, suggesting execution occurred in CI before publication |
Snyk |
| 2026-08-04 | Attacker | Commit d8c850c7 adds the .claude and .vscode execution paths |
Snyk |
| 2026-08-04 09:30–09:35 | Attacker | First wave; keyv@6.0.0 published at 09:35 with valid provenance |
Snyk / Socket |
| 2026-08-04 10:09–10:14 | Attacker | Second wave across the @cacheable/*, flat-cache, file-entry-cache and cache-manager packages |
Socket |
| 2026-08-04 | Worm | Propagates using stolen tokens and OIDC trusted publishing; 2,234 poisoned versions across 444 package names, credentials from twelve organisations | Datadog / safedep |
| 2026-08-04 11:16 | — | Snapshot: three versions removed from npm; eight still tagged latest |
Snyk |
| 2026-08-04 → 08-05 | npm | Rolls back latest tags on at least nine seed packages; no public statement |
Reporting |
| 2026-08-05 | Vendors | Snyk, Socket, Wiz, Datadog, Aikido, Chainguard and Cycode publish analyses | Vendors |
Attack Anatomy
Initial Access — An Account, By Means Unknown
The entry point was the maintainer account behind the keyv and cacheable namespaces. How it was taken has not been published, and this paper does not speculate: phishing, token theft, session hijack and credential reuse are all consistent with what is known, which is another way of saying the evidence does not distinguish between them.
What matters downstream is what the account conferred: commit access to the repositories, and — through OIDC trusted publishing — the authority to release. Those two together are what made everything after this possible without a single forged signature.
Technique mapping (this paper's assessment): T1078 Valid Accounts; T1195.001 Compromise Software Dependencies and Development Tools.
Weaponisation — Committing to the Source, Not the Artefact
The attacker did not tamper with a built package. They committed the payload into the repository:
ee2681a9 adds "preinstall": "node setup.mjs" and the payload files
f97eabcd removes only the preinstall test
d8c850c7 adds the .claude and .vscode execution paths
The second commit is quietly informative. Removing the preinstall test while leaving the hook in place suggests the payload had already executed inside CI, and the test was what would have made it visible.
Two payload files were added:
setup.mjs 29,918 bytes
Math_Symbol.js 727,680 bytes
A 727 KB file named Math_Symbol.js in a caching library is exactly the kind of thing that a human reviewer notices and an automated pipeline does not.
T1027 Obfuscated Files or Information; T1027.009 Embedded Payloads.
The Provenance Problem — An Honest Pipeline, Poisoned Input
With the malicious source committed and tagged, the project's normal GitHub Actions release workflow ran and did its job correctly. It built the artefact from the tagged commit, and OIDC trusted publishing issued an attestation binding that artefact to that workflow and that source.
Every statement in the resulting provenance was true. keyv@6.0.0 genuinely was built by the keyv project's own workflow, from the keyv repository, at the commit it names. A downstream consumer verifying provenance would find nothing wrong, because nothing about the build was wrong.
The gap is between two questions that look similar and are not:
Did this artefact come from where it claims? — provenance answers this, correctly. Was what it came from trustworthy? — provenance does not address this at all.
For most of the attacks provenance was designed against — artefact substitution, registry compromise, a stolen publish token used to upload a hand-built tarball — the first question is the right one, and provenance defeats them. This attack simply moved upstream of the point where the guarantee begins.
The comparison with the Codecov compromise is exact and worth drawing. There, a genuine TLS certificate and a legitimate vendor domain proved the bytes came from Codecov, and proved nothing about whether Codecov's build environment had been compromised. Here, a genuine attestation proves the build was faithful to the source, and proves nothing about whether the source was. Each control is sound. Each was asked a question it does not answer.
T1195.002 Compromise Software Supply Chain; T1553 Subvert Trust Controls.
Execution — Three Ways In, None Requiring Use of the Package
The package.json gained "preinstall": "node setup.mjs". That runs during npm install, before any code is imported and regardless of whether the package is ever used — a transitive dependency six levels down executes it just as readily as a direct one.
The other two paths are the novel part, and they do not involve installing anything:
.claude/settings.json SessionStart → .claude/setup.mjs
.vscode/tasks.json folderOpen → .vscode/setup.mjs
A developer who cloned the repository to read the code, opened it in VS Code, or started an AI coding session in that directory executed the payload. No npm install, no import, no application run.
These files are a genuine blind spot. Dependency scanners read manifests and lockfiles. SCA tools resolve package graphs. Neither reads .vscode/tasks.json, because until recently that file could not execute anything a developer had not asked for, and neither reads .claude/settings.json, because it did not exist. The attack surface arrived with the tooling and the scanners have not caught up.
T1204.002 User Execution: Malicious File; T1546 Event Triggered Execution.
Payload — Credentials, Then Propagation
setup.mjs performs platform checks for Linux, macOS and Windows, uses child_process.execFileSync, touches the filesystem and network, and handles the Bun runtime. It downloads a standalone Bun binary and uses it to execute an obfuscated second stage — a choice that sidesteps whatever Node-level monitoring or policy the host has, because the runtime doing the work arrived with the payload.
The second stage targets:
GitHub and npm tokens cloud provider credentials
private keys database connection strings
Vault tokens Kubernetes service account tokens
GitHub Actions runner memory
That last item is worth pausing on. Reading runner memory reaches secrets that were injected for a job and never written to disk, which is precisely the storage model most CI guidance recommends as the safer option.
Persistence was established through a gh-token-monitor mechanism.
Then it spreads. Using the harvested npm tokens and OIDC trusted publishing, the worm downloads the tarball of another package the compromised identity can publish, injects the same preinstall hook and payload files, and issues a PUT to the registry. Each new victim's credentials extend the reach. The wider campaign reached 2,234 poisoned versions across 444 package names using credentials from twelve organisations.
T1552.001 Unsecured Credentials; T1005 Data from Local System; T1105 Ingress Tool Transfer.
Threat Actor Profile
No attribution has been made, and the vendors that analysed the incident are careful about it.
Socket records that the tradecraft closely matches Shai-Hulud npm worm activity — credential harvesting, package enumeration, GitHub repository staging — while stating that unrecovered identifying markers prevent definitive attribution. Chainguard describes the activity as part of a "Mini Shai-Hulud" campaign, which places it in a lineage rather than assigning it to a group.
What the operation demonstrates is a working understanding of how modern JavaScript development is actually secured:
It attacked upstream of the control rather than the control. Provenance is hard to forge, so the source it attests was poisoned instead.
It used the security mechanism to propagate. OIDC trusted publishing exists so maintainers need not hold long-lived registry tokens. The worm used it to publish, which means the safer mechanism carried the attack.
It executed where nobody scans. Editor and AI agent configuration are a new execution surface that arrived faster than tooling to inspect it.
None of that requires novel exploitation. It requires knowing which assumptions the defence rests on.
Technical Indicators
Defanged where applicable. From Snyk's and Socket's published analyses.
Payload files
setup.mjs 29,918 bytes
sha256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668
Math_Symbol.js 727,680 bytes
sha256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc
Manifest indicator
"preinstall": "node setup.mjs"
Editor and agent execution paths
.claude/settings.json SessionStart → .claude/setup.mjs
.vscode/tasks.json runOn: "folderOpen" → .vscode/setup.mjs
Filesystem and persistence
setup.mjs · Math_Symbol.js · math_init.js · gh-token-monitor.sh
service unit files · .plist entries
Repository commits
ee2681a9 lifecycle hook and payloads added
f97eabcd preinstall test removed
d8c850c7 .claude and .vscode execution paths added
Affected versions (first and second waves)
keyv@6.0.0
@cacheable/net@2.1.1 @cacheable/node-cache@3.1.2
@cacheable/memory@2.2.1 @cacheable/utils@2.5.1
cacheable@2.5.1 cacheable-request@13.0.20
flat-cache@6.1.24 file-entry-cache@11.1.6 / 11.1.7
cache-manager@7.2.10 ecto@5.0.1
@thiennq/docs-viewer@1.6.2 (outside the primary namespaces)
Detection
npm ls keyv flat-cache file-entry-cache cache-manager cacheable
grep the lockfile for the affected names and versions
grep manifests for "preinstall": "node setup.mjs"
find . -name 'setup.mjs' -o -name 'Math_Symbol.js' -o -name 'gh-token-monitor.sh'
inspect .vscode/tasks.json and .claude/settings.json in any cloned repository
Note the reported discrepancy on file-entry-cache: Snyk records 11.1.6, Socket records 11.1.7. Both should be treated as suspect pending reconciliation.
Legal and Regulatory Response
Government advisories. None located for this incident at the time of writing. CISA has previously published on widespread npm ecosystem compromise, but no advisory naming this campaign has been identified.
No CVE. None applies. This was a compromised account publishing malicious code, not a defect in software that a vulnerability identifier describes.
Registry response. npm rolled back latest tags on at least nine affected seed packages and removed malicious versions. No public statement from npm or GitHub had been published in the days following disclosure.
Law enforcement. No investigation has been publicly confirmed and no charges have been brought.
The pattern holds from every other supply chain incident in this archive: the effective response came from a package registry acting on its own and from commercial security vendors publishing analyses within a day. There is no regulatory instrument that engages here, and it is not obvious what one would say.
Impact Assessment
The exposed population is unknown and the widely quoted number does not measure it. The download figures describe the packages' importance across a month, overlap heavily because the packages depend on one another, and cover a period vastly longer than the hours the malicious versions were live. What matters is how many installs occurred during that window and executed the payload, and nobody has published it.
Any exposure is total for the host. This is not a data-theft incident with a bounded scope. The payload takes cloud credentials, registry tokens, private keys, database strings, Vault tokens and Kubernetes service account tokens from a developer machine or CI runner — which is to say it takes the keys to everything that machine can reach. A single affected build agent can be a route into production.
Reading runner memory undermines specific advice. "Do not write secrets to disk, inject them as environment variables for the job" is standard guidance. A payload reading runner memory collects exactly those.
Remediation has an order, and getting it wrong wastes the effort. Snyk's guidance is to isolate and gather forensics first, hunt for the gh-token-monitor persistence before revoking GitHub credentials, and only then rotate from a clean system. Rotating first, with persistence still in place, hands the attacker the new credentials.
The provenance finding will outlast the incident. Organisations have spent real effort adopting attestation on the understanding that it closes this class of attack. It closes part of it. Anyone whose supply chain security posture is "we verify provenance" now knows what remains open, and that is a more valuable output than any indicator in this paper.
The AI agent execution path is a preview. Editor and agent configuration in repositories is a new, largely unscanned execution surface, and this is an early instance rather than an isolated one. Every organisation whose developers clone unfamiliar repositories — which is all of them — has this exposure today.
Lessons and Defensive Recommendations
For Security Teams and SOC Analysts
Add .vscode/tasks.json and .claude/settings.json to what you inspect in any cloned repository, and alert on folderOpen and SessionStart entries that invoke scripts. These execute without an install step and no dependency scanner reads them.
Alert on lifecycle hook additions in dependency updates. A package that gains a preinstall between versions is a high-signal event. It is visible in a lockfile diff and it is what this attack needed.
Hunt persistence before rotating. gh-token-monitor, unexpected service units and .plist entries first; credentials second. The reverse order gives the attacker your new keys.
Treat CI runners as compromised hosts, not build infrastructure, once any dependency in the tree is implicated. Memory-resident secrets were in scope here.
For Developers and Platform Engineers
--ignore-scripts should be the default in CI, with an explicit allow-list for the handful of packages that genuinely need lifecycle scripts. It defeats the primary execution path outright.
Pin and use a lockfile with integrity hashes. A newly published latest cannot enter a build that resolves to committed versions.
Understand precisely what provenance tells you. It attests build origin. It does not attest that the source was not compromised. Verifying it remains worth doing — it is real evidence about where an artefact came from — but "we check provenance" is not an answer to a poisoned commit.
Delay adopting brand-new releases of foundational packages. A cooling-off period of even a day or two would have excluded this entirely; the malicious versions were live for hours.
Review editor and agent configuration in repositories you clone, particularly ones you are evaluating rather than maintaining. Opening the folder is now an execution event.
For Registries and Tooling Vendors
Surface lifecycle hook additions prominently in the diff between versions. The information exists; nothing puts it in front of the person deciding whether to upgrade.
Treat a first-ever preinstall on a long-established package as worth a hold or a signal, rather than an ordinary publish.
Scanners need to read editor and agent configuration. The execution surface moved; the tooling has not.
For Leadership and CISOs
Ask what your provenance verification actually establishes. If the answer given is "it proves our dependencies are not malicious," that is the gap this incident found, and it is worth correcting before it is tested.
Your developers' laptops hold production credentials, and this payload was built to take them without anyone running the software. Endpoint posture for engineering machines is supply chain posture.
Note the response pattern once more. A registry acting alone, and vendors publishing within a day. No advisory, no directive, no enforcement. If your incident response plan assumes external notification of a dependency compromise, this is the third paper in this archive showing it does not arrive.
Sources
- Snyk — Inside the keyv npm Supply Chain Compromise
- Socket — Popular npm Packages in the keyv and Cacheable Namespaces Compromised in Active Supply Chain Attack
- Wiz — keyv and cacheable npm Package Hijacked in Supply Chain Attack
- Datadog Security Labs — Worm compromises hundreds of popular npm packages
- Chainguard — The keyv and cacheable npm Supply Chain Attack: Inside the Mini Shai-Hulud Campaign
- Cycode — No Install Required: The keyv and cacheable npm Worm That Weaponized AI Coding Agents
- Aikido — Keyv and friends compromised in npm supply chain attack
- safedep — keyv and cacheable npm compromise: 400+ packages
- Kodem — keyv npm Supply Chain Attack: IOCs and Runbook
- The Hacker News — Keyv-Linked npm Worm Poisons Hundreds of Packages, Plants Claude Code and VS Code Hooks
Related Research
A credential left in a Docker image layer let an unidentified attacker rewrite Codecov's Bash Uploader and harvest every secret from its customers' CI environments for sixty days. No one was ever identified.
Between late 2025 and mid-2026, the software supply chain threat landscape underwent a fundamental paradigm shift with the emergence of the Shai-Hulud malware lineage. Culminating in the highly...
A copy of Postmark's MCP server, published to npm by someone unaffiliated with them, worked perfectly for fifteen versions. The sixteenth added one line — a BCC to an address the publisher controlled.