Now in early access, book a 30-minute demo →
← Back to blog AdvisoryCritical

Shai-Hulud 'Here We Go Again' Hits keyv and cacheable - 868 npm Packages, Claude Code Hooks Planted

AI Supply-Chain Attacks·Critical·Shai-Hulud: Here We Go Again (no single CVE; npm supply-chain worm campaign)·
Affected keyv 6.0.0, flat-cache 6.1.24, file-entry-cache 11.1.6, cacheable-request 13.0.20, cache-manager 7.2.10 and the @cacheable scope; secondary spread reported across at least 868 packages and 1,381 versions with 2B+ combined monthly installs

On August 4, 2026, an attacker compromised the GitHub account of the maintainer behind keyv and the cacheable package family and used it to publish a self-replicating credential stealer. The campaign brands itself "Shai-Hulud: Here We Go Again" - the third wave of the Shai-Hulud lineage - and it reached some of the most-installed packages in the npm registry: keyv, flat-cache, and file-entry-cache together account for well over a billion monthly installs, mostly as transitive dependencies nobody chose directly. Beyond credential theft, this wave did something new: it wrote itself into AI coding-agent configuration, planting a Claude Code SessionStart hook and a VS Code folderOpen task, and it read .claude/settings.json as a file worth stealing. This advisory covers what happened, why the AI-artifact layer is the part your dependency tooling will not clean up, and how Anomity surfaces and governs it.

What happened

The initial access was an account takeover, not a typosquat. The attacker gained control of the GitHub account of the maintainer who owns the keyv and cacheable ecosystem, pushed malicious code to the main branch, and published poisoned versions to npm. Because one maintainer stewards a cluster of foundational caching libraries, a single compromised identity reached keyv 6.0.0, flat-cache 6.1.24, file-entry-cache 11.1.6, cacheable-request 13.0.20, cache-manager 7.2.10, and the @cacheable scope in one motion. These are dependencies of ESLint tooling, HTTP clients, and thousands of build pipelines - almost nobody installs them on purpose.

Execution came from an install-time hook. Each poisoned package added two files and a "preinstall": "node setup.mjs" entry to package.json, so the payload ran automatically during npm install. setup.mjs is an obfuscated dropper that downloads the Bun runtime (v1.3.13) from GitHub's release CDN and uses it to execute the real payload - a roughly 728 KB compiled Bun bundle shipped as Math_Symbol.js or math_init.js. Pulling its own runtime lets the payload sidestep the host Node environment entirely, and the Bun/1.3.13 user-agent became one of the cleaner network detections.

The payload is a broad-spectrum secret harvester. It collects npm tokens from ~/.npmrc (validating them against registry.npmjs.org/-/whoami before use), GitHub tokens in ghp_, gho_, and ghs_ forms plus OIDC JWTs from ~/.config/gh/hosts.yml, AWS credentials from ~/.aws/, the EC2 instance metadata service (IMDSv2 with IMDSv1 fallback), ECS container metadata, and Secrets Manager enumeration across regions, Kubernetes service-account tokens from /var/run/secrets/kubernetes.io/serviceaccount/ followed by API queries for namespace secrets, HashiCorp Vault tokens with full KV enumeration via /v1/sys/mounts, and Stripe and Slack keys by prefix. In CI it goes further, dumping GitHub Actions runner process memory to lift ACTIONS_ID_TOKEN_REQUEST_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL for OIDC token theft. A generic sweep of roughly 200 glob patterns covers .env*, private keys, SSH material, Terraform state, Docker configs, KeePass databases, VPN profiles - and IDE and agent settings, explicitly including .claude/settings.json.

Exfiltration is RSA-encrypted and uses GitHub itself as the dead drop: stolen bundles are pushed to public repositories whose description contains Shai-Hulud: Here We Go Again, of which roughly 1,300 existed at the time of reporting. Notably, the malware does not hardcode its fallback command-and-control domain. It resolves C2 at runtime by calling an Ethereum smart contract (StringListStore) via eth_call - infrastructure the operator can rotate without republishing the payload. The contract initially returned three domains and was later narrowed to npm-cache[.]com (registered 2026-05-22). The worm then replicates using the npm and GitHub tokens it just stole, which is what turned a handful of primary packages into secondary compromise across scopes including @onereach, @servicetitan, @or-sdk, @ornikar, @qlik, and @picsart.

Counts moved all day and still differ by tracker, which matters if you are scoping exposure: Aikido reported at least 868 packages across 1,381 versions exceeding 2 billion monthly installs, OX Security reported 440+ packages, and SafeDep revised from 353 poisoned versions to 1,684 versions across 420 package names. Treat every published figure as a floor, not a total. OX also reported a coercive dead man's switch - a payload branch threatening destruction if the stolen GitHub token is revoked, carrying the string IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients. Do not let that change the response: revoke the tokens anyway, and rebuild rather than negotiate with a threat string.

PackageMalicious versionApprox. monthly installs
keyv6.0.0604M (~127M weekly)
flat-cache6.1.24580M
file-entry-cache11.1.6571M
cacheable-request13.0.20137M
@cacheable/utils2.5.134M
cacheable2.5.130M
@cacheable/memory2.2.128M
cache-manager7.2.1016M
@cacheable/node-cache3.1.26M
@cacheable/net2.1.13.7K
ecto5.0.14.5K
IndicatorValue
Install hook"preinstall": "node setup.mjs" added to package.json
Dropped filessetup.mjs, Math_Symbol.js / math_init.js (~728 KB Bun bundle), router_runtime.js
AI/IDE persistence.claude/settings.json (SessionStart hook), .claude/setup.mjs, .vscode/tasks.json (folderOpen task), .vscode/setup.mjs
setup.mjs (SHA-256)54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668
setup.mjs, community spread (SHA-256)fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb
Math_Symbol.js / math_init.js (SHA-256)9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc
Networknpm-cache[.]com:443/router; Bun/1.3.13 user-agent; Bun download from GitHub releases; /tmp/bun-dl-*/
C2 discoveryEthereum StringListStore contract queried via eth_call
Exfil channelPublic GitHub repos with description Shai-Hulud: Here We Go Again (~1,300 observed)
StringsShai-Hulud: Here We Go Again; IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients

Why this is an agentic-endpoint risk

The part of this campaign that outlives the incident is the persistence it left in AI developer tooling. The worm wrote a SessionStart hook into .claude/settings.json pointing at .vscode/setup.mjs, and an Environment Setup task into .vscode/tasks.json with runOn: folderOpen pointing at .claude/setup.mjs - each config referencing the other's directory to look unremarkable in a diff. To be precise about the risk: neither fires on its own. VS Code blocks automatic tasks in an untrusted workspace, and Claude Code applies workspace trust to repository-supplied project settings, so both need a human to trust the folder. Those trust boundaries held, and they are the reason this was persistence rather than a second execution wave.

What they do not do is remove anything. The preinstall payload already ran before any trust prompt was relevant, and the planted hook stays on disk in every clone and every developer's working copy after the fact. This is where dependency remediation and AI-artifact remediation come apart: npm audit fix, a lockfile pin, or a version bump resolves the malicious package and leaves a .claude/settings.json hook sitting in a repository, waiting for someone to click *Trust* weeks later in a folder no scanner is watching. SCA tools read manifests and lockfiles. None of them inventory agent configuration, and this is the first Shai-Hulud wave to make that gap load-bearing.

The same blind spot explains why the theft went unnoticed. npm install running a preinstall script is ordinary developer activity to network monitoring; a Bun binary compiling and running looks like a toolchain to EDR; and secrets leave as an RSA-encrypted blob pushed to GitHub, which is indistinguishable from routine developer traffic to DLP. Hooks are one of the eight AI artifact types Anomity inventories per endpoint - alongside AI agents, MCP servers, extensions, skills, plugins, secrets, and CLIs - precisely because they are configuration that runs code on someone else's behalf, adopted bottom-up the way AI agents became the new shadow IT. This wave also confirms agent config is now a *target*, not just a vector: .claude/settings.json appeared in the harvester's own glob list. It shares its root cause with the sibling Shai-Hulud 2.0 and Mini Shai-Hulud waves, and with Miasma's binding.gyp variant: an install-time hook turned into credential theft that replicates.

How Anomity surfaces and governs it

First, inventory the artifact layer, not just the dependency tree. Anomity's unprivileged Endpoint Sensor runs on Windows, macOS, and Linux and inventories eight AI artifact types per endpoint, including hooks and the settings files that declare them. A planted SessionStart hook in .claude/settings.json, or a folderOpen task pointing at a dropped setup.mjs, is an inventoried artifact with a known first-seen time - so "which endpoints still carry agent config this worm wrote?" is one query rather than a fleet-wide hunt after your SCA report has already gone green. Anomity collects metadata only, and secrets are redacted on the endpoint, so the npm, GitHub, AWS, and Vault credentials this payload hunts are never centralized into a second target.

Second, decide at the hook. Where an agent exposes a control point - for example the PreToolUse event in Claude Code - Anomity evaluates each call against policy and returns allow, deny, or log before it runs. A session-start hook that spawns an unexpected process, reads a credential file, or pushes to a freshly created GitHub repository is denied at the boundary. That is what runtime governance provides against a payload whose whole design is to execute before review, and it is the control that still applies on the day someone clicks *Trust* on a repo cloned during this window.

Third, keep the record. Install events, config changes, and policy decisions land in a queryable 90-day audit trail, and findings route to SIEM, Slack, email, or Jira. For a campaign where the honest answer to "how many packages?" changed four times in a day, the useful question is narrower and answerable from your own fleet: which endpoints and pipelines ran an affected install, in what window, and what did their agents and hooks touch afterward. Anomity is SOC 2 Type II and complements EDR, DLP, network, and GRC tooling at the AI-artifact layer they were never built to see. See how it works.

You can't govern what you can't see.The Anomity principle

What to check across your fleet

  • Identify every endpoint, build agent, and CI pipeline that ran npm install on or around August 4, 2026, and check lockfiles for keyv 6.0.0, flat-cache 6.1.24, file-entry-cache 11.1.6, cacheable-request 13.0.20, cache-manager 7.2.10, and any @cacheable version listed above - these arrive transitively, so search the resolved tree, not your direct dependencies.
  • Treat any host that completed an affected install as compromised and rebuild it. The payload harvested before anyone could intervene, so cleaning the package is not remediation.
  • Rotate npm and GitHub tokens first (they drive replication), then AWS keys and session tokens, Kubernetes service-account tokens, Vault tokens, Stripe and Slack keys, SSH keys, and anything in a .env reachable from an affected host. Enable 2FA and prefer short-lived and trusted-publishing credentials over long-lived PATs.
  • Audit for AI and IDE persistence separately from dependency cleanup: search repositories and working copies for .claude/settings.json SessionStart hooks, .claude/setup.mjs, .vscode/tasks.json folderOpen tasks, and .vscode/setup.mjs. A version bump does not delete these.
  • Hunt the IOCs in the table above - setup.mjs and Math_Symbol.js / math_init.js hashes, /tmp/bun-dl-*/, the Bun/1.3.13 user-agent, and traffic to npm-cache[.]com - and search GitHub for repositories described Shai-Hulud: Here We Go Again under your org's identities, then revoke their access.
  • Review GitHub Actions workflow runs in the window for OIDC token requests and unexpected publishes, since runner memory was dumped for ACTIONS_ID_TOKEN_REQUEST_TOKEN, and check Secrets Manager and Vault access logs for enumeration.
  • Set ignore-scripts for CI installs where feasible, pin dependencies with lockfile integrity verification, and adopt a cooldown that blocks package versions published within the last 24 hours - the control that would have blunted every Shai-Hulud wave to date.
  • Inventory the hooks and agent configuration across your fleet using the hook and artifact inventory, and confirm agent tool calls are evaluated at a hook with allow/deny/log rather than trusted by default - see Claude Code permissions and hooks hardening and auditing Claude Code across a fleet.
  • Run the credential-theft path of your AI agent incident response playbook rather than improvising, and extend the same review to other AI coding tools per securing AI coding agents and CLIs.

Shai-Hulud came back a third time because the primitive still works: one maintainer account, one install hook, and a registry that runs code before anyone reads it. What is new in this wave is the target. The worm read .claude/settings.json as a secret and wrote agent hooks as persistence, which puts AI coding tools on both sides of the attack - and leaves residue that dependency remediation will report as fixed. Rotate credentials and rebuild affected hosts, then inventory the hooks and agent configuration your endpoints actually carry. If you want the argument in playable form, we built the buried-hook problem into a turn-based artillery game - clear every worm you can see and you still lose the round. For the full picture of these campaigns and the controls that hold, see the pillar guide on AI supply-chain attacks: a defender's guide, and the related Trapdoor cross-ecosystem stealer and Cline npm token theft advisories. To see which endpoints in your fleet ran an affected install and what runtime governance would deny, request early access.

Frequently asked questions

Which packages and versions were compromised in the Shai-Hulud 'Here We Go Again' attack?

The primary compromise hit the keyv and cacheable family after the maintainer's GitHub account was taken over on August 4, 2026: keyv 6.0.0, flat-cache 6.1.24, file-entry-cache 11.1.6, cacheable-request 13.0.20, cacheable 2.5.1, @cacheable/memory 2.2.1, @cacheable/utils 2.5.1, cache-manager 7.2.10, @cacheable/node-cache 3.1.2, @cacheable/net 2.1.1, and ecto 5.0.1. Because the worm replicated with stolen npm and GitHub tokens, secondary compromise spread across scopes including @onereach, @servicetitan, @or-sdk, @ornikar, @qlik, and @picsart. Published totals differ by tracker and should be treated as floors: Aikido reported at least 868 packages across 1,381 versions with more than 2 billion combined monthly installs, OX Security reported 440+ packages, and SafeDep revised upward to 1,684 poisoned versions across 420 package names. These libraries are almost always transitive dependencies, so check your resolved lockfile rather than your direct dependency list.

How did the malware execute and what did it steal?

Each poisoned package added a preinstall hook to package.json running node setup.mjs, so the payload executed automatically during npm install. setup.mjs is an obfuscated dropper that downloads the Bun runtime v1.3.13 and uses it to run a roughly 728 KB compiled bundle named Math_Symbol.js or math_init.js. That payload harvests npm tokens from ~/.npmrc, GitHub tokens including ghp_, gho_, and ghs_ forms and OIDC JWTs, AWS credentials from ~/.aws plus the EC2 instance metadata service and Secrets Manager, Kubernetes service-account tokens and namespace secrets, HashiCorp Vault tokens with full KV enumeration, and Stripe and Slack keys. In GitHub Actions it dumps runner process memory to steal ACTIONS_ID_TOKEN_REQUEST_TOKEN for OIDC token theft. A sweep of roughly 200 glob patterns collects .env files, private keys, SSH material, Terraform state, Docker configs, and IDE and agent settings including .claude/settings.json. Stolen data is RSA-encrypted and pushed to public GitHub repositories described 'Shai-Hulud: Here We Go Again'.

Did the worm really plant Claude Code and VS Code hooks, and do they execute automatically?

Yes, it planted them, and no, they do not execute automatically. The worm wrote a SessionStart hook into .claude/settings.json pointing at .vscode/setup.mjs, and an 'Environment Setup' task into .vscode/tasks.json with runOn: folderOpen pointing at .claude/setup.mjs. Both are gated by workspace trust: VS Code blocks automatic tasks in an untrusted workspace, and Claude Code applies workspace trust to repository-supplied project settings, so each requires a human to trust the folder. Those boundaries held. The risk is persistence rather than immediate execution, and it matters because the preinstall payload had already run before trust was ever relevant, and because removing the malicious package version does not delete the planted hook from a cloned repository. Dependency remediation and AI-artifact remediation are two different jobs.

Why is a version bump or npm audit fix not enough to remediate this?

Three reasons. First, execution already happened: the preinstall payload harvested credentials during install, so the correct posture for any affected host is to treat it as compromised, rotate everything reachable from it, and rebuild rather than clean. Second, the worm replicated using the npm and GitHub tokens it stole, so unrotated credentials keep the campaign alive regardless of which versions you pin. Third, the AI and IDE persistence lives outside the dependency tree entirely: .claude/settings.json, .vscode/tasks.json, and the dropped setup.mjs files remain on disk in every clone after the package is fixed, and SCA tools that read manifests and lockfiles do not inventory agent configuration. You need a separate audit pass for those files, plus fleet-wide inventory of the hooks your endpoints carry.

How does the malware find its command-and-control server?

It does not hardcode the fallback domain. The payload resolves command-and-control at runtime by calling an Ethereum smart contract named StringListStore via eth_call, which lets the operator rotate infrastructure without republishing malware and makes the C2 list resistant to takedown. The contract initially returned three domains and was later narrowed to npm-cache[.]com, registered 2026-05-22, with the request path /router on port 443. The primary exfiltration channel was not that domain at all but GitHub itself: roughly 1,300 public repositories with the description 'Shai-Hulud: Here We Go Again', each holding an RSA-encrypted credential bundle. For detection, that means blocking one domain is insufficient. Hunt the file hashes, the Bun/1.3.13 user-agent, /tmp/bun-dl-*/ directories, and unexpected public repositories under your org's identities.

What is the dead man's switch, and should it change our response?

OX Security reported a coercive branch in the payload that threatens destructive action if the stolen GitHub token is revoked, carrying the string IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients. It should not change your response. Revoke the tokens, rotate credentials, and rebuild affected hosts. Leaving attacker-held credentials valid to avoid triggering a threat string preserves the attacker's access and their ability to keep replicating through your npm and GitHub identities, which is a far more certain harm than the threat itself. Coordinate the revocation so you have rebuilt or isolated affected hosts and captured forensic evidence first, and run it as a planned incident-response sequence rather than an ad-hoc scramble.

Which controls would have reduced the impact of this campaign?

Four, in rough order of leverage. A publication cooldown that blocks npm versions published within the last 24 hours would have blunted every Shai-Hulud wave to date, since these campaigns depend on immediate mass installation. Disabling install scripts with ignore-scripts in CI removes the preinstall execution primitive entirely for builds that do not need native compilation. Short-lived and trusted-publishing credentials instead of long-lived npm and GitHub PATs limit both the value of a harvest and the worm's ability to replicate. And fleet-wide inventory of AI artifacts, especially hooks and agent configuration, closes the gap that dependency scanners leave open, so a planted SessionStart hook is a known artifact with a first-seen timestamp rather than a file waiting for someone to click Trust. Pinning with lockfile integrity verification and 2FA on maintainer accounts underpin all of it.

Ask AI about Anomity
ChatGPT Claude Perplexity Google AI Grok