I audit dependencies. Lockfiles, advisories, the whole ritual. Then the keyv worm shipped its payload as committed agent config, and I realised I have no equivalent reflex for the files that configure the thing writing my code.

What the keyv worm changed about how I review pull requests
I came back from two weeks off, ran git pull on a few repos, and got on with
my day. That is the whole story of my first morning back. Nothing dramatic.
Then someone shared the Datadog Security Labs write-up on the npm worm that hit
keyv and its neighbours, and I spent the next hour not doing the thing I had
planned to do.
Not because of the dependencies. Those I audit. Lockfiles, advisories, the whole ritual. That muscle works, and it has worked for years. It was a detail near the bottom of the article that I could not get past.
On 4 August 2026, someone got into the GitHub account of the maintainer behind
keyv and a family of related caching packages. They pushed commits straight to
main and cut releases. Within minutes the same payload was sitting in several
packages from the same maintainer, and from there it kept going on its own,
using stolen npm tokens to republish other people's packages.
The mechanism was the boring one. A preinstall entry in package.json
pointing at a small loader file. Run npm install and that loader executes
before your own code gets anywhere near the machine. It pulls down a bigger
second stage, goes looking for credentials in the usual places, and then uses
whatever tokens it finds to keep spreading.
The reach numbers depend on who you ask, which is itself telling. Different
security vendors published counts ranging from a few hundred packages to well
over two thousand affected versions, and nobody produced a list everyone agreed
on. What is not disputed is that most people who were exposed never installed
any of these packages directly. The common path ran through eslint, into
file-entry-cache, into flat-cache, into keyv. Four levels down. You do not
choose that dependency, you inherit it.
None of that is the part that stopped me, though. All of it is a normal, well understood supply chain attack, and the industry has a shape for responding to those.
The part that stopped me is that the attacker also committed editor and agent hook files into the repository itself. Configuration for VS Code, configuration for Claude Code, sitting in the source tree like any other file. On at least one repo, that meant opening a checkout could be enough. No install step required.
I want to be careful here, because it would be easy to turn this into a story about people being careless. It isn't. These files slipped through because they fall into a gap between two processes that both work fine.
Process one is code review. A human reads the diff and asks whether the change makes sense. That process covers application code. Agent config isn't application code, so it doesn't really get read. It gets scrolled past on the way to the files that matter, in the same way most of us scroll past a lockfile diff.
Process two is dependency scanning. Dependabot, Snyk, whatever you run in CI. That process covers things declared in a manifest. Agent config isn't a dependency, it isn't declared anywhere, and no scanner has an opinion about it.
So you end up with a category of file that is version controlled, present in every checkout, and reviewed by nothing. Not because anyone decided that, but because we added the category faster than we added the process around it.
There is a distinction here that I have not seen anyone make cleanly, and it matters for what you actually do about it.
The first level is files that execute. Hook definitions, MCP server entries,
tasks.json in VS Code. These contain commands, and something in your toolchain
runs those commands, sometimes without asking. This is the level the keyv attack
used, and it is the one that gets attention because the failure mode is obvious.
The second level is files that instruct. CLAUDE.md, slash commands, agent
definitions, skills. These are markdown. They do not execute anything. They are
read by an agent that has a terminal, a filesystem, and often credentials.
I think the second level is the more interesting one, precisely because it looks harmless. Markdown feels like documentation. It has none of the visual cues that make you slow down. And unlike a shell script, you cannot glance at it and see what it does, because what it does depends entirely on what the model decides to do with it.
Before writing any of this I went and looked at my own setup, on the assumption that I would find something embarrassing.
# what's committed
git ls-files | grep -E '^\.(claude|vscode|cursor)/'
# anything executable in there
git ls-files -z | grep -zE '^\.(claude|vscode|cursor)/' \
| xargs -0 grep -lE '"hooks"|"mcpServers"|"tasks"|preLaunchTask' 2>/dev/null
# shell escapes inside command files
grep -rn '^!' .claude/commands/ .claude/agents/ 2>/dev/nullThree files. An explorer agent, a /do command, a workflow skill. All markdown,
no hooks, no MCP servers, no shell escapes. Cleaner than I expected.
Which should have been reassuring and somehow wasn't, because none of that is the point. My repo is clean today, with one contributor, who is me. It takes one pull request from one other person adding a file nobody reads, and the state I just verified stops being true.
Five things, in rough order of how much they are worth.
Ownership on the agent paths. A CODEOWNERS entry covering .claude/,
.vscode/ and .cursor/, so any pull request touching those directories needs
an explicit review rather than passing on the strength of the rest of the diff.
This is the cheapest fix on the list and probably the one that matters most,
because it converts an invisible category into a visible one.
Lifecycle scripts off in CI. npm ci --ignore-scripts as the default, with
a short list of packages that genuinely need their install step, kept
deliberately short. If the keyv payload had landed on a build machine with that
flag set, the loader never runs.
A waiting period on new versions. Not pinning, which people already do and which does not help when the poisoned version is the one you pin to. A cooldown, so that a release published this morning does not enter my builds this afternoon. Most of these campaigns get caught within hours. The whole risk window is the beginning.
Local settings stay local. Anything with machine specific paths or tokens
goes in .gitignore and never gets committed. Related, and worth saying out
loud: the .claude.json in your home directory holds project history and
metadata, so it is not a file to paste into a screenshot or a support thread.
And the honest one. I run no MCP servers, which removes an entire class of this problem for me. That is not a security decision I made, it just happens to be where my setup is. I am not going to present an accident as a strategy, and if I add one next month I will be adding the exposure back.
This isn't really a story about npm, and it isn't a story about Claude Code either. Both are incidental. Swap in a different registry and a different agent and the shape holds.
The shape is that our tooling picked up a new category of file, that category became normal in about eighteen months, and our review habits did not move at all. We built the dependency review reflex over roughly a decade, mostly by getting burned, and it is genuinely good now. There is no equivalent reflex for the files that configure the thing writing our code, and there won't be one until enough people get burned to build it.
I would rather build the habit before that part.
The Datadog Security Labs write-up on the worm is here, and is worth reading in full if you want the payload analysis.
Was this helpful?