Skip to content

ChainDrop keyv / cacheable npm worm

Summary

Beginning August 4, 2026, StepSecurity, Socket, Aikido, Wiz, Snyk, JFrog, SafeDep, Elastic Security Labs, and Unit 42 reported on a fast-moving npm supply-chain worm affecting keyv, the cacheable package family, and packages reachable through stolen maintainer identities. StepSecurity named the activity ChainDrop. Aikido, JFrog, and Elastic described it as Shai-Hulud activity; Socket assessed that its tradecraft closely matches Shai-Hulud but did not recover a self-identifying campaign marker from the analyzed payload.

The malicious releases add an npm preinstall hook, download Bun 1.3.13, run a heavily obfuscated second stage, harvest developer, CI/CD, cloud, package-registry, Vault, and Kubernetes credentials, and use stolen npm access or OIDC trusted publishing to republish trojanized packages. Socket also reported GitHub and DNS exfiltration plus .claude and .vscode repository hooks that can execute when source is opened without requiring npm install.

This is an active incident. SafeDep's later August 4 snapshot counted 2,234 poisoned versions across 444 package names and 12 organizations, 22 more versions than StepSecurity's 18:10 UTC snapshot while leaving the package-name count unchanged. Unit 42's August 9 response-list update separately names 483 packages and 1,675 package-version pairs. The inventories use different collection and classification windows; use their union for scoping rather than treating either count as final.

Tags

Why this matters

  • StepSecurity's 18:10 UTC revision reported 444 package names and 2,212 compromised versions observed between 09:40 and 13:20 UTC. Aikido separately reported at least 1,280 compromised packages in an earlier update. The difference reflects changing collection windows and package classification during an active incident; neither count should be treated as final.
  • StepSecurity separated 11 full worm carriers in the Jared Wray ecosystem from 433 propagated package names covering 2,201 versions. This distinction matters: the first group carried the complete propagation logic, while the second wave was republished through credentials harvested from at least a dozen unrelated organizations.
  • The initial package family sits deep in common dependency trees. Public reporting identifies keyv, cacheable-request, flat-cache, file-entry-cache, and related caching packages used transitively by developer tooling.
  • Aikido says malicious source changes were pushed to the legitimate repository and released through GitHub Actions, so affected packages could carry valid provenance. Provenance proved which workflow built the artifact, not that the source or maintainer identity was clean.
  • The payload turns credential theft into automated package propagation and adds source-repository execution paths for IDEs and AI coding agents.
  • By StepSecurity's 18:10 UTC update, npm had reverted all 11 full worm carriers to safe versions. Cleanup of the propagated wave was incomplete: @servicetitan/* and @nebula.js/* removals were underway, clean replacements existed for @thiennq/docs-viewer and @onereach/ui-components, and two reported malicious releases still held the latest tag. Registry cleanup does not remove copies already pinned in lockfiles, mirrors, caches, or artifacts.
  • StepSecurity found real execution in ten public backstage/backstage CI runs between 09:31 and 10:40 UTC. Fresh E2E scaffolding resolved a compromised transitive dependency outside the repository's committed lockfile; Bun then contacted Ethereum RPC services and npm-cache.com. StepSecurity found no evidence of long-lived credential loss in those runs because the affected workflows referenced no repository secrets, but the payload did execute and reach C2.
  • Snyk independently fetched and compared the maintainer-linked tarballs without installing them, confirmed the same 11 full-carrier releases, and published malicious-code advisory SNYK-JS-KEYV-18515941 for keyv@6.0.0. Its registry sweep also found that the other @keyv/* packages published before the payload commit did not carry the malicious lifecycle hook, preventing an overbroad all-@keyv assessment.
  • SafeDep's later registry reconstruction counted 2,234 poisoned versions under the same 444 package names across 12 organizations between 09:35 and 13:18 UTC. It found that 80% of affected names carried more than one poisoned release and 43 names carried at least 11, making package-name, lifecycle-hook, and payload-hash checks more durable than relying on an early version list.
  • SafeDep found the same 727,680-byte payload across the initial keyv/cacheable, @hubsync, and @ornikar publisher clusters, but two loader builds and different publication paths. The initial family retained valid OIDC/SLSA provenance, while the latter clusters used direct npm identities without provenance. One campaign therefore crossed both source/CI compromise and stolen-token publication paths.
  • JFrog independently recovered repository-infection and GitHub Actions secret-harvesting detail, including branch and workflow artifacts plus file hashes defenders can hunt independently of the package list.
  • Wiz's 19:50 UTC payload update found selective, C2-controlled arming of the token-revocation dead-man switch, per-host fingerprinting, a rotated exfiltration key, nearly 70% more credential-target definitions, and two prior smart-contract-resolved domains. This changes containment and scoping: responders should not assume every infected host received the same destructive command or that blocking only the currently resolved domain covers the campaign history.
  • Microsoft Threat Intelligence independently classified the payload as a Mini Shai-Hulud variant and published Defender detections and Advanced Hunting pivots. Microsoft also found that many propagated patch releases had no matching source commit, pull request, tag, or legitimate release, supporting direct tarball modification with stolen npm publication access rather than a separate source-repository compromise for every affected publisher.
  • Sonatype independently tracked 2,225 affected component versions under sonatype-2026-005579. Its August 5 snapshot is nine versions below SafeDep's later 2,234-version reconstruction, reinforcing that vendor inventories reflect different collection and classification windows rather than a stable final count. Sonatype also added Math_Init.js as a filename/case variant and explicitly recommends treating any environment that executed an affected version as potentially compromised.
  • Elastic Security Labs independently detonated the payload and observed an additional smart-contract-resolved C2 domain, awqhnjewqjkl[.]icu, alongside npm-cache[.]com. Elastic also published endpoint process and DNS hunts and identified worm-generated Git commits by the author claude / claude@users.noreply.github.com and message chore: update config.
  • Unit 42 detected ChainDrop execution in 10 distinct environments and found 453 public repositories across five accounts matching the worm's exfiltration marker and Dune-themed naming pattern. It treats those accounts as candidate, not confirmed, victims; the repositories had been removed by publication time.
  • Unit 42 tied awqhnjewqjkl[.]icu to a specific August 4 Ethereum transaction and observed the rotated domain become operational within an hour, followed by traffic involving environments on four continents. This converts the domain from a detonation-only pivot into an observed live C2 rotation while still not establishing how many connections represented successful compromise.
  • On August 9, Unit 42 updated its article with a public response list containing 483 unique package names and 1,675 listed package-version pairs. That is 39 more package names but 559 fewer version pairs than SafeDep's 444-name / 2,234-version snapshot. The difference is a scoping signal, not evidence that one list supersedes the other: preserve both lists, search their union across lockfiles, caches, mirrors, SBOMs, and built artifacts, and retain the source and retrieval time with results.
  • OX Security reported a new distribution boundary on August 9: a ChainDrop-infected repository was linked from a V.A.P.E entry in the public MCP Registry, while its referenced PyPI package remained clean. The repository carried the already known .claude/settings.json and .vscode/tasks.json execution hooks, showing that registry metadata can route users to a poisoned source tree even when package-only scanning finds nothing.
  • OX also found five GitHub repositories still serving the hook pair five days after the initial wave and said two @ornikar packages remained available for roughly 72 hours. A threat.wiki recheck early on August 10 found the exact known hook hashes in four of the five named repositories; the fifth repository had changed, and the MCP Registry API no longer returned V.A.P.E. Treat repository and registry status as volatile response snapshots, not permanent indicators.

Confidence and attribution

  • The compromise and malicious package behavior are corroborated by StepSecurity, Socket, Aikido, Wiz, Snyk, JFrog, SafeDep, Microsoft Threat Intelligence, Sonatype Research Labs, Elastic Security Labs, and Unit 42.
  • Aikido labels the wave active Shai-Hulud activity. Socket says the behavior closely matches Shai-Hulud: TruffleHog-style secret collection, maintainer-package enumeration, npm token and OIDC publication, and victim-account GitHub repositories.
  • Socket did not recover the campaign's self-identifying repository or commit markers because relevant strings were assembled at runtime. Public Shai-Hulud-derived tooling also makes copycat reuse possible. Track ChainDrop as a Shai-Hulud-lineage assessment, not confirmed TeamPCP attribution.
  • StepSecurity assesses the payload as a direct, heavily evolved descendant of Shai-Hulud 2.0 based on Bun/preinstall delivery, Runner.Worker memory scraping, npm self-republication, and GitHub exfiltration. Its Russian-locale kill switch is an operator-language clue, not sufficient actor or nationality attribution.
  • Socket and Aikido identify compromise of the Jaredwray maintainer/GitHub account as the initial high-impact access path. Maintainer and registry postmortems were not yet public at capture time.
  • JFrog treats the Shai-Hulud: Here We Go Again dead-drop description as a self-identifying campaign marker. That supports Shai-Hulud lineage, but a reusable public marker still does not establish TeamPCP operator identity.
  • Microsoft Threat Intelligence calls the payload a Mini Shai-Hulud variant. This strengthens multi-vendor lineage agreement but does not resolve whether TeamPCP, another operator, or a copycat controlled this incident.
  • Unit 42 likewise concludes that the code is clearly in the Shai-Hulud lineage but says the public May 2026 source release prevents confident TeamPCP attribution.

Reported execution chain

  1. The attacker publishes a new package version containing setup.mjs, Math_Symbol.js (also referenced internally as math_init.js), and "preinstall": "node setup.mjs".
  2. setup.mjs detects platform and architecture, including Alpine/musl, and obtains a platform-specific Bun 1.3.13 runtime from the legitimate oven-sh/bun GitHub Releases path when Bun is absent.
  3. The loader extracts Bun with system unzip, PowerShell Expand-Archive, or a JavaScript ZIP fallback, then executes the second stage under Bun.
  4. Socket describes the second stage as a roughly 728 KB bundle with polymorphic basE91-protected strings and internal modules tagged [collector], [dispatcher], [provenance], and [publish].
  5. The collector reads local credentials, environment variables, cloud metadata, managed secret stores, runner identity material, and generic token/private-key patterns.
  6. The worm calls npm identity and search endpoints, discovers packages reachable by the stolen maintainer identity, downloads clean tarballs, injects its files and lifecycle hook, bumps versions, recomputes integrity metadata, and republishes.
  7. Where trusted publishing is available, it attempts npm's OIDC token-exchange endpoint. A poisoned source tree can therefore produce a valid npm/Sigstore provenance attestation.
  8. Socket reports encrypted findings sent through attacker-created GitHub repositories and a separate DNS channel. StepSecurity further observed results-*.json staging repositories and a GitHub-token monitor that creates a delayed execution path when defenders revoke the stolen token.
  9. The source repository can receive .claude/settings.json SessionStart hooks and .vscode/tasks.json folderOpen tasks that rerun the loader when a developer or coding agent opens a clone.
  10. StepSecurity reports that the payload resolves command-and-control domains from an Ethereum mainnet contract, falls back to signed-commit searches on GitHub, and sends encrypted data to /router. A response containing a code field is passed to eval, making the channel bidirectional remote access rather than exfiltration only.

Credential and secret targets

Reported collection includes:

  • npm authentication tokens and registry identity/token endpoints;
  • GitHub CLI tokens, PATs, session material, Actions OIDC request tokens, and organization/repository secrets;
  • AWS credential chains, instance/container metadata, and Secrets Manager across regions;
  • GCP service-account private keys and Azure client secrets;
  • HashiCorp Vault tokens, including /home/runner/.vault-token, /run/secrets/VAULT_TOKEN, and VAULT_TOKEN;
  • Kubernetes service-account tokens under /var/run/secrets/kubernetes.io/serviceaccount/token;
  • generic bearer tokens, API keys, and private-key blocks discovered with a TruffleHog-style filesystem sweep.

Initial high-impact package set

Socket's ongoing list at capture time included:

Package Reported malicious version
keyv 6.0.0
cacheable 2.5.1
cacheable-request 13.0.20
flat-cache 6.1.24
@cacheable/net 2.1.1
@cacheable/node-cache 3.1.2
@cacheable/memory 2.2.1
@cacheable/utils 2.5.1
cache-manager 7.2.10
file-entry-cache 11.1.6

StepSecurity's early list also included packages in @arv-bedrock, @deliveroo, @hubsync, @onereach, @or-sdk, @ornikar, @picsart, @qlik, and @servicetitan scopes, plus ecto, pob-test-typescript-package-in-monorepo, and tslint-folder-schema. Aikido reported rapid spread into additional maintainers and organizations. Do not treat this table as complete.

StepSecurity scope update — 13:20 UTC

StepSecurity's updated incident snapshot counted 444 package names and 2,212 malicious versions during the 09:40–13:20 UTC observation window. Eleven Jared Wray ecosystem packages were full carriers; the worm republished 433 additional package names and 2,201 versions through credentials belonging to at least a dozen unrelated organizations. Many historical versions were replayed, so the version count is not a victim or execution count. Scope exposure from lockfiles and caches separately from confirmed execution of the preinstall hook.

StepSecurity technical update — 15:13 UTC

The expanded analysis places the first poisoned keyv commit (ee2681a) at 09:02:37 UTC and the repository-hook commit (d8c850c) at 09:04:30. keyv@6.0.0 was then published at 09:35 through GitHub Actions workflow run 30896232272 with a valid trusted-publishing attestation. StepSecurity observed the automated second wave beginning at 09:38:13 and continuing through 11:44.

The payload also:

  • exits when LANG indicates a Russian locale and otherwise respawns detached outside GitHub Actions;
  • writes a camouflaged <tmpdir>/tmp.dpkg_<pid>.lock state file;
  • creates results-*.json exfiltration commits under victim identities;
  • installs ~/.local/bin/gh-token-monitor.sh with a user service or macOS LaunchAgent, polls api.github.com/user every 60 seconds for 24 hours, and executes an attacker-supplied handler after token revocation;
  • resolves C2 through Ethereum and sends a gzip, AES-256-GCM, RSA-OAEP-SHA256, and base64 envelope that StepSecurity says it intercepted and decrypted in its sandbox.

StepSecurity containment update — 18:10 UTC

StepSecurity reported that npm's rolling response began with removal of cacheable-request@13.0.20 at 10:39 UTC and a keyv dist-tag rollback to 5.6.0 around 11:15. By 18:10, all 11 full carriers had been reverted to safe versions. The response was still incomplete across the worm-propagated package set:

  • @servicetitan/* and @nebula.js/* packages were being removed wholesale;
  • clean releases were available for @thiennq/docs-viewer@1.6.4 and @onereach/ui-components@27.0.4;
  • @picsart/ai-sdk@3.32.2 and @deliveroo/reevent@1.0.1 reportedly remained on latest at the capture time; and
  • the compromised maintainer account and three initially affected GitHub repositories were no longer available, limiting access to the original issue and commit history.

Treat these as a time-bounded response snapshot, not a final registry inventory. Continue using vendor-maintained affected-version lists and inspect internal registry proxies, package caches, lockfiles, and built artifacts even after public removal or dist-tag rollback.

Confirmed public CI execution — Backstage

StepSecurity searched roughly 44,000 public workflow runs from an eight-hour window for connections to npm-cache.com. Excluding five runs in its own detonation repository, it found ten matching runs in backstage/backstage. In the affected E2E jobs, fresh application scaffolding installed current dependencies outside the project's committed lockfile. The resulting Bun process contacted eth.llamarpc.com, go.getblock.io, eth-mainnet.nodereal.io, and then npm-cache.com in the same sequence observed in StepSecurity's detonation.

The runs occurred across Renovate pull requests, pushes to master, and changeset branches between 09:31 and 10:40 UTC. StepSecurity reported the finding to Backstage as issue backstage/backstage#35100. It found no evidence of long-lived credential loss: the two affected workflow definitions referenced no repository secrets, the only job credential was an ephemeral GITHUB_TOKEN, and the older audit-mode agent did not record a Runner.Worker memory-read event. This is confirmed payload execution and C2 reachability, not confirmed credential theft.

The case demonstrates a lockfile boundary: a committed application lockfile does not constrain jobs that scaffold a new project or otherwise resolve fresh dependencies during testing.

Snyk independent tarball validation

Snyk queried npm for 61 package names associated with maintainer jaredwray, checked August 4 releases, and independently confirmed the 11 full-carrier versions listed above. It found ecto@5.0.1 particularly important for scoping because it appeared after the first public warnings, while the other @keyv/* version 6 packages published between 09:30 and 09:32 predated the payload and did not contain the malicious hook.

Its file-by-file comparison of keyv@6.0.0 against keyv@6.0.0-rc.1 found the compiled dist/ tree unchanged. The material differences were the stable version metadata, setup.mjs, Math_Symbol.js, and the preinstall hook. Snyk calculated SHA-256 d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782 for the keyv@6.0.0 tarball and independently reproduced the two payload hashes below.

Snyk also clarified the commit-verification boundary. The initial poisoned release commits were reported as unsigned, while repository-hook commit d8c850c7 was GitHub-verified and used github-actions[bot] identity. A verified commit badge proves how GitHub signed that commit object; it does not prove the project authorized the change. Snyk did not execute or independently decrypt the second stage, so detailed second-stage capabilities remain grounded in the runtime and malware analyses cited above rather than in Snyk's static validation.

JFrog and SafeDep scope and infrastructure follow-up

SafeDep reconstructed 2,234 poisoned versions across 444 package names and 12 unrelated organizations between 09:35 and 13:18 UTC. This is 22 versions above StepSecurity's 18:10 snapshot but does not increase the package-name count. SafeDep reported 537 poisoned versions across 47 @ornikar/* and related names, 27 consecutive poisoned @hubsync/web-sdk-react releases, and continued republishing after initial public warnings. These are registry-observation counts, not confirmed installations or victims.

The same 727,680-byte stage-two payload, SHA-256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc, appeared under different filenames across three publisher clusters. SafeDep separated two loaders: the 29,918-byte 54dc7ea5… build in the initial keyv/cacheable family and the 11,017-byte fd3ca400… build in @hubsync, @ornikar, and the repository's IDE hooks. The initial family published through a valid trusted-publisher workflow; the other two clusters lacked provenance and used direct npm identities. This demonstrates that one payload family can arrive through both compromised source/CI and stolen registry tokens.

SafeDep also verified that the earlier @keyv/* storage-adapter releases were clean as published, agreeing with Snyk's narrower scoping. It warned that a later repository commit staged the malicious files across 19 workspaces even though only core keyv had shipped the hook, so responders should distinguish published tarballs from compromised source awaiting a possible release.

Registry state remained uneven. SafeDep reported the initial keyv/cacheable family largely unpublished or rolled back, while many @hubsync and @ornikar names still resolved latest to poisoned releases. Its statement that there was “no C2 host” is narrower than, and conflicts with, StepSecurity and JFrog's dynamic-HTTPS analysis: JFrog independently documented the Ethereum contract, selector, and /router path. Treat SafeDep's finding as no plaintext embedded host in its analyzed material, not evidence that the Ethereum-resolved HTTPS channel was absent.

JFrog added hashes for the planted repository and workflow artifacts and reported that the worm can target up to 50 writable branches per repository, skipping dependabot/* and copilot/*. It described a separate dependabot/github_actions/format/setup-formatter branch containing .github/workflows/codeql_analysis.yml, which writes the secrets context to format-results.txt and uploads it as an artifact. These artifacts should be hunted even where package-install evidence is absent.

Wiz payload-control and credential-scope update

Wiz's August 4 update provides a later payload snapshot rather than another registry count. It reported that repository-hook commits had shifted to the message chore: update config, the state-file name had become tmp.dpkg_14527.lock, and the operator had rotated the RSA key used to encrypt exfiltrated data while retaining the key used to verify GitHub-hosted fallback C2 instructions.

The dead-man switch was no longer simply armed for every infection. Wiz found that the C2 code response could select both the command and the GitHub-valid token to monitor through api.github.com/user. The payload also attached a SHA-256 host identifier built from concatenated system properties to stolen data. Wiz assesses that this supports per-host delivery of customized dead-man-switch values. Responders should still isolate and stop the implant before token revocation, but should preserve C2 responses and host identifiers to determine which systems were actually armed.

Wiz independently confirmed Ethereum eth_call resolution through a StringListStore contract and reported that its on-chain history initially returned three domains before being changed to only npm-cache[.]com. Its IOC table identifies the two historical alternatives as pypi-get[.]com and js-mirror[.]com. The contract owner's funding source had previously been flagged for scam association; that is infrastructure context, not actor attribution.

The same payload revision expanded credential-target definitions by almost 70%, according to Wiz. Newly covered stores included Claude, OpenAI, Codex, Cursor, and Gemini agent credentials; Foundry, Solana, and Monero keystores; Jenkins master.key, Argo CD, and Harbor secrets; Alibaba Cloud and Tencent Cloud CLI configurations; and /etc/shadow. This widens post-execution rotation and review beyond the cloud, GitHub, npm, Vault, and Kubernetes targets in earlier reports.

Microsoft Defender and direct-publication follow-up

Microsoft Threat Intelligence's late August 4 analysis independently reproduced the preinstall-to-Bun execution chain, cloud and secret-store API enumeration, npm token and GitHub Actions OIDC propagation, Ethereum and signed-commit C2 resolution, GitHub fallback exfiltration, and repository-hook persistence. It classified the payload as a Mini Shai-Hulud variant.

Microsoft observed that many malicious patch releases lacked a corresponding source commit, pull request, tag, or legitimate release. This supports a mixed propagation model: the initial keyv path could publish attacker-controlled source through a legitimate OIDC workflow, while many later publisher identities were used to alter and publish tarballs directly. A clean public repository is therefore not evidence that its npm artifact was clean.

Microsoft added two useful implementation details:

  • the repository path set includes .claude/setup.mjs and .vscode/setup.mjs alongside the previously reported settings and task files; and
  • one GitHub fallback path stores a stolen token under double Base64 encoding rather than the RSA/AES encrypted results envelope. Responders should not assume every value in a results-*.json repository is cryptographically protected or search only for encrypted blobs.

Published Microsoft Defender Antivirus labels include Trojan:NPM/ShaiLoader.BY, Trojan:NPM/MalBun.A, and Trojan:NPM/ShaiWorm.DAY!MTB, with behavior detections Behavior:Linux/SuspBunActivity.A and Behavior:Win32/SuspBunActivity.A. Microsoft Defender for Endpoint hunting focuses on node setup.mjs, a Node-launched bun or bun.exe under bun-dl-* or node_modules, the three known loader/payload hashes, and Bun-launched credential commands such as gh auth token, gcloud config config-helper, az account get-access-token, and azd auth token.

Sonatype affected-version and response update

Sonatype Research Labs' August 5 snapshot tracks 2,225 component versions under advisory sonatype-2026-005579. That total is lower than SafeDep's 2,234-version reconstruction despite being published later. Treat the difference as a reminder that active-incident inventories depend on observation windows, registry state, and classification rules; do not infer that nine versions were remediated or removed solely from the count change.

Sonatype independently describes the same preinstallsetup.mjs → downloaded Bun → Math_Symbol.js / Math_Init.js chain, credential discovery across files, environment variables, processes, metadata services, Kubernetes, and Vault, and automated republishing through stolen npm access. It also highlights the case-sensitive Math_Init.js spelling in addition to names already reported by other vendors.

For response, Sonatype says an environment that installed an affected release should be treated as potentially compromised. Its guidance aligns with the containment order already recorded here: isolate and preserve evidence, remove malware and persistence, then revoke and rotate exposed credentials, and rebuild from known-good components. A numerically later package release is not sufficient evidence of cleanliness.

Elastic endpoint and C2 follow-up

Elastic Security Labs' August 6 analysis independently reproduced the npm preinstall path, Bun download, cross-platform payload, credential collection, automated npm republishing, Ethereum dead-drop resolution, GitHub fallback, and Claude Code / VS Code repository hooks. Its detonation observed awqhnjewqjkl[.]icu returned through the smart-contract discovery path in addition to npm-cache[.]com. This is a new historical C2 pivot; because the contract is mutable, a domain's presence in historical telemetry is stronger evidence when correlated with Bun or Node ancestry, the contract query, or known files.

Elastic reported that the collector checks more than 300 credential patterns and highlighted AI-tool credentials for Anthropic, Claude, Codex, Cursor, OpenAI, and Gemini alongside cloud, GitHub, npm, Vault, SSH, and Kubernetes material. It also tied worm-generated repository changes to author claude / claude@users.noreply.github.com and commit message chore: update config. The author and message are hunt pivots, not proof on their own: both fields can be chosen by legitimate users or copied by another actor.

Elastic's process hunts focus on node setup.mjs, Bun command lines containing Math_Symbol.js or math_init.js, and Bun/Node DNS lookups for the Ethereum RPC providers plus the two observed C2 domains. Its reported 1.3-billion-plus aggregate monthly-download figure describes package reach, not malicious-version downloads, successful installations, or victims.

Unit 42 observed execution and C2-rotation follow-up

Unit 42's August 6 deobfuscation adds victim-side and control-plane evidence. It detected ChainDrop execution in 10 distinct environments and, at approximately 12:20 UTC on August 4, found 453 public GitHub repositories across five accounts matching the exact Shai-Hulud: Here We Go Again description and the worm's Dune-themed repository-name generator. The repositories were later removed. Unit 42 explicitly treats the five accounts as candidate rather than confirmed victims; repository creation proves attacker-pattern activity under those identities, not the initial infection path or downstream impact.

The report also identifies the exact Ethereum state-change transaction 0xc55920f1bd0531b6738153068a666c080ddded47e6256f1fd980d51c0b507c91. The contract owner rotated the active domain from npm-cache[.]com to awqhnjewqjkl[.]icu on August 4. Unit 42 says the new domain was registered at 15:15:26 UTC, first observed in use at 16:10:03 UTC, and reached by environments in North America, Europe, Asia, and Africa within roughly 19 hours. The reported addresses 104.21.91[.]101 and 172.67.215[.]154 are shared Cloudflare edge addresses; hunt or block on domain/SNI and correlated process ancestry rather than treating the IPs as attacker-owned infrastructure. Monitoring contract setter 0xd3c159e5 can provide warning of future rotations.

Unit 42 recovered a narrowly gated propagation branch for opensearch-project/opensearch-js. When GITHUB_ACTIONS is set and GITHUB_REPOSITORY and GITHUB_WORKFLOW_REF match the repository and release-drafter.yml, the code requests npm and Sigstore OIDC tokens, injects a pinned @opensearch/setup dependency into @opensearch-project/opensearch, and can publish with a genuine Fulcio/Rekor-backed SLSA statement. Unit 42 did not observe this branch execute. Treat it as implemented, reachable targeting evidence—not proof that OpenSearch was compromised or that the typosquatted dependency was published.

Its sample also narrows persistence claims. The VS Code task calls .claude/setup.mjs and reaches the dropped .claude/math_init.js; the reciprocal Claude Code hook calls .vscode/setup.mjs, but that loader looks for a missing .vscode/math_init.js, so only the VS Code path was functional in that build. The macOS LaunchAgent and Linux user-service installer was embedded but had no call site in the analyzed main path. Other reports observed or described token-monitor persistence, so responders should still hunt all artifacts while distinguishing latent code from execution evidence on each host.

OX MCP Registry and residual-repository follow-up

OX Security's August 9 response review found a V.A.P.E MCP server entry whose linked PyPI package was clean but whose source repository, jUXTAPOSITION1/V.A.P.E, contained the ChainDrop .claude/settings.json and .vscode/tasks.json hooks. The Claude SessionStart hook called node .vscode/setup.mjs; the VS Code folderOpen task called node .claude/setup.mjs. Their SHA-256 values match the JFrog-published hook indicators already listed below. This is a registry-to-repository delivery path, not evidence that the clean PyPI artifact itself was compromised.

OX named five repositories that were still distributing this configuration pair at publication time:

  • techtoboggan/claude-desktop-hardened-linux
  • rainb0w-clwn/node-cache-manager-fs-binary-ts
  • diegobbarbosa09/Automacao_swaglabs_cypress
  • evilgodfahim/kal
  • jUXTAPOSITION1/V.A.P.E

An August 10 threat.wiki API recheck found all five repositories reachable, but only the latter four still exposed both known hook files. The V.A.P.E search returned no result from the MCP Registry API. Those changes reinforce that public takedown state is transient: retain repository IDs, commit and blob metadata, audit logs, clones, and endpoint evidence rather than relying on a later URL check.

OX separately reported that @ornikar/intl-config and @ornikar/react-native-svg-transformer remained in npm for about 72 hours after the initial infection, and estimated more than 3,800 searchable results-<ID>.json credential-dump repositories across recent Shai-Hulud variants. The latter is a cross-variant structural count and should not replace Unit 42's narrower ChainDrop finding of 453 repositories across five candidate victim accounts. Repository-pattern matches also do not independently establish credential validity, unique victims, or downstream use.

August 9 affected-package inventory update

Unit 42 marked the article updated at 16:06:20 UTC on August 9 and linked a public GitHub response list. The file contains 483 unique package rows and 1,675 comma-delimited package-version pairs. It includes scoped families such as @servicetitan, @onereach, @or-sdk, @ornikar, @qlik, @nebula.js, @redhat-cloud-services, and the initial @cacheable family, plus unscoped packages.

This list expands package-name scoping beyond SafeDep's 444-name reconstruction but contains fewer version pairs than SafeDep's 2,234. Public reporting does not yet reconcile the methodology, collection time, or inclusion rules behind that difference. Responders should not subtract entries found in only one source. Store a local, timestamped copy of each vendor list and search the union; public lists can change while compromised artifacts remain in internal mirrors and caches.

Unit 42 SDLC-perspective follow-up (August 21)

Unit 42's August 21, 2026 "Connecting the Dots" post reframes ChainDrop (and the Shai-Hulud lineage) as evidence that supply-chain attack focus has shifted from hunting bugs in finished software to poisoning the digital factory that builds it — CI/CD pipelines, developer endpoints, and registries. No new ChainDrop indicators are reported; the post's durable additions are scoping and defender heuristics:

  • Three-step ChainDrop chain, restated for defenders: (1) the hook — malicious preinstall scripts download the legitimate Bun runtime to silently launch the ~727 KB obfuscated payload; (2) the theft — a hidden Python script reads live process memory from GitHub Actions runners to steal temporary OIDC tokens and secrets (not just disk files), plus a local developer-credential sweep; (3) the payload — stolen npm/GitHub tokens republish infected packages with legitimate functionality intact, with long-term persistence via cross-linked hooks in VS Code and Claude Code and C2 managed through Ethereum transactions.
  • Runner-memory scraping is the distinguishing primitive. ChainDrop's value over disk-only scrapers is that build-runner OIDC tokens and CI secrets exist transiently in process memory; defenders who hunt only file-based credential theft miss this. Correlate npm install/preinstall ancestry with Python memory-access behavior on CI runners.
  • SBOM-at-the-end-of-build is insufficient. The post's core claim: an SBOM inventory created at the finish line does not catch malware that executed during the build. Map every place third-party packages touch (developer laptops, CI/CD, cloud) and treat build-time execution as the incident class, not build-time presence only.
  • Developer endpoints lack the guardrails browsers have. npm install, pip install, cargo build, IDE extensions, and setup scripts run with the user's full permissions and no sandbox; registry and marketplace targets (cf. the earlier GlassWorm developer-botnet activity) are prime because of that, not in spite of it.
  • Named historical anchors: XZ Utils CVE-2024-3094 (years-long contributor impersonation to hide backdoors in core software), the Axios supply-chain attack (account hijack to drop malware into popular libraries), the Shai-Hulud npm worm (setup-script credential theft), and the GlassWorm developer supply-chain botnet.

Indicators and hunting pivots

Files and execution

  • setup.mjs
  • Math_Symbol.js
  • Math_Init.js
  • math_init.js
  • npm lifecycle entry "preinstall": "node setup.mjs"
  • process chain node setup.mjs spawning a downloaded bun
  • temporary paths matching bun-dl-*
  • unexpected .claude/settings.json SessionStart hooks
  • unexpected .vscode/tasks.json tasks with runOn: folderOpen
  • unexpected .claude/setup.mjs or .vscode/setup.mjs repository loaders
  • ~/.local/bin/gh-token-monitor.sh and unexpected associated user-level systemd service or macOS LaunchAgent
  • temporary files matching tmp.dpkg_<pid>.lock
  • tmp.dpkg_14527.lock in the later Wiz-analyzed payload revision
  • victim-account repositories or commits containing results-*.json
  • ~/.config/gh-token-monitor/, ~/.config/systemd/user/gh-token-monitor.service, or ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
  • unexpected GitHub Actions workflow named Run Copilot, artifact named format-results, or workflow content that writes ${{ toJSON(secrets) }} to format-results.txt
  • branch dependabot/github_actions/format/setup-formatter, workflow .github/workflows/codeql_analysis.yml, output format-results.txt, commit message Add CodeQL Analysis, or forged github-advanced-security[bot] identity
  • repository-hook commit message chore: update config
  • repository-hook author claude or claude@users.noreply.github.com, especially when paired with chore: update config and changes under .claude/ or .vscode/
  • router_runtime.js
  • unexpected @opensearch/setup dependency insertion or opensearch-project/opensearch-js release-drafter.yml OIDC publication activity; Unit 42 did not observe this targeted branch execute
  • repository commits ee2681a9b62f3637b0eb5133c36c864d3376cc5b (payload), d8c850c7800e… (IDE/agent hooks), f97eabcdd057105f1fce3f05d6c029dac3f2ac78 (evidence removal), and 174f6a55690b0812a69adef47260ba8714a9be48 (sibling staging)

SHA-256

  • d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782keyv@6.0.0 npm tarball, independently calculated by Snyk
  • fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1ebsetup.mjs
  • 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668setup.mjs tarball variant
  • 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bccmath_init.js / Math_Symbol.js
  • 927387d0cfac1118df4b383decc2ea6ba49c9d2f98b47098bcbcba1efc026e1f — planted .vscode/tasks.json, per JFrog
  • 14eb4ce01dd4307759887ff819359b70d7d9ff709ecde039a5abc1aac325b128 — planted .claude/settings.json, per JFrog
  • 3f3f42d072bd36860ab7bd7fb5e10ac0d22c741c13c89505ccd6ec0ea572eea7 — injected GitHub Actions workflow, per JFrog
  • 29ac906c8bd801dfe1cb39596197df49f80fff2270b3e7fbab52278c24e4f1a7 — runner-memory scraper, per JFrog
  • b27b82afa5f15512f3856e549fb83d873fd0049759a4b62ce64c8d7d4dc2c678 — Unit 42 TLSH-pivoted setup.mjs.malicious variant

Network and control-plane behavior

  • github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/ — legitimate Bun distribution path; validate package-install process ancestry rather than blocking blindly
  • 169[.]254[.]169[.]254 and 169[.]254[.]170[.]2 metadata access from developer or build processes
  • registry[.]npmjs[.]org/-/whoami
  • registry[.]npmjs[.]org/-/npm/v1/tokens
  • registry[.]npmjs[.]org/-/npm/v1/oidc/token/exchange/package/
  • GitHub API POST /user/repos, GraphQL createCommitOnBranch, newly created repositories, and commits from developer or CI identities
  • anomalous DNS exfiltration from package-install or Bun processes
  • Ethereum mainnet contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103, queried with eth_call selector 0x53ed5143
  • GitHub commit-search strings thebeautifulmarchoftime and IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients
  • npm-cache[.]com — C2 domain observed by StepSecurity; investigate GET /router health checks returning HTTP 400/404 and encrypted POST /router traffic
  • awqhnjewqjkl[.]icu — additional smart-contract-resolved C2 domain observed during Elastic Security Labs detonation
  • pypi-get[.]com and js-mirror[.]com — historical smart-contract-resolved domains reported by Wiz; validate current ownership and historical DNS/HTTP telemetry before blocking
  • Ethereum transaction 0xc55920f1bd0531b6738153068a666c080ddded47e6256f1fd980d51c0b507c91 — Unit 42-observed August 4 rotation to awqhnjewqjkl[.]icu
  • contract owner 0x55f9780e1492344b7417fa723aedc4d0b97f31cd and setter selector 0xd3c159e5 — monitor as control-plane pivots, not actor identity
  • 104.21.91[.]101 and 172.67.215[.]154 — shared Cloudflare edges observed for awqhnjewqjkl[.]icu; do not block or attribute these IPs without hostname context
  • Bun/1.3.13 — Wiz-reported user agent; correlate with npm install, temporary Bun-download paths, and the listed C2/RPC destinations because Bun itself is legitimate

The npm and GitHub endpoints are legitimate. Alert on unusual process ancestry, identity, volume, and timing rather than treating the domains as stand-alone malicious indicators.

Defender actions

Immediate exposure check

  1. Freeze dependency updates and block the affected package families/scopes at registry proxies while the maintainer and registry response is unresolved.
  2. Search package-lock.json, yarn.lock, pnpm-lock.yaml, package-manager caches, CI logs, SBOMs, and artifact repositories for the vendor-maintained package/version lists.
  3. Determine whether install scripts executed. A lockfile reference without installation is a different exposure class from a developer host or runner that ran the malicious preinstall hook.
  4. Search cloned repositories independently for .claude and .vscode hooks because those paths can execute without npm installation.

If an affected version executed

  1. Isolate developer hosts and runners; stop active workflows and publication paths.
  2. Preserve endpoint, CI, npm, GitHub, DNS, cloud, and registry evidence before rebuilding.
  3. Before revoking the stolen GitHub token, contain the host and remove the token-monitor persistence described above; StepSecurity reports that token revocation can trigger an attacker-supplied handler. Then revoke and replace npm, GitHub, cloud, Vault, Kubernetes, SSH, CI, and application secrets; do not rotate only npm tokens.
  4. Audit npm for unexpected versions published by affected identities and GitHub for force pushes, deleted/recreated tags, new repositories, unexpected commits, transient workflows, OIDC exchanges, and repository hooks.
  5. Rebuild affected machines/runners and dependency caches from known-clean images and commits. Roll back to verified clean package versions only after maintainer or registry confirmation.
  6. Review cloud control planes for metadata-credential use outside expected hosts, broad secret enumeration, and activity by CI identities after the first package installation.
  7. Where Microsoft Defender telemetry is available, hunt for the published malware and behavior labels plus Node-to-Bun process lineage; preserve detections and process trees before reimaging.

Preventive controls

  • Deny package install scripts by default where feasible and explicitly approve required scripts. This reduces the initial preinstall lane but does not stop malicious source hooks or runtime imports.
  • Use dependency cooldowns and tarball diffs; flag new lifecycle hooks, large obfuscated root files, runtime downloads, and sudden releases across many packages.
  • Microsoft specifically recommends npm CLI 11.10.0 or later and its min-release-age control. Treat this as a delay layer, not a substitute for install-script restrictions or artifact inspection.
  • Bind trusted publishing to protected GitHub Environments and branch rules. Provenance alone cannot distinguish a legitimate workflow building attacker-controlled source.
  • Separate untrusted pull-request workflows from release permissions, protect release branches/tags, require reviewed changes to workflow and editor/agent configuration, and monitor force pushes.
  • Restrict CI and developer egress to cloud metadata, secret stores, npm publication endpoints, GitHub repository creation, and unnecessary DNS resolvers.

Open questions

  • Final affected package/version and download scope after npm containment; SafeDep's later August 4 snapshot is 444 packages and 2,234 versions across 12 organizations, while Unit 42's August 9 response list contains 483 package names and 1,675 package-version pairs.
  • Initial access and whether the Jaredwray account, endpoint, token, GitHub session, or another upstream identity was first compromised.
  • Registry and GitHub containment actions, malicious-version removal times, and credential invalidation scope.
  • Names, visibility, and recoverable indicators for attacker-created GitHub exfiltration repositories and the DNS channel.
  • Current values and transaction history of the Ethereum C2 contract, replacement domains, and signed-commit fallback infrastructure.
  • Whether additional package, MCP, skill, extension, or AI-tool registries still point to repositories carrying ChainDrop hooks despite clean linked package artifacts.
  • Which infected hosts received selectively armed dead-man-switch instructions, what commands were returned, and whether the per-host identifier was used for other targeted tasking.
  • Whether the implemented OpenSearch-specific trusted-publishing branch ever executed; current Unit 42 evidence does not show that it did.
  • Whether ChainDrop is operated by TeamPCP, another Shai-Hulud-lineage actor, or a copycat using leaked tooling.
  • Confirmed victim execution and downstream cloud/repository compromise beyond package publication.
  • Final disposition of propagated releases, including the @picsart, @deliveroo, @servicetitan, and @nebula.js scopes, and whether clean restoration preserved or replaced package names.

Sources