keyv npm Supply Chain Attack: Shai-Hulud Returns

Date Observed: August 4, 2026
Ecosystem: npm
Targets: CI/CD runners and developer workstations consuming keyv, cacheable, flat-cache, file-entry-cache, cache-manager, plus 400+ downstream packages
Attack Type: Maintainer account compromise (initial-access method unconfirmed) leading to a self-replicating credential-stealing worm
Impact: Credential theft from developer machines and CI runners, automatic republishing to every package the victim can write to

Key Takeaways

  • A compromised maintainer account was used to poison keyv@6.0.0 at 09:35 UTC on August 4, 2026. Within hours the worm had reached 444 package names.
  • The 11 initial poisoned releases were published through legitimate GitHub Actions OIDC trusted-publishing workflows and carried valid SLSA provenance attestations, allowing them to pass provenance checks.
  • The payload targets npm and GitHub tokens, cloud credentials, Kubernetes and Vault secrets, SSH keys, and AI tool credentials, then uses accessible publishing credentials to spread.
  • Exfiltration runs over GitHub-owned infrastructure and an endpoint whose address is fetched from an Ethereum smart contract.
  • A potential dead-man’s switch can trigger if a stolen GitHub token is revoked, so token rotation alone is not a safe first step.
  • The payload runs from a preinstall hook. Teams using npm 12 with its default script policy were not exposed through this install-time path, but the IDE hook files run Node directly and bypass npm entirely.

Introduction

On August 4, 2026, a self-replicating worm compromised the npm packages in the keyv, cacheable, and related ecosystems. The first poisoned release, keyv@6.0.0, went live at 09:35 UTC. Within roughly four hours, it had spread to hundreds of packages across more than a dozen separate organizations. StepSecurity named the variant ChainDrop, while other researchers identified it as the latest evolution of the Shai-Hulud npm worm first seen in September 2025. The exfiltration repositories reinforce that connection, using the description “Shai-Hulud: Here We Go Again.”

Scope and Impact of the Attack

The attacker operated under the jaredwray GitHub account. How that access was obtained has not been confirmed publicly. The account belongs to the maintainer of a set of caching libraries that sit deep in the JavaScript dependency tree. Because the account controlled several high-reach package families, the compromise allowed the attacker to poison them in rapid succession.

The confirmed first-wave versions are keyv@6.0.0, flat-cache@6.1.24, file-entry-cache@11.1.6, cacheable-request@13.0.20, cacheable@2.5.1, cache-manager@7.2.10, ecto@5.0.1, and four @cacheable/* scoped packages. These are not niche libraries. Aikido recorded keyv at 604 million monthly downloads, flat-cache at 580 million, and file-entry-cache at 571 million, and put the affected packages’ combined monthly download volume above 2 billion.

From there, the worm moved outward using stolen tokens. As of 18:10 UTC on August 4, StepSecurity had identified 444 affected package names and 2,212 malicious versions, spanning @servicetitan (141 packages), @onereach (78), @or-sdk (74), @ornikar (42), @qlik (28), and @nebula.js (22). Other researchers reported somewhat different totals as the investigation and registry cleanup continued.

How the Attack Works

Stage 0: Repository Compromise

At 09:02:37 UTC, an unsigned commit added two files to the keyv main branch: setup.mjs and Math_Symbol.js. A second GitHub-verified commit attributed to github-actions[bot] added IDE and AI-agent hook files. Release tooling was modified so the malicious build would ship as latest.

Stage 1: Install Trigger

The poisoned package.json added “preinstall”: “node setup.mjs”, causing the loader to execute during installation in environments that permit dependency lifecycle scripts. npm 12 blocks unapproved dependency install scripts by default, so the hook does not run unless the affected package’s scripts have been explicitly approved or otherwise allowed.

Stage 2: Runtime Staging

setup.mjs downloads the official Bun 1.3.13 runtime from GitHub releases, uses it to execute the 727 KB Math_Symbol.js payload, then deletes the runtime. The download comes from the official Bun GitHub release location, so a domain-reputation blocklist would see only trusted GitHub infrastructure, which is what makes the first stage hard to catch on network signals alone.

Stage 3: Collection and Spread

The second-stage payload harvests credentials, exfiltrates the encrypted results through GitHub or dynamically resolved infrastructure, and uses stolen npm and GitHub credentials to infect additional packages and repositories.

Propagation and Persistence

The worm spreads in two ways. On npm, it enumerates packages the stolen token can write to, injects the same preinstall hook into their tarballs, and republishes. JFrog found it specifically selects tokens where bypass_2fa is true and write permission exists.

On GitHub, it commits hook files to as many as 50 branches per reachable repository, including .claude/settings.json and .vscode/tasks.json. Those fire when a developer opens the folder in VS Code or starts a Claude Code session. No npm install is required.

Researchers identified a potential dead-man’s switch capability that monitors a stolen GitHub token and can execute an attacker-supplied handler when the token stops working. Socket reports that the malware writes a watcher script to ~/.local/bin/gh-token-monitor.sh, stores the stolen token and handler under ~/.config/gh-token-monitor/, and persists through a Linux user service or a macOS LaunchAgent named com.user.gh-token-monitor. The watcher polls the GitHub API every 60 seconds for up to 24 hours and triggers the handler if the token begins returning an HTTP 4xx response, including after revocation. JFrog found the same installer embedded in the payload but did not find an active call site in the execution path it analyzed, so the monitor may not be present on every infected system. Teams should isolate affected systems and check for these files and services before rotating GitHub tokens. If the monitor is present, it should be disabled and removed while the host remains isolated, followed by credential rotation from a separate, clean system.

Key Capabilities

Credential collection covers npm and package manager tokens, GitHub PATs and Actions OIDC tokens, AWS, Azure and GCP credentials including instance metadata, Kubernetes service account tokens, HashiCorp Vault tokens, Stripe and Slack keys, database connection strings, SSH private keys, and credentials for AI tools including Claude, OpenAI, Cursor, and Gemini.

On Linux GitHub Actions runners where sufficient permissions are available, the payload attempts to read Runner.Worker process memory through /proc//mem to recover masked secrets. It can also plant a workflow that serializes the secrets available to that workflow into a build artifact.

Exfiltration rides GitHub itself. Stolen data is gzipped, encrypted with AES-256-GCM, wrapped with an embedded RSA key, and committed to public repositories. A second channel uses an endpoint at npm-cache[.]com:443/router resolved through an Ethereum mainnet contract, allowing the attacker to change the endpoint without modifying the malware and making infrastructure takedowns less durable.

Why This Matters to DevOps and DevSecOps Teams

The window was small and the blast radius was not. Socket flagged the malicious hook roughly six minutes after publication. CI runners are among the worst places for this to land: they hold cloud credentials, registry tokens, and signing access, and they run without a human watching.

The IDE persistence also changes the cleanup process. A developer who never ran npm install can still be compromised by opening an affected repository. And where the dead-man’s switch is present, it arms on token revocation. The normal instinct to rotate first makes things worse.

Why Existing Controls Miss This

All 11 initial poisoned releases were published through legitimate OIDC trusted-publishing workflows and carried valid npm/SLSA provenance attestations. Provenance correctly identified the workflow and source commit used to create each release, but it could not establish that the commit itself was authorized or safe.

Known-vulnerability databases and reputation-based feeds had no prior record to match when the releases first appeared. Behavioral package scanners identified the malicious files within minutes, but builds that installed them before detection could already have executed the payload.

Much of the early activity uses legitimate GitHub infrastructure, including the Bun download, GitHub API operations, and secondary exfiltration. Other communications use public Ethereum RPC infrastructure to resolve the current C2 endpoint. Provenance checks, known-vulnerability scanning, and egress controls built only on domain reputation each pass this attack through.

What Teams Should Do Now

Treat any developer machine or CI runner that executed an affected version as compromised:

  • Check lockfiles and package inventories for confirmed affected versions, including transitive dependencies.
  • Isolate any developer machine or CI runner that executed an affected version.
  • Preserve package tarballs, npm logs, CI logs, GitHub audit logs, and runner images before cleanup.
  • Search all repository branches for the .vscode and .claude persistence files and check for the potential token-monitor artifacts.
  • If the token monitor is present, disable and remove it while the host remains isolated before revoking the monitored GitHub token.
  • Revoke and rotate credentials exposed to affected environments.
  • Rebuild affected runners and developer environments from clean images rather than simply deleting the package.

How InvisiRisk Protects Against This Attack

The InvisiRisk Build Application Firewall (BAF) applies controls at the point where this attack actually executes: inside the running build.

Stability Buffer. InvisiRisk’s Stability Buffer blocks newly published packages from entering protected builds during a configurable window that defaults to 48 hours. keyv@6.0.0 was weaponized from the moment it was published. Blocking fresh releases keeps a package that is new out of the build entirely, which is the difference between reading about this incident and responding to it.

Build Proxy / Network Interception. The InvisiRisk BAF runs as a network proxy inside the CI/CD build environment and intercepts all outbound traffic. Policy can block unexpected Bun downloads, Ethereum RPC calls, GitHub API writes, and C2 or exfiltration requests before credentials or other data leave the runner.

Unauthorized API Action Enforcement. The InvisiRisk BAF enforces policy on outbound API calls during builds. Unexpected PUT, POST, and GraphQL operations that create repositories, push commits, or write data outside the expected behavior of the build can be blocked. These are the same types of operations the worm depends on, both to exfiltrate credentials and to propagate.

Why Build-Time Defenses Matter

This campaign carried valid provenance and still executed arbitrary code inside build environments within minutes of publication. Provenance can establish where a package came from, and scanning can identify known vulnerabilities or suspicious code, but neither inherently constrains what a package is allowed to do once execution begins. Stopping a worm like this requires enforcement inside the build and on the network path the payload must use. If your pipeline cannot block an unexpected outbound write today, assume the next variant gets through.