What the worm does
The family is called Shai-Hulud, and it has come in waves. Palo Alto's Unit 42 traced the first, in September 2025, to a phishing campaign that spoofed npm and asked maintainers to "update" their multi-factor settings. With a stolen token, the malware logged in as the maintainer, found every package they could publish, injected itself, bumped the version and published. The infected versions ran a post-install script on whichever machine pulled them next. Hundreds of packages, including one with millions of weekly downloads, in days.
The second wave, in November 2025, moved execution to pre-install, which widened the blast radius to developer machines and CI pipelines that never finished installing. It harvested .npmrc files, environment variables, GitHub personal access tokens, AWS, Google Cloud and Azure keys and SSH keys, sent them to an endpoint the attacker controlled, and also created a public repository under the victim's own GitHub account and committed the secrets there. Unit 42 counted more than 25,000 of those repositories across about 350 accounts. It left a GitHub Actions workflow behind that registered the machine as a self-hosted runner, so the attacker could keep running commands later. If it could not steal anything, it tried to destroy the user's home directory instead.
The August 2026 variant Microsoft calls ChainDrop added the parts that matter most to a small team. It harvests credentials from GitHub Actions runner memory, not just from files. It authenticates to npm, GitHub, AWS, Kubernetes and HashiCorp Vault to enumerate and pull further secrets. And it persists by committing files into paths that developer tools execute automatically: .claude/settings.json, .claude/setup.mjs, .vscode/tasks.json. Open the project in your editor or your coding agent, and the payload restarts.
Three defaults that made it work
None of the teams involved did anything unusual. That is the point. The worm runs on defaults that every JavaScript project inherits.
Install scripts run. A package can execute arbitrary code at install time, and by default it does. The feature exists for good reasons, native builds mostly, and almost nobody needs it for the hundreds of packages in a typical lockfile.
New versions are taken immediately. A patch release published at 02:14 is in your build at 02:15. The worm's lifespan in the registry is usually hours before it is pulled, and hours is all it needs if your pipeline pulls on every commit.
The runner can see everything. A CI job needs a token to publish, a key to deploy and a credential to read the vault, so it has all three, long-lived, in environment variables. Whatever runs inside that job inherits the lot. The runner is the most privileged machine in the company and the least watched.
Nobody decided that a cache library should be able to read the deployment credentials for production. It was never a decision. The permission was inherited by whatever ran in the job, and a worm is just the first thing that bothered to ask.
Why removing the package is not the end
The package is the delivery mechanism. By the time you remove it, three things have already happened. Every secret the machine or runner could see has been read and sent out. The repository may contain new files that restart the payload the next time a human or an agent opens it. And the runner may be registered somewhere it should not be. Deleting the dependency from the lockfile addresses none of those. Microsoft's guidance is to rotate credentials from a clean host, purge package caches on endpoints and build hosts, and rebuild shared base images and golden runners from a known-good baseline. That is an incident, not a dependency update.
What to do, in order
- Set a minimum release age. The npm CLI supports it from version 12. A version must exist for a set number of days before your installs will take it. Most worm versions are pulled within that window. This is the single highest-value change and it costs nothing.
- Disable install scripts by default.
ignore-scripts=truein.npmrc, then allow the handful of packages that genuinely need them. The first time this breaks a build, you have found a package worth looking at. - Commit the lockfile and pin. No floating ranges in production builds. A lockfile that changes in a pull request is a diff someone reads.
- Shrink what the runner can see. Short-lived tokens issued per job rather than long-lived secrets in environment variables. Publish tokens scoped to one package. If a job does not deploy, it does not hold a deploy key.
- Treat editor and agent config as code. A change to
.vscode/tasks.jsonor.claude/goes through review like any other file, because it is executed like any other file. - Watch the build hosts. Outbound connections from a CI runner to an endpoint it has never spoken to before is the detection that catches a worm you have never heard of. Route it to a person.
- If you were hit: rotate everything from a clean machine, inspect the repository for files you did not write, check for self-hosted runners you did not register, purge caches, rebuild images. Then do steps one to six.
Where we fit
Critical Cloud keeps mission-critical software running, and the build pipeline is part of the software. The Code Security accelerator puts Datadog's software composition analysis and CI visibility into your pipeline in four weeks, with a findings baseline and a triage model, so a compromised dependency is a finding that reaches a named person rather than a line in a scanner report. Continuous runtime security validation, delivered with our partner Tarian Labs, tests whether the detections fire and whether the fix held. The testing is theirs. The operating, the patching and the credential rotation at 2am are ours.
We operate the stack. You own the product, including the dependencies you choose. We just make sure the ones you did not choose get noticed.
Frequently asked questions
If we removed the compromised npm package, is the incident over?
No. The package was the delivery mechanism. By the time it is removed, the payload has already read every credential it could reach on the machine or runner that installed it, sent them out, and may have committed files to the repository that restart it later. Treat every secret that machine could see as stolen, rotate from a clean host, and inspect the repository and the runner before trusting either again.
What is the single most effective change for a small team?
Stop installing packages the moment they are published. A minimum release age, which the npm CLI now supports, means a version has to exist for a set number of days before your builds will take it, and most worm versions are pulled within that window. Pair it with install scripts disabled by default and a committed lockfile, and the three defaults that made the worm work are gone.
Does Critical Cloud do dependency scanning and runtime detection?
Yes. The Code Security accelerator puts Datadog software composition analysis and CI visibility into your pipeline with a triage model, so a compromised dependency is a finding that reaches a named person. Continuous runtime security validation, delivered with our partner Tarian Labs, tests whether the detections fire and the fix holds. Patch and credential rotation work is part of Critical Support. We operate the controls; the testing is theirs.
Sources
- Palo Alto Networks Unit 42, "Shai-Hulud" worm compromises npm ecosystem in supply chain attack, September 2025, updated through December 2025, covering both waves, the stolen data and the self-hosted runner persistence.
- Microsoft Security, ChainDrop supply chain compromise: anatomy of a self-propagating worm, 4 August 2026, including the package count, the runner-memory harvesting, the editor config persistence and the recovery guidance.