keyv, a widely used key-value storage interface for Node.js applications, sits deep in the dependency graphs of countless projects. The malicious version keyv@6.0.0 alone accounted for hundreds of millions of monthly downloads. Related packages maintained by the same account—including flat-cache, file-entry-cache, cacheable-request, cacheable, cache-manager, and several @cacheable scoped packages—added hundreds of millions more. Within hours the worm had compromised hundreds of additional packages belonging to other organizations, pushing the combined exposure into the range of roughly two billion monthly installs according to multiple trackers.
The attack started early that morning UTC. An unsigned commit introduced two new files—setup.mjs and Math_Symbol.js (later seen as math_init.js in propagated packages)—along with a preinstall script in package.json that executed the loader during installation. Similar malicious commits appeared in related repositories. The attacker then published new versions through the legitimate GitHub Actions release process. Because the builds ran in the compromised but still-authorized workflows, the packages carried valid SLSA provenance and Sigstore attestations. Provenance correctly recorded the build origin; it simply could not distinguish a compromised account from a legitimate one.
Once installed, the preinstall hook runs setup.mjs. The 11 KB loader checks for an existing Bun runtime or downloads a legitimate Bun binary from GitHub releases if none is present. It then launches the heavily obfuscated second-stage payload under Bun rather than Node.js. Outside continuous-integration environments the second stage backgrounds itself, creates a single-instance lock (tmp.dpkg_14527.lock), and continues collecting data after the install process ends.
The payload’s primary goal is credential theft. It scans the filesystem for environment files, shell histories, SSH keys, npm and GitHub tokens, cloud configuration, Kubernetes secrets, HashiCorp Vault material, cryptocurrency wallets, and more—hundreds of targeted patterns across Linux, macOS, and Windows. On Linux GitHub Actions runners it attempts to read secrets from Runner.Worker process memory. It queries AWS STS, Secrets Manager, and Systems Manager Parameter Store across multiple regions, extracts Kubernetes secrets from the in-cluster API, and attempts Vault access. When a sufficiently privileged GitHub token is available it injects a temporary workflow that dumps secrets via ${{ toJSON(secrets) }} and retrieves the resulting artifact.
Stolen data is compressed, encrypted with AES-256-GCM under a random key that is itself RSA-wrapped, and exfiltrated. Primary command-and-control discovery uses an Ethereum smart-contract dead drop: the malware queries contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103 across dozens of public RPC endpoints to obtain current domain names. Healthy domains receive the encrypted payload over HTTPS (examples include npm-cache[.]com, pypi-get[.]com, and js-mirror[.]com). If that path fails, the malware falls back to creating public GitHub repositories under stolen accounts, often with the description Shai-Hulud: Here We Go Again, and uploading Base64-encoded result files. In some cases GitHub tokens themselves are relayed through commit messages for later reuse.
Propagation is automatic. Valid npm tokens with package-write and bypass_2fa privileges are used to download other packages the identity can publish, inject the same loader and second stage, replace the scripts section with a preinstall hook, increment the patch version, and republish. A parallel path writes IDE and AI-agent hooks into .vscode/tasks.json and .claude/settings.json so that opening the repository in Visual Studio Code or starting a Claude session can re-trigger the loader. The combination of install-time execution, IDE persistence, and credential-driven republishing allowed the worm to jump from one maintainer’s packages to many others within a few hours.
The scale quickly became clear. Trackers reported between roughly 400 and more than 800 unique packages and well over a thousand malicious versions at the peak of the initial wave. High-profile organizations whose packages appeared among the compromised set included names associated with Deliveroo, Picsart, Qlik, Ornikar, ServiceTitan, OneReach, and others. Some of the root packages are present in a large fraction of cloud environments, amplifying the potential impact on CI/CD pipelines and developer workstations.
Researchers noted several distinctive technical choices. Running under Bun may evade monitoring focused solely on Node.js. The use of a smart-contract dead drop for C2 domain discovery provides resilience against domain takedowns. The legitimate provenance signatures created a temporary window of trust that delayed some automated defenses. Persistence mechanisms that survive deletion of node_modules force responders to inspect developer tooling configuration as well as package lockfiles.
Response guidance from multiple firms is consistent. Organizations should audit lockfiles and installed packages for the known malicious versions, treat any machine or runner that installed them after the publication window as potentially compromised, rotate npm tokens, GitHub tokens, cloud credentials, and other secrets that may have been present, and search for the characteristic repository descriptions and commit markers. Blocking known C2 domains and monitoring for the Ethereum contract interactions can help. Tools that enforce dependency allowlists, disable lifecycle scripts where possible, or maintain an internal firewall of known-bad packages reduce exposure to the next wave.
This incident is the latest in a series of Shai-Hulud-related campaigns that have repeatedly targeted the npm registry. Earlier waves demonstrated the same core pattern of credential theft followed by automated republishing. The August 2026 event stands out for the extreme popularity of the initial packages and the speed with which the worm moved beyond a single maintainer. It underscores a structural reality of the open-source JavaScript ecosystem: a single compromised high-degree-of-connectivity account can place malicious code into the dependency trees of a large fraction of production systems within hours.
Developers and security teams continue to clean up residual malicious versions and monitor for further propagation. The fundamental challenge remains. As long as package installation executes untrusted code by default and maintainer credentials grant broad publish rights, self-propagating worms will keep finding opportunities. Stronger defaults around lifecycle scripts, more rigorous multi-factor and short-lived credential practices for publishing, and wider adoption of reproducible builds and internal mirrors offer partial defenses. The August 4 campaign showed once again how quickly those defenses can be tested—and how far the blast radius can extend when they fail.

