On this page · 15 sections
- What actually happened, and why the usual controls did not catch it
- Control 1: contain before you rotate
- Control 2: stop treating provenance as a safety signal
- Control 3: move to npm v12 and build the allowlist properly
- Control 4: close the editor and agent execution path
- Control 5: put a cooldown between publish and install
- Control 6: isolate CI tokens from the install step
- Control 7: treat pinning as temporary, and clean the source
- Control 8: build an SBOM you can actually query in an hour
- Control 9: know which numbers you can defend
- What this costs to run
- India-specific considerations
- FAQ
- How eCorpIT can help
- References
Summary. At 09:35 UTC on 4 August 2026, a compromised maintainer account published keyv 6.0.0 to npm with a preinstall hook and a 727,680-byte credential stealer. The package draws roughly 127 million weekly downloads. The poisoned release carried valid npm provenance signed by GitHub Actions, so every cryptographic check passed. Vendor counts of the resulting worm range from 400 to over 2,200 packages, and Chainguard states plainly that no source has published a complete, independently verified list. Two things about the response are counter-intuitive and matter more than the incident itself. First, the implant installs a dead-man's switch that polls GitHub every 60 seconds and executes a remote handler the moment the stolen token starts returning HTTP 4xx, which means rotating credentials before containment is what triggers it. Second, npm v12, generally available since 8 July 2026, would have blocked the preinstall path but not the editor-based one, because the payload also stages itself in repository config files that execute with no npm install at all. Sonatype identified more than 454,600 new malicious packages during 2025, with over 99% of open source malware occurring on npm. Against that, GitHub Secret Protection is $19 per active committer per month and GitHub Code Security $30, which makes tooling the cheap part. The expensive part is the nine controls below, and the order you apply them in.
This is a working note from building and hardening Node-heavy delivery pipelines, not an incident recap. If you want the blow-by-blow, our keyv npm compromise containment runbook covers it. This piece is about what to change permanently.
What actually happened, and why the usual controls did not catch it
The timeline is tight. Snyk's analysis records commit ee2681a9 preparing keyv 6.0.0 between 09:02 and 09:17 UTC, adding the lifecycle hook, the payload files and a test that executes the loader, with npm publishing the malicious version at 09:35. Two files appear in every affected package: setup.mjs at 29,918 bytes and Math_Symbol.js at 727,680 bytes, with the package.json gaining a single line, "preinstall": "node setup.mjs". Second-generation infections spread by the worm itself use the filename math_init.js instead.
The attacker got in through the maintainer's GitHub account. Aikido Security's Ilyas Makari describes the method: "The compromise was carried out by pushing malicious files directly to the main branch and then immediately cutting a new release, meaning the poisoned versions were published to npm with valid provenance signed by GitHub Actions."
Chainguard is careful about what is still unknown: "No named actor has claimed this campaign. The initial access vector is still unknown, and neither the maintainer, npm, nor GitHub has issued a public statement as of this writing." Snyk adds a note worth repeating in any internal write-up: "The evidence supports compromise of an account, credential, session, or release path. It does not identify the person operating it, and the maintainer should be treated as an incident victim."
The stolen material list is long. Aikido enumerates npm tokens from .npmrc validated live against the registry's whoami endpoint, GitHub tokens including OIDC JWTs, AWS credentials from files, environment variables and instance metadata, Kubernetes service account tokens, HashiCorp Vault tokens, Stripe and Slack tokens, plus around 200 glob patterns covering .env files, PEM keys, SSH keys, Terraform state and Docker credentials. On GitHub Actions runners it goes further, and Aikido documents that "the payload also executes a shell command that reads the runner process memory directly to dump the entire secret store".
Wiz reports the target list grew by almost 70% over earlier variants, adding AI-agent credential stores for Claude, OpenAI, Codex, Cursor and Gemini, cryptocurrency keystores, and self-hosted CI secrets including Jenkins master.key, Argo CD and Harbor.
Control 1: contain before you rotate
This is the control that reverses standard incident-response muscle memory, and three vendors reached the same conclusion independently.
Socket's guidance is explicit: "Before rotating any credential, hunt for and remove the host-level dead-man's switch. Revocation is its trigger: the watcher runs eval on a remote-supplied handler the moment the stolen token returns an HTTP 4xx."
The mechanism, again from Socket: "A watcher script at ~/.local/bin/gh-token-monitor.sh polls the GitHub API with the stolen token every 60 seconds; when the token stops working (an HTTP 4xx, the moment it is revoked or rotated), it evaluates a remote-supplied handler string, then deletes its state and exits. It also self-clears after a 24-hour TTL."
Snyk's ordered runbook puts the same step second, right after host isolation: "Hunt for gh-token-monitor persistence before revoking GitHub credentials." Chainguard is blunter about the general rule: "Do not rotate credentials before checking for persistence artifacts. No destructive trigger has been confirmed for this variant, but prior versions of this malware lineage have shipped exactly this kind of token-revocation-triggered logic."
The artifacts to remove first, per Socket, are the watcher script, ~/.config/gh-token-monitor/, the macOS LaunchAgent plist, the systemd user service (with loginctl disable-linger), and the log files in /tmp.
Write this into your incident runbook now, before you need it. The order is: isolate the host, hunt persistence, remove persistence, then rotate from a known-clean machine. Chainguard's rotation sequence is npm publish tokens, GitHub personal access and OIDC tokens, AWS access keys and session tokens, Kubernetes service account tokens, database credentials, and Vault tokens.
Control 2: stop treating provenance as a safety signal
This is the most important architectural lesson from the incident, and npm documented the limitation years before it happened.
npm's own documentation states it directly: "When a package in the npm registry has established provenance, it does not guarantee the package has no malicious code. Instead, npm provenance provides a verifiable link to the package's source code and build instructions, which developers can then audit and determine whether to trust it or not."
keyv 6.0.0 is the field demonstration. Snyk found that "The npm manifest identifies GitHub Actions as the trusted publisher for keyv@6.0.0 and links to an npm attestation for the release. The malicious source was present in the tagged repository state, so the legitimate workflow built and attested the malicious artifact."
Socket draws the conclusion in one line: "The lesson is that provenance attests build integrity, not source integrity. The npm and sigstore pipeline did exactly what it is designed to do and still produced a signed, verifiable attestation for malware, because the source it built from was already trojanized."
Snyk makes a related point about commit signatures that catches a lot of teams: the malicious commit "is cryptographically verified by GitHub and uses github-actions[bot] as its author identity. A verified badge proves that GitHub signed the commit object. It does not establish that the change was authorized by the project maintainer."
The practical change is to your policy engine. If your admission or CI policy is "allow if provenance verifies", it passed this attack. Provenance belongs in the evidence chain for forensics and for detecting unsigned or unexpectedly-sourced artifacts. It does not belong as the sole gate. Our npm provenance attestations and cooldown policy piece goes deeper on where the signal is still useful.
Control 3: move to npm v12 and build the allowlist properly
npm v12 went generally available on 8 July 2026. GitHub's changelog lists exactly what flipped:
| Default | Before v12 | In v12 |
|---|---|---|
allowScripts |
Lifecycle scripts and implicit node-gyp builds run automatically | Off; scripts no longer run unless explicitly allowed |
--allow-git |
Git dependencies resolve | none; git dependencies not resolved unless allowed |
--allow-remote |
Remote HTTPS tarballs resolve | none; remote URL dependencies not resolved unless allowed |
strict-allow-scripts |
n/a | Opt-in; turns the skip into a hard error, aimed at CI |
The default is a soft skip rather than a failure. Socket's read: "An unapproved script is skipped, npm prints a warning, and the install still succeeds." That has a nasty second-order effect the same analysis calls out: "Native modules behave differently. A skipped node-gyp build does not fail the install. It fails later at runtime, when your code tries to load a module that was never compiled."
npm's own migration advice is to allow everything you already have, then tighten. In their words, "the fastest safe migration is to allow everything currently in your tree first, then tighten later, rather than agonizing over each package and delaying rollout":
npm install
npm approve-scripts --allow-scripts-pending # review what needs scripts
npm approve-scripts --all # snapshot the current state
git add package.json
git commit -m "chore: snapshot install-script allowlist"
Two details decide whether this actually protects you. Approvals pin to the installed version by default, so npm approve-scripts keyv allows keyv@1.2.3 and not a future malicious 6.0.0, unless someone passes --no-allow-scripts-pin. And if you already set ignore-scripts=true globally, npm states that it "takes precedence and no scripts run, the allowlist does not override it", so remove it once the allowlist exists.
The packages that genuinely need scripts are predictable: node-gyp natives such as sharp, better-sqlite3, canvas, bcrypt, bufferutil and utf-8-validate, plus Cypress, Playwright, Puppeteer, Electron and Husky. npm notes that node-gyp packages "are blocked even if they have no explicit install script, because npm runs an implicit node-gyp rebuild for any package with a binding.gyp". Set strict-allow-scripts in CI so a skipped build fails the pipeline rather than the runtime. We walk through the CI side in the npm 12 install scripts approval guide.
Control 4: close the editor and agent execution path
npm v12 would have stopped the preinstall payload. It would not have stopped everything, and this is the part most coverage missed.
Snyk documents that the campaign also staged five files into the repository itself: .claude/settings.json, .claude/setup.mjs, .claude/math_init.js, .vscode/tasks.json and .vscode/setup.mjs. Socket notes this path fires when a developer or an AI coding agent opens the cloned repository, with no npm install required at all.
Socket's own analysis of npm v12, published a month before the incident, had already drawn the boundary: "The Shai-Hulud and Miasma attack waves that defined the past year ran from compromised maintainer accounts publishing through legitimate pipelines, some with valid provenance. Neither install-script blocking nor OIDC closes that entry point."
Chainguard notes the mitigation that saved a lot of people here: "VS Code blocks automatic tasks in an untrusted workspace by default, and Claude Code applies the same workspace-trust check to repo-supplied settings." That default is doing real work. Verify it is still on across your fleet, treat .vscode/ and agent config directories as executable content in code review, and add them to the paths your secret and malware scanning actually reads.
Control 5: put a cooldown between publish and install
Socket reports detecting the malicious releases an average of five minutes and 18 seconds after publication. That gap is the entire argument for a cooldown: if new versions are not installable for a short window, automated detection has time to fire before your CI pulls the package.
The registry-side controls all pre-date this incident, which is worth knowing because none of them were a response to it:
| Date | Control |
|---|---|
| 18 February 2026 | npm minimumReleaseAge and --allow-git land in npm CLI 11.10.0 |
| 22 May 2026 | Staged publishing and new install-time controls for npm |
| 25 June 2026 | Preventive account protection for high-impact npm accounts |
| 8 July 2026 | npm v12 general availability |
| 14 July 2026 | Dependabot version updates introduce a default package cooldown |
| 28 July 2026 | npm publish-time malware scanning and dual-use metadata |
| 31 July 2026 | Restriction of npm bypass-2FA granular access tokens |
Chainguard's account of its own detection makes the case: "malware and greyware scanning and cooldown periods run before any new release becomes available to customers. In this case, scanning and cooldowns were the layers that gave Chainguard the opportunity to catch this campaign."
One gap to plan around: Socket reported earlier in 2026 that unlike pnpm's implementation, npm's initial cooldown version does not include a built-in exclusion mechanism, with an open issue proposing flexible exclusions. If you need an emergency patch to bypass the cooldown, design that escape hatch yourself rather than assuming the tool has one.
Control 6: isolate CI tokens from the install step
The memory-reading behaviour on GitHub Actions runners is the detail that should change your pipeline design. A payload that dumps the runner's entire secret store makes every secret exposed to that job compromised, not just the ones the job used.
The structural fix is boring and effective. Split dependency installation into a job with no secrets at all, and pass only the resolved artifact forward. Use OIDC short-lived credentials rather than long-lived cloud keys so the blast radius expires. Scope npm publish tokens to a separate workflow that never runs untrusted dependency code. Our GitHub Actions secret isolation and runner memory analysis covers the runner-level detail, and the npm OIDC trusted publishing hardening piece covers the publish side.
Control 7: treat pinning as temporary, and clean the source
Chainguard flagged a consequence almost nobody accounted for: "The GitHub source repository remained poisoned even after these versions were restored to latest. The payload files were staged across all 19 packages in the keyv monorepo, so a release cut from that tree, even one published by the legitimate maintainer account, could still carry the payload until the source itself is cleaned. Pinning to a known clean version is only a point-in-time fix, not a permanent one."
For consumers this means a version pin buys time and nothing more. Track upstream remediation of the source tree, not just the registry, before unpinning. For anyone maintaining an internal package registry, Snyk's runbook adds the step most teams forget: "Purge affected artifacts from private registries and caches. npm removal does not delete copies already stored by an internal proxy or developer cache."
Control 8: build an SBOM you can actually query in an hour
The value of a software bill of materials is measured on the day an incident breaks. The question is always the same: which of our builds, in the last 90 days, contained package X at version Y, and which environments are they running in?
Most SBOM programmes fail that test because the artifacts are generated and then filed. The controls that make it answerable are a build-time SBOM per artifact, stored with the artifact rather than beside it; a queryable index keyed on package name and version across all builds; and a deployment mapping from artifact digest to running environment. None of that is exotic. It is a few days of pipeline work that turns a two-week manual audit into a query.
Be sceptical of the market numbers used to sell this. Sonatype's 2026 report, which does publish a methodology, states that "Throughout 2025, Sonatype identified more than 454,600 new malicious packages, bringing the cumulative total of known and blocked malware to over 1.233 million packages across npm, PyPI, Maven Central, NuGet, and Hugging Face", and that "over 99% of open source malware occurred on npm". That cumulative 1.233 million figure is regularly recycled as an npm-only, single-year number. It is neither.
Control 9: know which numbers you can defend
An honest incident report is more useful than a confident one. The package counts for this campaign do not agree:
| Source | Reported scope |
|---|---|
| Snyk | 11 malicious releases across keyv, cacheable-related packages and ecto |
| Socket | At least ten packages in the keyv and cacheable namespaces |
| Wiz | Over 400 distinct npm packages |
| Aikido | 444 packages across 1,381 versions, over 2 billion monthly installs |
| SafeDep | 2,234 poisoned versions across 444 package names |
| Chainguard | "Reported figures range from roughly 400 to over 2,200 packages, and no source has published a complete, independently verified list" |
The published file hashes conflict too. Socket and Aikido publish SHA-256 values while Wiz and Chainguard publish SHA-1 for what appear to be the same files, and Chainguard advises treating "file names and paths as the more reliable indicator until this settles".
Download counts need the same care. Aikido's figure of roughly 127 million weekly downloads for keyv is a reach measure. Snyk pulled 619,682,667 downloads for keyv from npm's API for 5 July to 3 August and then added the caveat that matters: those numbers "are measures of ecosystem reach, not counts of compromised hosts. The exposure window for each malicious version was also much shorter than one month."
Endor Labs' Kiran Raj offers the pattern that generalises beyond this one campaign: "an npm publishing token was stolen and used to push malicious versions, in most cases a CI or service-account token likely harvested from a build runner that had itself installed a poisoned dependency." That is the loop these controls are designed to break.
What this costs to run
Tooling is rarely the constraint. GitHub prices Secret Protection at $19 per active committer per month and Code Security at $30, on metered billing. For a 40-engineer team that is a known monthly number, and it buys scanning rather than the policy and pipeline work around it.
The real cost sits in three places. Migrating to npm v12 and building an allowlist across a large monorepo is measured in engineer-days, mostly spent on native modules. Splitting CI so that dependency installation runs without secrets touches every workflow file you have. And writing a containment-first incident runbook, then rehearsing it, is the item that gets deferred indefinitely because nothing is on fire. Rehearsing it is what makes the difference between a contained incident and a rotated-credential incident that triggered a dead-man's switch.
India-specific considerations
Global capability centres in India carry a particular version of this problem. A GCC typically runs shared CI infrastructure across several product lines with a single set of cloud credentials, which is exactly the topology the runner-memory dump exploits. Segmenting CI per product line is more valuable here than at a single-product company, and it is usually deferred because the shared runner pool is cheaper.
Data protection law adds a second dimension. A credential stealer that reaches a database credential is a personal data breach under the Digital Personal Data Protection Act 2023 if that database holds personal data, with the notification obligations that follow. The evidence you need to determine scope, which builds ran, which secrets that runner held, which environments used them, is the same SBOM and CI-isolation work described above. Teams that treat supply-chain security purely as an engineering concern discover the reporting requirement mid-incident.
Finally, a note on timing. Indian delivery teams working to European or North American business hours often run their heaviest CI overnight in the target timezone, which is precisely when nobody is watching the pipeline. A cooldown on new package versions is disproportionately valuable when your build window and your on-call window do not overlap.
FAQ
How eCorpIT can help
eCorpIT builds and hardens delivery pipelines for Node-heavy engineering teams, product companies and GCCs. A typical engagement starts with a dependency and CI exposure map, then covers the npm v12 allowlist migration including native modules, CI secret isolation with OIDC, registry allowlisting and cooldown policy, build-time SBOM you can query, and a rehearsed containment-first incident runbook. Our senior engineering teams are CMMI Level 5, MSME Certified and ISO 27001:2022 certified, and we design pipelines aligned with DPDP Act 2023 and SOC 2 requirements rather than claiming certification on your behalf. Send us your stack and CI platform at /contact-us/ and we will scope it. If application security is the wider need, our secure AI-assisted development and AppSec service covers the code side, and the Interop 2026 web platform developer guide is the broader engineering hub.
References
Last updated: 7 August 2026.