Executive Summary
On August 4, 2026 at 09:35 UTC, [email protected] was published to npm carrying a credential stealer, an npm worm, and a persistence mechanism designed to detonate during incident response.
Keyv ranks #274 by npm reach and is present in 84,759 customer environments, and the release shipped with valid GitHub OIDC provenance and a SLSA attestation from the real repository – every supply chain integrity signal said it was authentic.
Upwind is detecting this attack in customer environments right now, on runtime behavior rather than package version, so coverage did not wait on an advisory or a signature update.
What is the Keyv Compromise?
The entry point was an account and CI compromise. What follows belongs to the Shai-Hulud worm family, and the marker string is literally Shai-Hulud: Here We Go Again.
Three details separate this from previous Shai-Hulud waves:
- Three independent triggers, only one of which involves installing anything. A preinstall hook runs node setup.mjs on npm install. A .vscode/tasks.json entry with runOn: folderOpen fires when a developer opens the repository in VS Code. A .claude/settings.json SessionStart hook fires when an agent session starts in the project. This is why –ignore-scripts does not close the exposure.
- The IDE and agent hooks are cross-wired. .claude/settings.json invokes node .vscode/setup.mjs, and .vscode/tasks.json invokes node .claude/setup.mjs. Deleting one directory leaves the other able to re-trigger the whole chain.
- There is no attacker domain in the first stage at all. setup.mjs downloads the official Bun 1.3.13 release from github.com/oven-sh, then executes a 727 KB payload under it. Egress allowlists and domain reputation monitoring never fire, by design. Not having Bun installed is not a mitigation: the dropper fetches the runtime, uses it, and deletes it.
Is the Keyv Worm Actively Exploited?
Yes. This is not a vulnerability awaiting a proof of concept – it is a live, self-propagating campaign that ran inside real build pipelines and developer laptops. There is no geofence and no environment check beyond a single-instance lock file, so workstations are in scope, not only CI.
The malicious release was live for roughly two and a half hours before the latest was rolled back, and [email protected] has since been removed from the registry. That closes the window for new installs and does nothing for credentials already exfiltrated. Any host that installed it, or that opened an affected repository in VS Code or an agent session, should be treated as compromised rather than as needing an upgrade.
Which Packages Are Affected?
Confirmed malicious as published: [email protected] only. Safe versions are 5.6.0 and 6.0.0-rc.1. Both are hook-free, and the dist/ output of 6.0.0-rc.1 is byte-identical to 6.0.0, so teams that need the 6.x line can pin the release candidate rather than reverting a major version.
Eleven packages in the same family had versions published the same morning that the maintainer has since rolled back. First-party analysis of the published tarballs found the preinstall hook only in Keyv core, with the siblings clean as published but poisoned on the main branch. Anyone who built from source rather than from the registry during that window is exposed regardless.
- flat-cache 6.1.24 – roll back to 6.1.23
- file-entry-cache 11.1.6 – roll back to 11.1.5
- cacheable-request 13.0.20 – roll back to 13.0.19
- cacheable 2.5.1 – roll back to 2.5.0
- cache-manager 7.2.10 – roll back to 7.2.9
- @cacheable/utils 2.5.1 – roll back to 2.5.0
- @cacheable/memory 2.2.1 – roll back to 2.2.0
- @cacheable/node-cache 3.1.2 – roll back to 3.1.1
- @cacheable/net 2.1.1 – roll back to 2.1.0
- ecto 5.0.1 – roll back to 5.0.0
The published @keyv/* storage adapters and the Keyv 5.x line are unaffected. Cloud CI runners are the highest-value target in this incident, because they hold the short-lived credentials and OIDC tokens the worm needs to spread.
How Does the Keyv Worm Exploitation Work?
- Staging. setup.mjs detects platform and libc, downloads Bun 1.3.13 from GitHub, executes the payload through execFileSync, then deletes the runtime directory.
- Harvesting. The payload walks an OS-keyed list of 469 file globs across ~, /home/*, and /root: cloud and cluster credentials, ~/.aws/credentials, the AWS and ECS metadata endpoints, ~/.kube/config, Vault addresses including loopback, SSH host keys, GNOME and KDE keyrings, shell histories, crypto wallets, messaging application data, and AI agent credentials in ~/.claude/, ~/.codex/, ~/.cursor/, ~/.gemini/, and ~/.kiro/.
- CI secret extraction. On GitHub Actions it locates the Runner.Worker process and reads masked secrets out of its memory through /proc/<pid>/mem. Separately, it pushes a workflow disguised as CodeQL analysis on a branch disguised as Dependabot, exfiltrates every repository secret through a legitimate Actions artifact, then deletes the run and the branch to remove the evidence.
- Command and control. The C2 domain list is fetched from an Ethereum mainnet smart contract through roughly 75 hardcoded public RPC endpoints, then data is posted to https://<domain>:443/router under AES-256-GCM with the session key wrapped in RSA-OAEP. If the response contains code, the payload evaluates it.
- Fallback exfiltration. Failing that, it creates a public repository in the victim’s own GitHub account and commits results there, embedding the victim’s own token into the commit message.
- Propagation. It scans CI configuration for release tooling, mints an Actions OIDC token scoped to the npm registry, exchanges it for publishing rights, injects a malicious optionalDependencies entry into the tarball, bumps the patch version, and republishes.
How to Check if Your Systems Are Affected
- Look for the host infection marker, a lock file masquerading as a package manager lock:
ls -la "${TMPDIR:-/tmp}"/tmp.dpkg_14527.lock - Look for the payload and dropper:
find . -name 'Math_Symbol.js' -o -name 'math_init.js' -o -name 'setup.mjs' - Look for planted hooks in every repository anyone cloned or opened:
find . -path '*/.claude/settings.json' -o -path '*/.vscode/tasks.json' - Look for the worm’s injected dependency in your manifests:
grep -rn '@opensearch/setup' --include=package.json. - Check what is installed rather than what is declared: npm ls keyv
- Review CI logs from August 4, 2026 for eth_call or JSON-RPC requests to public Ethereum endpoints from a build agent, and for POST requests to a /router path
- Search your GitHub organization for new public repositories described Shai-Hulud: Here We Go Again and containing a results/ directory, and for a branch named dependabot/github_actions/format/setup-formatter
Mitigation and Patching
- Hunt the kill switch before you rotate anything. The payload installs a watcher as a macOS LaunchAgent or a Linux systemd user service that polls the GitHub API with the stolen token every 60 seconds. When that token starts returning a 4xx status, meaning you revoked it, the watcher evaluates an attacker-supplied handler. Rotation is the trigger.
- Pin to a safe version: 5.6.0, or 6.0.0-rc.1 if you need the 6.x line. Remediation here is a rollback, not an upgrade.
- Rotate broadly once the watcher is gone. GitHub PATs and App installation tokens, npm tokens, AWS keys, GCP service account keys, Vault tokens, Kubernetes service account tokens, and AI agent credentials, which are explicit targets in this payload.
- Remove both halves of the persistence. Audit .claude/ and .vscode/ in every repository anyone opened, because the hooks are cross-wired and deleting one leaves the other live.
- Rebuild rather than clean. Treat affected workstations and runners as compromised and replace ephemeral runners outright.
ls -la ~/.local/bin/gh-token-monitor.sh ~/.config/gh-token-monitor/ 2>/dev/null
# macOS
launchctl list | grep gh-token-monitor
# Linux
systemctl --user status gh-token-monitor.service
# remove it, then: loginctl disable-linger $(whoami)
ls -la "${TMPDIR:-/tmp}"/tmp.dpkg_14527.lock
npm ls keyv && npm i [email protected]Prioritize CI and build infrastructure first, since that is where cloud credentials and OIDC tokens sit and where the worm found its propagation path. Developer workstations come next, given that this campaign has no geofence and no CI-only check. Non-privileged local environments last.
Indicators of Compromise
- setup.mjs – SHA-256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668
- Math_Symbol.js – SHA-256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc, shipped identically as .claude/math_init.js
- <tmpdir>/tmp.dpkg_14527.lock
- ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/, ~/Library/LaunchAgents/com.user.gh-token-monitor.plist, ~/.config/systemd/user/gh-token-monitor.service
- Ethereum contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103, selector 0x53ed5143
- POST to a /router path over 443, or eth_call traffic from a build agent or laptop
- Repository description Shai-Hulud: Here We Go Again with a results/ directory
- Commit markers IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients and thebeautifulmarchoftime
- Commits headlined chore: update config authored as claude <[email protected]>, which causes author-based triage to misattribute them to an AI assistant
- optionalDependencies[“@opensearch/setup”] pinned to github:opensearch-project/opensearch-js#d446803f4c3bc116263faa3499a1d3f95b2825de
Domain allowlisting is not a useful detection here. The durable fingerprints are the contract address, the crypto material, the lock file, and the Ethereum RPC traffic pattern.
How Upwind Helps
The signals that catch this attack are behavioral, because every static signal pointed the wrong way: the release carried valid OIDC provenance and a SLSA attestation, the first-stage download went to github.com, there is no C2 domain to block, and two of the three triggers never touch node_modules. Upwind observes the chain as it executes – an install step or editor session spawning a child process that downloads and runs an unexpected runtime, that runtime reading credential files and querying 169.254.169.254, a build container reaching a public Ethereum RPC endpoint, and a new LaunchAgent or systemd user service appearing on a developer host. Correlated against the workload’s baseline, a pipeline that suddenly reads ~/.aws/credentials and posts to an unfamiliar /router endpoint surfaces with the full process tree and cloud identity attached. For teams scoping exposure after the fact, the same telemetry answers which environments actually executed the payload, and which credentials were resident when they did.



