All posts
Unproven Execution

keyv npm Worm: AI Agent Hook as Unproven Execution

A Shai-Hulud npm worm hijacked keyv@6.0.0 on Aug 4, planting a Claude Code SessionStart hook that silently runs node on every dev's next session

Securityv0 Intelligence Team OWASP: ASI05 sv0 finding: unproven_execution
supply-chain nhi npm shai-hulud claude-code unproven-execution

The Incident

On 2026-08-04 at 09:35 UTC, an attacker in control of the GitHub account of Jared Wray (jaredwray) pushed malicious commits directly to the main branch of the keyv repository and cut keyv@6.0.0. npm accepted the release with a valid GitHub Actions OIDC provenance attestation and SLSA signature — trusted publishing did exactly what it was designed to do; the identity behind the publish credential had already been compromised. Over the next three hours and forty-three minutes (until roughly 13:18 UTC) a self-replicating worm — variously named ChainDrop, Mini Shai-Hulud, or Shai-Hulud “Here We Go Again” — spread to a confirmed footprint of 444 package names across 2,234 versions belonging to twelve unrelated organizations, powered by npm publish tokens the payload harvested from developer workstations as they installed poisoned versions of keyv (~600M downloads/month), flat-cache (~580M/month), file-entry-cache, cacheable, cacheable-request, and cache-manager. Aikido raised the alarm on X during the active push; Semgrep, Wiz, Snyk, Socket, SafeDep, Datadog Security Labs, JFrog, Elastic Security Labs, StepSecurity, Endor Labs, Chainguard, and the SANS Internet Storm Center published analyses within twenty-four hours. No CVE was assigned; Singapore’s CSA issued advisory AD-2026-009. Semgrep and Chainguard trace the same setup.mjs dropper and identical IDE-hook plants to the April 2026 Lightning PyPI compromise SecurityV0 covered on 2026-05-01 and the May 2026 @antv npm campaign — the same TeamPCP-adjacent lineage, now on its third major wave.

MITRE ATT&CK coverage: T1195.002 Compromise Software Supply Chain, T1546 Event Triggered Execution, T1552.001 Credentials in Files, T1102 Web Service (used for the Ethereum-smart-contract C2 channel), T1053 Scheduled Task/Job (the LaunchAgent / systemd token watcher).

The Authority Path That Failed

Two authority failures compound. The initiating failure is a maintainer-account takeover: whoever pushed to jaredwray/keyv held the org’s full publish scope for the keyv, cacheable, flat-cache, file-entry-cache, and cache-manager namespaces on npm. The trust anchor on the publish side was “commits merged to main by an authorized principal, released by the project’s OIDC-linked GitHub Actions workflow.” Trusted Publishing bound the release credential to a specific repository and workflow; it did not verify that the human behind the commits was still the human the account represented. The SLSA signature on keyv@6.0.0 attests to the pipeline that built it, not to the intent of the code that came out.

The propagating failure is the SV0-shaped one. The dropped payload plants two persistence artefacts inside every installed project: .claude/settings.json registers a Claude Code SessionStart hook that executes node .vscode/setup.mjs, and .vscode/tasks.json defines an Environment Setup folder-open task that executes node .claude/setup.mjs. When a developer next opens the project in Claude Code — or in VS Code with trusted-workspace tasks enabled — the environment executes attacker-supplied JavaScript under the developer’s local account through configuration the operator did not author. The gap between what the operator declared (an editor-assistant reading files) and what the runtime exercised (a shell interpreter, at session start, against an in-repo config file) was auditable at deploy time; every installed dependency that carried a .claude/settings.json or a folder-open .vscode/tasks.json was a de-facto execution surface, whether or not the maintainer had been compromised yet. From that foothold the payload also installs com.user.gh-token-monitor.plist (macOS LaunchAgent) or gh-token-monitor.service (Linux systemd), which polls the GitHub API every sixty seconds with a stolen token and detonates an attacker-supplied handler the moment the token starts returning 4xx — inverting the standard “revoke first” incident-response reflex.

SecurityV0 Perspective

This maps to unproven_execution (ASI05). The Claude Code SessionStart hook attaches a code-execution tool — node against an in-repo file — to an agent’s session with no prompt, no confirmation, and no operator-authored declaration. That is the Langflow/CSV-Agent+REPL shape, moved one layer up the stack: instead of a framework default silently binding a REPL, a backdoored dependency rewrites the agent’s startup contract via a config file the framework treats as authoritative. Prior SecurityV0 Shai-Hulud coverage (Lightning PyPI, Hades PyPI, AsyncAPI npm bot PAT) has framed this family as nhi_compromise (ASI06) — accurate for the publish side, and still the secondary angle here — but the August variant is the moment the payload’s persistence surface moves inside the AI coding agent’s session configuration, and that reframes the primary control question.

What To Do

  • Pin the keyv/cacheable family to pre-2026-08-04 versions. keyv@6.0.0, and every version of cacheable, flat-cache, file-entry-cache, cacheable-request, cache-manager, and @cacheable/utils published inside the 09:35–13:18 UTC window on 2026-08-04, are the confirmed malicious releases. Refuse >= ranges in CI for this set, downgrade any transitive resolution that landed on those tags, and cross-reference the CSA AD-2026-009 advisory. Lockfile-drift alerts should treat any bump into the keyv/cacheable namespace on that date as a security event, not a maintenance one.
  • Inventory .claude/settings.json and .vscode/tasks.json on every workstation and CI runner. Diff against a known-good baseline. Unexpected SessionStart hooks, runOn: folderOpen entries, or command strings invoking node .claude/setup.mjs or node .vscode/setup.mjs (or any sibling filename) are incident indicators. Alert on any creation or modification of those files inside the 2026-08-04 window, and treat the file’s hash — not “does it exist” — as the control.
  • Kill the token watcher before revoking any credential. Grep ~/Library/LaunchAgents/ for com.user.gh-token-monitor.plist on macOS and ~/.config/systemd/user/ for gh-token-monitor.service on Linux. launchctl unload (or systemctl --user disable) and delete the unit file first; only then revoke and rotate the associated npm, GitHub, AWS, Vault, and cloud credentials. The watcher self-clears at twenty-four hours, but “wait it out” is not the plan — the plan is to remove the trigger before you become the trigger.
  • Rotate every credential reachable from a host that installed a keyv-family release in the malicious window. In scope: .npmrc publish tokens (revoke and reissue, do not rotate in place), GitHub CLI tokens, fine-grained and classic PATs, AWS/Azure/GCP SDK credentials, HashiCorp Vault tokens, kubeconfigs, SSH keys, database connection strings, Stripe and Slack API tokens, and crypto wallet material. Order matters: watcher first (previous bullet), then credentials; the payload reads secrets at install time, not just on first agent session.
  • Treat AI-agent and IDE config drops from dependencies as install-time scripts. Whatever policy your organization applies to preinstall and postinstall — sandboxing, disabling, or install-time review — must extend to .claude/settings.json hooks and .vscode/tasks.json folder-open tasks that dependencies write into project directories. A .claude/settings.json written by npm install is functionally a postinstall script that fires on the developer’s next agent session; scan for it, gate it, and refuse to open projects whose IDE-agent config appeared with a dependency install.

Sources