npm Worm Plants Hooks in Your AI Coding Agent

On August 4, 2026 at 09:35 UTC, keyv@6.0.0 shipped to npm with a malicious payload. Within the hour the rest of the cacheable family followed: flat-cache, file-entry-cache, cacheable-request and more. The campaign — a new wave of Shai-Hulud — reached more than 400 package names across well over a thousand versions, together accounting for over two billion monthly installs.

The scale is not the interesting part. The interesting part is that the payload did not stop at stealing credentials. It left a door open inside the developer's own tooling — including the AI coding agent.

The problem: trust travels down the tree

keyv is rarely a dependency anyone picks on purpose. It arrives through eslint, through build tooling, through layers nobody opens. Poison a package at that depth and every tree below it becomes a delivery channel.

Worse, provenance behaved exactly as designed. The npm manifest records keyv@6.0.0 as published by GitHub Actions as a trusted publisher, with valid attestations attached. The malicious code was already in the repository when the legitimate workflow built and signed the artifact. A signature proves where a file came from, not what is inside it.

What changed: persistence inside developer tooling

The payload runs in stages. A preinstall hook executes setup.mjs at install time. That loader pulls a second file, Math_Symbol.js, which scans the machine for GitHub and npm tokens, cloud keys, Vault tokens, Kubernetes service account tokens and database connection strings — then uses what it finds to republish itself through other maintainers' accounts.

The stage worth pausing on is the third. The worm writes two files into the repository itself:

  • .claude/settings.json, carrying a SessionStart hook.
  • .vscode/tasks.json, carrying a task with "runOn": "folderOpen".

Clear out node_modules and the execution path survives. Someone opens the folder in the editor, or starts a session with the agent, and it runs again. This is not a flaw in either tool — VS Code gates automatic tasks behind workspace trust, and the agent applies the same trust model to repository-supplied settings. But on a project the team works in daily, that trust was granted a long time ago.

A practical check

Look for the persistence artifacts before rotating anything:

bash
# 1) planted execution paths inside the repo
grep -rn "Math_Symbol" . --include="*.js" --include="*.mjs" 2>/dev/null
grep -n  "SessionStart" .claude/settings.json 2>/dev/null
grep -n  "folderOpen"   .vscode/tasks.json    2>/dev/null

# 2) does your tree resolve a poisoned version?
npm ls keyv flat-cache file-entry-cache cacheable cacheable-request

Then close the door across the whole tree with overrides in package.json:

json
{
  "overrides": {
    "keyv": "<6.0.0",
    "flat-cache": "<6.1.24",
    "file-entry-cache": "<11.1.6",
    "cacheable": "<2.5.1",
    "cacheable-request": "<13.0.20"
  }
}

Real-world caveats

Order matters. Rotating tokens before removing persistence hands the attacker the new ones. Clean first, rotate second.

Unpublishing is not instant. Public warnings appeared around 10:18 UTC; an hour later eight releases were still resolving under the latest tag. Any build in that window may have pulled a poisoned artifact.

`--ignore-scripts` alone is not enough. This campaign used preinstall, but earlier waves moved execution to import time — covered here previously. Disabling scripts is a step, not a solution.

The lockfile is the real defence. npm ci plus a deliberate delay before adopting any freshly published version shrinks the exposure window more than any after-the-fact scan.

Takeaway

The supply chain no longer ends at the registry. It now extends into the configuration files an editor and an agent read when the project opens. Treat .claude/ and .vscode/ as executable code: review them in pull requests, and give them the same scrutiny as a dependency bump.

Primary sources: Snyk's analysis and Socket's report.