Threat · curated 4 Aug 2026
Worm compromises hundreds of popular npm packages | Datadog Security Labs
First reported · updated · 2 reports thehackernews.com
Coverage timeline
Why it matters
The keyv-linked npm worm shows a real-world supply-chain compromise that abuses AI coding-assistant hooks (Claude Code, VS Code tasks) to execute and propagate, exposing developers who rely on these agents to automatic code execution from poisoned dependencies.
Datadog Security Labs reports that on August 4, 2026 a threat actor compromised hundreds of npm packages — some with over 150 million weekly downloads, including keyv, cacheable, and ecto — to propagate a self-spreading backdoor similar to the Shai-Hulud worm. The injected payloads notably add Claude (.claude/settings.json) and Visual Studio Code execution hooks and payload blobs, weaponizing AI coding-agent configuration as an execution vector.
Summary
On August 4, 2026, a threat actor compromised hundreds of npm packages — some with over 150 million weekly downloads — and used them to propagate a self-spreading, credential-stealing backdoor that Datadog assesses behaves like the Shai-Hulud npm worm. The compromise began with malicious commits in the keyv repository and quickly extended to the jaredwray/cacheable and jaredwray/ecto projects and far beyond.[0][2]
The poisoned releases declare a preinstall script (node setup.mjs) that runs a first-stage Bun loader, which fetches or reuses Bun and executes a heavily obfuscated second stage. That payload harvests filesystem secrets, environment variables, GitHub Actions runner memory, and cloud/cluster secret stores (AWS, Kubernetes, Vault), then exfiltrates encrypted results to attacker infrastructure and can execute arbitrary code returned by the C2. Any workstation or CI runner that installed an affected version should be treated as credential-exposed.[0]
C2 endpoints are resolved through an Ethereum smart-contract dead drop rather than a hardcoded domain, with a signed GitHub-commit fallback for domain discovery and a public-repository fallback for exfiltration. The worm further propagates using stolen npm publishing tokens and by planting editor/agent hook files across repository branches, and in one narrowly gated path attempts to publish attacker content with genuine SLSA/Sigstore provenance.[0]
Attack chain
- Initial compromise / package poisoning: Malicious commits were pushed to the keyv, cacheable and ecto GitHub repositories, adding setup.mjs and a second-stage payload (Math_Symbol.js / math_init.js), replacing the scripts object with a preinstall of 'node setup.mjs', and publishing malicious versions (e.g. keyv@6.0.0, ecto@5.0.1, cacheable@2.5.1) through the projects' GitHub Actions release workflows.[0][2][6][8]
- Execution via lifecycle script: When a compromised package is installed under a package manager that permits dependency lifecycle scripts, npm invokes 'node setup.mjs' during preinstall, running the loader without any application importing the package.[0]
- Runtime staging under Bun: The 11 KB loader accepts an installed bun or downloads Bun v1.3.13 as an official (unverified) GitHub release asset, extracts it, and launches the second stage under Bun rather than the npm-spawned Node.js process. Outside recognized CI the second stage respawns itself detached with a recursion guard and single-instance lock.[0]
- Credential harvesting: The second stage runs fast and deep collectors against the filesystem (290 Linux/129 macOS/50 Windows patterns), process environment, 'gh auth token', GitHub Actions Runner.Worker memory via sudo python3, AWS STS/Secrets Manager/Systems Manager across 17 regions, in-cluster Kubernetes secrets, and HashiCorp Vault KV stores, and can abuse GitHub Actions workflows to reveal secret values via ${{ toJSON(secrets) }}.[0]
- C2 discovery and encrypted exfiltration: The payload resolves C2 domains via an eth_call to a smart-contract dead drop across 73 Ethereum RPC endpoints, with a signed GitHub-commit fallback; it gzip-compresses and AES-256-GCM encrypts results, wraps the key with RSA-OAEP, and POSTs the envelope, using a public GitHub repository as an exfiltration fallback when no domain is available.[0]
- Code-execution channel: After receiving HTTP 200, the primary sender parses the JSON body and passes any 'code' field directly to eval(); the body is unsigned and unsandboxed, so an operator controlling a selected domain can run arbitrary JavaScript in the Bun process.[0]
- Propagation: The worm republishes packages using stolen npm granular tokens (requiring bypass_2fa:true and write permission) by injecting setup.mjs/math_init.js and a preinstall, and separately writes .claude and .vscode hook files to up to 50 GitHub branches so the loader retriggers when a developer opens the repo or starts a Claude session.[0]
Disclosure timeline
| Date | Event |
|---|---|
| 2026-08-04 09:02:37 UTC | Earliest recovered malicious state: unsigned commit 29c773 adds setup.mjs, Math_Symbol.js, the malicious preinstall, package inclusion, an executing test, and release configuration in the keyv lineage.[0][3] |
| 2026-08-04 09:35:00 UTC | Genuine SLSA provenance for keyv@6.0.0 enters Rekor binding tag v6.0.0 and commit f97eabc to the release workflow; the confirmed malicious keyv package publishes at 09:35:00.763.[0][6] |
| 2026-08-04 10:09:44–10:14:41 UTC | Nine confirmed malicious Cacheable-family versions publish from commit 893f73f, including cacheable@2.5.1, flat-cache@6.1.24, cacheable-request@13.0.20, file-entry-cache@11.1.6 and cache-manager@7.2.10.[0][7] |
| 2026-08-04 10:28:01 UTC | Confirmed malicious ecto@5.0.1 publishes from commit 983ce1a after the same payload blobs and preinstall are added to the ecto repository.[0][8] |
| 2026-08-04 | Datadog Security Labs publishes the incident article and adds an analysis of the payload.[0] |
| 2026-08-05 | Datadog updates the entry, adding a new C2 domain (awqhnjewqjkl[.]icu) recently added to the smart contract.[0] |
How it works
The worm abuses npm dependency lifecycle scripts. Poisoned packages replace the entire scripts object with {'preinstall':'node setup.mjs'}, so installing the package under a client that permits lifecycle scripts runs the loader during installation without any code importing the package. The 11 KB, lightly obfuscated setup.mjs either accepts an already-installed bun or downloads Bun v1.3.13 from an official oven-sh GitHub release, extracts it (preferring system unzip, then PowerShell Expand-Archive, then an internal parser), makes it executable (0755), and launches a second stage under Bun — evading monitoring limited to Node.js internals. The asset is not cryptographically verified.[0]
The second stage begins with '// @bun @bun-cjs' and is heavily obfuscated (a 4,613-entry Base91 string table, 1,283-element constant array, 73 unique Base91 alphabet permutations, generator state machines). It backgrounds itself outside CI using detached respawn with a _NODE_RUNTIME_INIT=1 recursion guard and a tmp.dpkg_14527.lock single-instance lock. A dead 'Exiting as russian language detected!' branch always resolves false, so it is not an active geofence.[0]
C2 is resolved via an Ethereum smart-contract dead drop: the payload makes an eth_call (selector 0x53ed5143) to contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103 across 73 RPC endpoints and decodes an ABI-encoded array of domains, probing HTTPS /router on port 443 (only status 400/404 counts as healthy). Data is gzip-compressed, encrypted with AES-256-GCM, the AES key wrapped with RSA-OAEP/SHA-256 using an embedded 4,096-bit key, and POSTed with a 32-character host fingerprint. A C2 response 'code' field is passed to eval(), making the implant a general unsandboxed code-execution channel.[0]
When no primary domain is available, the payload falls back to GitHub: it validates a ghp_/gho_ token, confirms public-repo creation capability, and creates a public repository (description 'Shai-Hulud: Here We Go Again') where it writes Base64-encoded result files under results/, splitting objects over 30 MiB. For users in no GitHub organizations, it embeds the double-Base64-encoded token in results and commit messages, allowing the exfiltration repository to double as a token relay.[0]
Affected versions and patch status
| Product | Affected | Patch status |
|---|---|---|
| keyv (npm) | 6.0.0 (malicious preinstall payload) | Malicious version published 09:35 UTC Aug 4; the GitHub release object was created/published after the npm artifact and the v6.0.0 tag was deleted and recreated multiple times. Datadog maintains a dynamic list and CSV of compromised packages/versions.[0] |
| Cacheable family (npm) | @cacheable/net@2.1.1, @cacheable/node-cache@3.1.2, cacheable@2.5.1, flat-cache@6.1.24, cacheable-request@13.0.20, @cacheable/memory@2.2.1, file-entry-cache@11.1.6, @cacheable/utils@2.5.1, cache-manager@7.2.10 | Confirmed malicious versions published from commit 893f73f; consult Datadog's maintained list for status.[0] |
| ecto (npm) | 5.0.1 | Confirmed malicious version published from commit 983ce1a.[0] |
| Hundreds of additional npm packages across many namespaces (e.g. @onereach/*, @or-sdk/*, @ornikar/*, @servicetitan/*, @nebula.js/*, @qlik/*) | Numerous specific versions enumerated in Datadog's compromised-package list and CSV | Situation evolving; Datadog maintains a dynamic list and downloadable CSV of malicious packages and versions.[0][10] |
Indicators of Compromise
| Type | Indicator | Context |
|---|---|---|
| file-hash | 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc | SHA-256 of the stage-two payload, shipped as math_init.js or Math_Symbol.js.[0] |
| file-hash | fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb | SHA-256 of the embedded and propagated 11 KB first-stage loader setup.mjs.[0] |
| file-hash | 927387d0cfac1118df4b383decc2ea6ba49c9d2f98b47098bcbcba1efc026e1f | SHA-256 of the injected .vscode/tasks.json hook file.[0] |
| file-hash | 14eb4ce01dd4307759887ff819359b70d7d9ff709ecde039a5abc1aac325b128 | SHA-256 of the injected .claude/settings.json hook file.[0] |
| file-hash | 3f3f42d072bd36860ab7bd7fb5e10ac0d22c741c13c89505ccd6ec0ea572eea7 | SHA-256 of the injected GitHub workflow used for secret exfiltration.[0] |
| file-hash | 29ac906c8bd801dfe1cb39596197df49f80fff2270b3e7fbab52278c24e4f1a7 | SHA-256 of the embedded Python program piped into sudo python3 to read Runner.Worker memory.[0] |
| file-hash | 619c56acf572df75b6004a6fc013c80900316a76099b241d64312da3a44f10b4 | SHA-256 of the token-monitor installer.[0] |
| other | 0xE1f2395ee43e45A1556EC6438a88c31B83493103 | Ethereum smart-contract dead-drop queried to obtain C2 exfiltration domains.[0] |
| other | 0x53ed5143 | eth_call selector used against the dead-drop contract.[0] |
| domain | awqhnjewqjkl.icu | C2 exfiltration host (/router on port 443); added to the smart contract and noted in the Aug 5 update.[0] |
| domain | pypi-get.com | C2 exfiltration host (/router on port 443).[0] |
| domain | js-mirror.com | C2 exfiltration host (/router on port 443).[0] |
| domain | npm-cache.com | C2 exfiltration host (/router on port 443).[0] |
| other | thebeautifulmarchoftime | GitHub commit-search query used for signed fallback domain discovery; trailing space is significant.[0] |
| other | thebeautifulsnadsoftime | Marker parsed from signed fallback commit messages, preceding a Base64-encoded domain and RSA signature.[0] |
| other | IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients | GitHub token-relay marker; public commits containing it can carry a reusable double-Base64-encoded token.[0] |
| other | Shai-Hulud: Here We Go Again | Description applied to the randomized Dune-themed public GitHub repository created for exfiltration.[0] |
| file-path | tmp.dpkg_14527.lock | Single-instance lock file created under the OS temporary directory by the second stage.[0] |
| other | _NODE_RUNTIME_INIT=1 | Environment variable set as a detached-child recursion guard when backgrounding.[0] |
| file-path | ~/.local/bin/gh-token-monitor.sh | Dormant token-monitor script path installed by the payload.[0] |
| file-path | ~/Library/LaunchAgents/com.user.gh-token-monitor.plist | macOS LaunchAgent persistence path for the dormant token monitor.[0] |
| file-path | ~/.config/systemd/user/gh-token-monitor.service | Linux systemd user service persistence path for the dormant token monitor.[0] |
| file-path | .claude/settings.json | Repository hook installing a SessionStart hook that runs node .vscode/setup.mjs, retriggering the loader on a Claude session.[0] |
| file-path | .vscode/tasks.json | Repository hook running node .claude/setup.mjs when a folder opens in VS Code.[0] |
| file-path | package/math_init.js | Path where propagation copies the running second stage into a victim npm tarball.[0] |
Key takeaways
- Genuine SLSA/Sigstore provenance faithfully bound the malicious source and attacker-controlled build to the published package; provenance is valuable evidence of where and how a package was built, not a safety verdict, so consumers must still inspect the referenced source and workflow.[0]
- The worm decouples C2 from fixed infrastructure by resolving destinations from an Ethereum smart-contract dead drop with signed GitHub and public-repository fallbacks, making simple domain blocklists insufficient and enabling resilient, updatable C2.[0]
- An unsigned, unsandboxed C2 response handled via eval() turns the credential-stealer into a general arbitrary-code-execution channel, so the absence of a hardcoded wiper does not limit an operator from destructive actions using collected credentials.[0]
- The campaign carries multiple execution and propagation paths — npm preinstall scripts, stolen npm-token republishing, and Claude Code/VS Code workspace hooks across branches — extending risk from package installation to merely opening a checked-out repository in an editor or AI-agent context.[0]
Defensive actions
- Treat any workstation, container, or CI runner that installed an affected package version as credential-exposed and rotate exposed secrets.: The payload harvests filesystem secrets, environment variables, GitHub Actions runner memory, and AWS/Kubernetes/Vault secret stores, so any execution implies broad credential compromise.[0]
- Assess exposure using exact package names and resolved versions from lockfiles against Datadog's maintained compromised-package list/CSV, not current 'latest' tags.: Registry tags and GitHub tags/releases changed repeatedly during the incident (the keyv v6.0.0 tag was deleted for the fourth observed time), so a GitHub ReleaseEvent is not a reliable npm publication marker.[0][10]
- Restrict or disable npm dependency lifecycle scripts and prefer clients or an install-time firewall that blocks known-malicious packages.: The worm executes solely through a preinstall script; Datadog notes its supply-chain firewall (SCFW) can proactively block known malicious packages at installation time.[0][11]
- Do not automatically trust checked-out worm-affected repositories in VS Code or Claude Code.: Injected .vscode/tasks.json and .claude/settings.json hooks run setup.mjs on folder open or session start, retriggering the loader from a repository even without an npm install.[0]
- Hunt for the documented network and behavioral indicators, including the Ethereum dead-drop contract/selector, the four C2 /router domains, the tmp.dpkg_14527.lock and _NODE_RUNTIME_INIT=1 markers, and the dormant gh-token-monitor persistence paths; remove the token monitor before rotating tokens.: These artifacts identify infected hosts and C2 activity, and the token-monitor watcher can fire on revocation, so it should be removed prior to credential rotation.[0]
- Audit GitHub Actions for the injected workflow path (dependabot/github_actions/format/setup-formatter branch and .github/workflows/codeql_analysis.yml) and residual audit-log, workflow-run, and artifact records.: The payload creates a temporary branch and workflow that assigns ${{ toJSON(secrets) }} to an artifact to exfiltrate secret values, and cleanup may leave recoverable records even after the branch is deleted.[0]
Changelog
- Detailed payload analysis from Datadog replaces the earlier third-party summary: the C2 mechanism is now established as an Ethereum smart-contract dead drop (contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103, selector 0x53ed5143) resolving domains, with signed GitHub-commit and public-repository fallbacks, rather than an unspecified compiled bundle.[0]
- Named C2 exfiltration infrastructure identified: pypi-get[.]com, js-mirror[.]com, npm-cache[.]com and (added Aug 5) awqhnjewqjkl[.]icu, each on /router port 443.[0]
- New file, key and behavioral IOCs added, including SHA-256 hashes for the stage-two payload, loader, injected hooks, workflow, runner-memory Python and token-monitor installer; exfiltration/verification RSA key fingerprints; and markers tmp.dpkg_14527.lock, _NODE_RUNTIME_INIT=1, thebeautifulmarchoftime, and the token-relay string.[0]
- C2 responses are confirmed to be handled via eval(), establishing an unsandboxed arbitrary-code-execution channel not detailed in the prior version.[0]
- A precise UTC timeline of the keyv, cacheable and ecto compromises and specific confirmed malicious versions (keyv@6.0.0, ecto@5.0.1, nine Cacheable-family versions) is now documented, along with a maintained compromised-package list and CSV.[0][10]
- Propagation mechanics clarified: stolen npm granular tokens require bypass_2fa:true and the registry publish disables certificate verification (rejectUnauthorized:false); a narrowly gated provenance path targets @opensearch-project/opensearch via Sigstore/Rekor.[0]