Independent AI intelligence Two editions daily · ET
FervorAI

Analysis · August 4, 2026 · concept

keyvClaude CodenpmSafeDepagent-securityclaude-codeagent-infrastructureprivacy

The keyv npm Worm Planted a Claude Code Hook. Opening the Repo Is the Second Attack.

On August 4 the keyv supply chain compromise shipped two triggers, not one. The second fires when you open the checkout in Claude Code or VS Code, with nothing installed. Here is the mechanism, and the audit that actually catches it.

Two commits landed in the keyv repository two minutes apart on the morning of August 4, 2026. The first, ee2681a9 at 09:02:37 UTC, is the one everybody wrote about: it added a preinstall script and a 727,680-byte payload, and it went out to npm as keyv@6.0.0 half an hour later, on a package with 619 million downloads in the preceding month.

The second commit, d8c850c7 at 09:04:30 UTC, added five files nobody was scanning for. Two of them were .claude/settings.json and .vscode/tasks.json.

That second commit is the story. It carries a green verified badge, its commit message is chore: update config, and it needs no npm install to fire at all. It fires when you open the folder.

The lockfile stopped being the perimeter

Here is my position, and I do not think it is a stretch: for anyone running a coding agent, the project checkout is now an execution surface with roughly the same privileges as a dependency install, and almost none of the tooling treats it that way.

Every supply chain control most teams have built points at the dependency tree. Lockfile scanning, install-script policy, SBOM generation, registry allowlists. All of it assumes the malicious thing arrives as a resolved package. SafeDep's writeup puts the gap in one sentence: "Scanning a lockfile catches a poisoned dependency. It does not catch a poisoned tasks.json in a repository the developer just cloned."

And the attacker knew exactly who would clone it. SafeDep counted 3,167 stars and 199 forks on the repository, then made the point that stings: the people most likely to pull down a fresh copy of keyv on August 4 were the ones reading the incident reports and going to look at the source for themselves. Careful people. The trigger was aimed at them.

What the two files actually do

The mechanism is small enough to read in full, which is what makes it worth reading in full.

.claude/settings.json registers a SessionStart hook with a * matcher that runs node .vscode/setup.mjs. .vscode/tasks.json registers a task labeled Environment Setup with runOptions: { runOn: "folderOpen" } that runs node .claude/setup.mjs.

Read that again slowly. The Claude hook invokes the script in the VS Code directory. The VS Code task invokes the script in the Claude directory. They are crossed over each other.

That crossing is not an accident, and it is the most professional thing in the whole incident. Open .claude/settings.json, notice the hook, go looking for .claude/setup.mjs, and the reference points somewhere else entirely. Do the same audit starting from .vscode/ and you get the same misdirection in reverse. Neither directory reads as a self-contained loop, so a reviewer checking one and moving on finds a dangling pointer instead of a payload. The label Environment Setup does the rest of the work, because that is what a real task in that slot would be called.

Both setup.mjs copies in the repository are 11,017 bytes, less obfuscated than the 29,918-byte version that shipped in the npm tarball. .claude/math_init.js is 727,680 bytes, byte-identical in size to Math_Symbol.js in the package. The two paths converge on the same second stage.

Stage one is careful in a way that defeats network controls. Per the static analysis in issue #2047, reproduced by SafeDep as a reported reconstruction rather than a confirmed finding, setup.mjs runs which bun, and if Bun is missing it downloads a matching build from github.com/oven-sh/bun/releases/download/bun-v1.3.13/, runs the payload, and deletes the runtime on the way out. The only outbound host at install time is GitHub. There is no attacker domain to catch in egress logs, and no novel C2 hiding in the loader, because there is not one.

The badge that signed it and the provenance that vouched for it

Two trust signals failed here, and they failed differently.

The commit that planted the hooks is cryptographically verified by GitHub, with its author field set to github-actions[bot] and a Co-authored-by: claude trailer. GitHub signs commits created through its API and web UI with its own key, and the caller supplies the author name and email as free text. So a credential with write access is enough to produce a verified commit attributed to a bot, dressed up as routine agent-assisted maintenance. As Snyk put it, a verified badge proves GitHub signed the commit object. It says nothing about whether the maintainer authorized the change.

The npm release, separately, carries valid SLSA provenance over OIDC pointing at refs/tags/v6.0.0 and the real release workflow. That attestation is genuine. The poisoned source was in the tagged repository state, so the legitimate pipeline built and attested it faithfully. Snyk's line is the one to keep: provenance "can faithfully attest a build whose source or workflow context has already been compromised."

I spent a decent chunk of 2025 reading arguments that build provenance would put an end to this class of attack. It did not. It answered the question it was designed to answer, which is where the artifact came from, and that question turned out not to be the one that mattered.

Put this into practice

Three things, in this order, and none of them take long.

First, find every checkout on disk that grew an agent or editor config this week. This is the audit nobody is running:

find ~ -path "*/.claude/settings.json" -newermt "2026-08-01" 2>/dev/null
find ~ -path "*/.vscode/tasks.json" -newermt "2026-08-01" 2>/dev/null

Then read what you find, all the way through, following every path reference out of the directory it lives in. A hook that points at a sibling directory is the pattern.

Second, check the dependency path, including transitive. flat-cache and file-entry-cache arrive through ESLint in a huge number of projects, and both sat above 570 million monthly downloads. Snyk's advisory SNYK-JS-KEYV-18515941 covers the family:

npm ls keyv flat-cache file-entry-cache cacheable-request cacheable \
  @cacheable/utils cache-manager @cacheable/net \
  @cacheable/node-cache @cacheable/memory ecto --all

Pin back to the clean releases (keyv@5.6.0 is the last clean stable 5.x), rebuild the lockfile with npm install --package-lock-only --ignore-scripts, and reinstall with npm ci --ignore-scripts.

Third, if anything ran, hunt persistence before you rotate anything. Both Snyk and SafeDep flag a gh-token-monitor mechanism that watches a stolen GitHub token and executes an attacker-supplied handler when the token stops working. Revoking first is observable to the monitor. Look in ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/, ~/.config/systemd/user/gh-token-monitor.service, and ~/Library/LaunchAgents/com.user.gh-token-monitor.plist, disable what you find, then rotate from a clean machine.

The durable habit, past this week: treat the "do you trust the authors of the files in this folder" dialog as a security decision with real consequences, because in a repository with agent config in it, that is now what it is. I have clicked through that prompt without reading it more times than I want to count.

What we still do not know

Plenty, and the honest reporting says so.

Nobody executed the payload or decoded its string table. SafeDep did not run it. Snyk did not decrypt Math_Symbol.js. The second-stage capability list (GitHub and npm tokens, ghs_ App installation tokens, a Vault auth response shape, aws_access_key_id, tar construction, npm publish fields) is recovered from property names an obfuscator cannot rename. SafeDep is careful about what that proves: "The evidence establishes reach, not intent." No C2 host was recovered. No exfiltration was observed. There is no verified public count of successful second-stage executions.

The scale numbers disagree with each other, too. Snyk independently confirmed 11 malicious releases across the maintainer's packages, with eight still tagged latest at 11:16 UTC. SafeDep's broader monitoring reported 442 versions across 353 names. Aikido reported at least 868 packages across 1,381 versions. I could not reconcile the larger totals against a complete public list, so I would treat the eleven Snyk hash-verified releases as the floor and everything above it as still-moving telemetry.

On the IDE path specifically, exposure is not the same as execution. Snyk's phrasing is right: VS Code may prompt before running automatic tasks depending on workspace trust settings, so opening a checkout "is an exposure condition rather than proof that code ran." Whether Claude Code's SessionStart hook prompts in your configuration is worth checking on your own machine rather than taking my word for it.

And attribution is genuinely open. A verified commit with a spoofed author is consistent with a stolen token, a hijacked session, or the account itself, and those are not distinguishable from outside the organization. The maintainer should be treated as a victim until they say otherwise. The response is identical either way.

The thing to do tomorrow

Pick the last repository you cloned because a security post told you to look at it. Before you open it in anything, run find . -path "*/.claude/*" -o -path "*/.vscode/*" and read every file that comes back.

If that takes you ninety seconds and finds nothing, good. Do it again next time. The attack that worked this week is the one that assumed you would skip it.


Sources: SafeDep, "keyv 6.0.0 Compromised With an Install Hook and an IDE Auto-Run Hook"; Snyk, "Inside the keyv npm Compromise"; The Hacker News; Aikido.