Shai-Hulud hits npm again: keyv and hundreds of packages
On 4 August 2026 Aikido Security reported an active supply chain attack on npm. The attackers took over the GitHub account of the maintainer of keyv, injected malicious code into eleven related libraries, and from there the Shai-Hulud worm spread further. The code ran on its own during npm install, pulled tokens and secrets from developer machines and CI runners, then used them to publish more infected packages.
In an update published the same day at 13:37 CEST, Aikido put the count at 434 packages and 1,381 malicious versions, together over two billion installs per month. OX Security and JFrog reported higher figures that day, up to roughly 868 packages. The investigation is still open, so those numbers will grow.
This campaign is uncomfortable for one reason: everything along the way looked correct. The package carried valid provenance. A real maintainer account published it, through a real GitHub Actions workflow. And it contained working malware. This is the third Shai-Hulud wave, after the November 2025 attack and the axios takeover in March, and every time the vector has been maintainer identity rather than a flaw in the code.
Key facts
- Entry point: the hijacked GitHub account of the
keyvmaintainer, with malicious files pushed straight tomain. - Execution: a
"preinstall": "node setup.mjs"entry, so the code ran automatically duringnpm install. - Payload: 728 KB in
Math_Symbol.js, executed through a Bun runtime downloaded from GitHub. - Target: npm and GitHub tokens, AWS credentials, Kubernetes, Vault, SSH keys,
.envfiles, IDE configuration. - Propagation: with the stolen tokens the worm published new versions of the victim's own packages and appended hooks to
.vscode/tasks.jsonand.claude/settings.json. - Exfiltration: data encrypted with RSA and pushed to public GitHub repositories described as
Shai-Hulud: Here We Go Again.
What happened in the keyv ecosystem
keyv is a library that gives you one shared interface to key-value stores. It rarely lands in a project on purpose, because it usually arrives as a dependency of something else. The attackers took over its maintainer's GitHub account, added malicious files directly to main, and published new versions to the npm registry.
The publish ran through a legitimate GitHub Actions workflow. That is why the packages received valid provenance even though the source was already poisoned. Provenance tells you where an artifact came from and what built it. It does not tell you whether the repository is in the right hands.
Eleven packages were compromised in the first wave. Monthly download figures per Aikido:
[email protected]- 604M downloads per month[email protected]- 580M downloads per month[email protected]- 571M downloads per month[email protected]- 137M downloads per month@cacheable/[email protected]- 34M downloads per month[email protected]- 30M downloads per month@cacheable/[email protected]- 28M downloads per month[email protected]- 16M downloads per month@cacheable/[email protected]- 6M downloads per month@cacheable/[email protected]- 3.7K downloads per month[email protected]- 4.5K downloads per month
This is where the campaign started, not where it ended. The worm used stolen npm and GitHub tokens to take over packages belonging to entirely different maintainers, so you cannot assess exposure by asking whether anyone on the team wrote require('keyv'). What counts is the full dependency tree, transitive and dev dependencies included.
How the malicious code ran during npm install
Two files were added to the compromised packages, setup.mjs and Math_Symbol.js, plus a single entry in package.json:
"preinstall": "node setup.mjs"preinstall runs automatically, before npm install finishes. Nobody had to open anything or launch anything by hand. Building the project was enough.
setup.mjs is a heavily obfuscated dropper. It downloaded the Bun runtime from the project's official releases (bun-v1.3.13) and used that Bun to execute the real payload from Math_Symbol.js. The payload is around 728 KB and combines three jobs: credential theft, exfiltration, and self-propagation.
Bun is not an accidental choice. It is a legitimate signed binary pulled from github.com, so controls built on domain reputation or on lists of known malicious executables have nothing to report here.
What secrets Shai-Hulud stole
The payload was written for a specific environment: a developer workstation, a CI runner, a cloud account.
npm and GitHub tokens
The malware read .npmrc, environment variables, and the GitHub CLI configuration, looking for:
- npm publish tokens,
- classic GitHub Personal Access Tokens,
- OAuth tokens,
- GitHub Apps tokens (the
ghs_prefix), - OIDC tokens used when publishing packages.
On GitHub Actions runners it also tried to read the runner process memory, to get at secrets that would have been masked in the pipeline logs.
Cloud and infrastructure credentials
On the AWS side the payload checked configuration files, environment variables, and EC2 instance and ECS container metadata, then tried to query AWS Secrets Manager and read what was stored there.
Beyond AWS it wanted:
- Kubernetes service account tokens,
KUBECONFIGfiles and namespace secrets,- HashiCorp Vault tokens,
- container registry credentials,
- Terraform state and
tfvarsfiles.
Application secrets and local files
The scanner walked the filesystem against roughly 200 patterns, skipping anything over 5 MB. The list included:
.envand.envrcfiles,- SSH keys and other private keys,
- certificates and keystores,
- Slack tokens,
- Stripe keys,
- database connection strings,
- VPN configurations,
- KeePass databases,
- VS Code and Claude Code settings.
A single install on a developer laptop or on a runner potentially hands the attacker your repositories, your cloud account, your cluster and production all at once.
How the exfiltration worked
The collected data was encrypted with RSA using the attacker's public key, then pushed to public GitHub repositories. The repositories used as drop points carried the phrase Shai-Hulud: Here We Go Again in their description. Aikido found around 1,300 of them.
That is clever, because traffic from a CI runner to github.com is normal traffic. No rule based on destination reputation will separate it from an ordinary git push.
If the GitHub upload failed, the payload fell back to npm-cache[.]com, registered on 22 May 2026, more than two months before the attack. The address was not hardcoded: it was fetched dynamically from an Ethereum smart contract at 0xE1f2395ee43e45A1556EC6438a88c31B83493103. That let the operator swap infrastructure without publishing new package versions, and it means blocking one domain settles nothing.
Why this is a worm, not a plain backdoor
Stealing credentials was only half the job.
Once it found an npm token, the malware checked which packages that token could publish to. Then, for each one:
- it downloaded the current package,
- bumped the patch version,
- added the malicious
preinstallscript, - placed the payload in the archive,
- published the new version to npm.
One npm install on the machine of someone who maintains a few dozen packages was enough to push that whole list to the registry with malware inside. That is the jump from eleven packages to several hundred in a single day.
The second mechanism ran through GitHub Apps tokens. The malware committed changes to up to 50 branches per repository and appended hooks to .claude/settings.json and .vscode/tasks.json. The code then ran when someone opened the repository in VS Code or started a Claude Code session, with no npm install involved. The commits were authored as claude, with the message chore: update config, which is exactly what it takes to slip past a quick scroll through the history.
How to check whether this affects your projects
Updating the package is not an incident response if the malicious version already ran. In that case, assume everything that was reachable from that machine has leaked.
1. Search the whole dependency tree
Go through:
package-lock.json,npm-shrinkwrap.json,yarn.lock,pnpm-lock.yaml,- generated SBOMs,
- package manager caches,
- install logs and pipeline logs.
Direct dependencies are the smallest part of the problem. Check transitive ones and devDependencies, because a package used only for linting runs its install script exactly like a production dependency. file-entry-cache sits in the ESLint tree of a very large number of projects.
2. Pause the affected pipelines
If a malicious version got in, hold the related pipelines and isolate the runners. Further builds before you clean the environment only add leaks and can carry the payload into the next artifacts.
With ephemeral runners the machine is already gone, so check what you can: which secrets, permissions and trust relationships were wired into the infected job.
3. Rotate the exposed credentials
Order matters, so start with the most urgent:
- npm tokens,
- GitHub PATs, OAuth and GitHub Apps,
- AWS keys and other cloud credentials,
- Kubernetes and Vault tokens,
- SSH keys,
- application secrets from
.envfiles, - Slack tokens and Stripe keys,
- container registry credentials.
Rotating one npm token achieves nothing. The payload searched around 200 locations and took everything it could reach in that environment.
4. Review repositories and audit logs
Look for:
- commits authored as
claudethat nobody recognises, - the message
chore: update config, - changes to
.claude/settings.jsonand.vscode/tasks.json, - package versions published without the team knowing,
- new public repositories on the organisation account,
- unusual operations performed by GitHub Apps,
- AWS Secrets Manager calls,
- reads of Kubernetes and Vault secrets.
The evidence is spread across GitHub, npm, cloud, CI and secrets manager logs. Work through them in parallel, because a trace in one place usually explains an anomaly in another.
5. Rebuild the environment from scratch
Once the malicious dependencies are gone:
- rebuild the lockfile from verified versions,
- clear the package caches,
- run the build in a fresh environment,
- rebuild container images and artifacts,
- check whether infected files reached any repository or image,
- compare the result against an earlier, trusted build.
Indicators of compromise
Aikido published the following IoCs:
- Dropper:
setup.mjs, SHA-25654dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668 - Propagating variant, SHA-256
fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb - Payload:
Math_Symbol.jsandmath_init.js, SHA-2569fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc - Fallback endpoint:
npm-cache[.]com:443/router - Ethereum RPC:
eth-mainnet.nodereal.io - C2 smart contract:
0xE1f2395ee43e45A1556EC6438a88c31B83493103 - GitHub repositories described as
Shai-Hulud: Here We Go Again
These indicators help an investigation, but no hits is not proof that nothing happened. The infrastructure rotates through a smart contract, payload variants have different hashes, and a good part of the activity runs through GitHub, a service you already have on your allowlist.
Do lockfiles, SCA and provenance protect against this
Each of these does something useful. None of them would have stopped this campaign on its own.
A lockfile keeps versions from changing without the team knowing. It does not help when it already points at a malicious version.
Classic SCA asks about CVEs. Freshly published malware has no CVE and does not need one, because the problem is not an exploitable bug but code added on purpose. A vulnerability scanner sees a clean package here.
Trusted Publishing and provenance are the right direction: fewer long-lived publish tokens, a verifiable source for the artifact. GitHub goes further and recommends WebAuthn, enforced 2FA for publishing, and retiring classic npm tokens. All of it makes sense, and all of it was bypassed by one situation: the attacker took over the maintainer account, changed the code in the repository, and let a correct workflow do the rest.
So workable protection means several layers at once:
- control over accounts and publishing processes,
- branch and CI/CD workflow protection,
- minimal token permissions,
- lockfile and SBOM analysis,
- malware detection in dependencies,
- package verification before install,
- monitoring of developer machines and runners.
How Aikido detects malicious packages
Aikido analyses lockfiles and the packages themselves, and its malware detection looks at behaviour rather than a CVE list. It flags things like:
- heavy code obfuscation,
- commands executed during install,
- unexpected communication with external servers,
- attempts to exfiltrate data,
- downloading and running further payloads.
Aikido rates this campaign as a critical issue, 100/100. It tells platform users to check the central feed, filter for malware issues, and start a scan by hand instead of waiting for the nightly cycle.
Aikido Safe Chain is a separate layer. It intercepts npm, npx, yarn, pnpm and pnpx, checks the package against Aikido Intel, and only then allows the install. It runs on developer workstations and in pipelines, which are both places where preinstall executed.
Aikido Device Protection adds visibility into what lands on the team's hardware outside application dependencies: IDE extensions, browser add-ons, locally installed packages. In a campaign that appends hooks to .vscode/tasks.json, that layer stops being optional.
How ICWT helps secure the software supply chain
As an Aikido partner in Poland we roll the platform out to cover the whole path: code, pipeline, and developer hardware.
A typical deployment covers:
- connecting repositories and configuring dependency scanning,
- finding the projects that pull affected versions directly or transitively,
- turning on malware detection and tuning it so it does not drown in noise,
- Aikido Safe Chain on developer workstations and in CI/CD,
- reviewing how npm tokens, GitHub tokens and cloud credentials are managed, and cutting workflow permissions back,
- wiring Aikido findings into incident response and into the rules for admitting new open source dependencies.
The point is to shorten the time between a malicious package going live and someone noticing it, and to stop a dependency before it executes on a laptop or in a pipeline. One more scanner producing alerts changes nothing here.
If you want to check whether the affected versions are sitting in your repositories and CI/CD environments, get in touch. We will help you assess exposure and plan supply chain protection on Aikido.
Sources
- Aikido Security, "Keyv and friends compromised in active Shai-Hulud supply chain attack"
- OX Security, "A Massive Shai-Hulud Campaign Hits npm"
- JFrog Security Research, "Major Shai-Hulud campaign strikes npm again"
- GitHub Security, "Our plan for a more secure npm supply chain"
- Aikido documentation: malware detection and Safe Chain.
FAQ
Are we safe if we do not use keyv directly?
Not necessarily. keyv and cacheable reach projects as transitive dependencies, for example through file-entry-cache in an ESLint setup. On top of that, the worm infected packages belonging to other maintainers using stolen tokens, so the list of affected packages is far wider than the original eleven. Check the full lockfile, not the entries in package.json.
Is updating the infected package enough?
No, not if the malicious version already ran. The preinstall script executed before npm install finished and could steal secrets at that point. Updating removes the problem from future builds but does not invalidate credentials that have already leaked. Those have to be rotated.
Does valid provenance mean a package is safe?
No. Provenance confirms which repository and which workflow produced the artifact. In this campaign the attacker changed the code in the real repository, then let an ordinary GitHub Actions workflow publish the package. The provenance was correct and the package contained malware.
Will a CVE scanner catch this kind of attack?
Not always. Freshly published malware has no CVE identifier, because the problem is not a bug in the code but code added on purpose. You need separate analysis of the packages themselves: obfuscation, install scripts, network traffic during installation.
Does disabling install scripts solve the problem?
It reduces the risk but does not close the case. Some legitimate packages need install scripts, and this campaign also ran from .vscode/tasks.json and .claude/settings.json, entirely outside npm install. The --ignore-scripts flag is one layer, not the whole defence.
How many npm packages were infected?
Aikido confirmed 434 packages and 1,381 malicious versions in its update of 4 August 2026 at 13:37 CEST. Other researchers, including OX Security and JFrog, reported higher figures the same day, up to roughly 868 packages. The investigation is still open, so the final number will be larger.
Related articles
The Evolution of the Threat: Why Shai-Hulud 2.0 Forces Us to Redefine Software Supply Chain Security
An analysis of the Shai-Hulud 2.0 attack on the npm ecosystem — 796 compromised packages, a dead man's switch, and how OX Security protects the software supply chain.
Cloudflare AI Gateway: logs, DLP and AI cost control
Cloudflare AI Gateway centralizes traffic from applications, agents and automations to AI models. We show how to use logs, DLP, Guardrails, caching and spend limits - and what the service does not cover.
Malware in the axios package on npm - how to secure your applications?
The main axios maintainer's account (100+ million downloads/week) was compromised - versions 1.14.1 and 0.30.4 installed a RAT trojan on macOS, Windows, and Linux. Full analysis with IOCs, remediation steps, and Aikido Safe Chain protection.