The short version: the Shai-Hulud worm harvested API keys, cloud credentials, and tokens at install time, straight out of the environment variables and files where your secrets manager had just delivered them. That is the uncomfortable lesson: a secrets manager's job ends the instant it hands plaintext to your process, and a poisoned dependency runs inside that same process. The only way to survive this class of attack is to make sure the real key is never in the process at all.
On September 15, 2025, an engineer noticed something wrong with @ctrl/tinycolor, a color-parsing package with around two million weekly downloads. Within hours it was clear this was not an ordinary compromised package. It was a worm: malware that, once installed, used its victim's own credentials to infect the victim's other packages and publish them to npm automatically. No human attacker in the loop. It spread by itself.
Researchers named it Shai-Hulud, after the sandworms of Dune. The first wave compromised more than 500 packages, including modules published under CrowdStrike's npm scope. Then, in November 2025, it came back far larger. The second wave, whose payload tagged its exfiltration repositories "Sha1-Hulud: The Second Coming," exposed tens of thousands of GitHub repositories and hit packages from AsyncAPI, Zapier, PostHog, and Postman. It is one of the most consequential software supply chain attacks to date, and the mechanism is worth understanding precisely, because it invalidates a defense most teams believe they have.
What the worm actually did
The genius and the horror of Shai-Hulud is that it never needed to trick a human into running anything. It rode the npm install lifecycle itself.
1 · Install
You (or your CI pipeline) ran npm install with a compromised package somewhere in the dependency tree, often several layers deep in a transitive dependency you had never heard of. An install lifecycle script executed automatically, before any of your application code ran.
2 · Harvest
The script scanned the machine for credentials: environment variables, config files, npm tokens, GitHub personal access tokens, and AWS and Google Cloud keys. Where the OS allowed it, it ran TruffleHog, an off-the-shelf secret scanner, to find anything it had missed.
3 · Exfiltrate
Stolen secrets were serialized and shipped out. The first wave wrote a malicious GitHub Actions workflow that posted secrets to a webhook.site endpoint; the second wave pushed them to newly created public GitHub repositories for anyone to read.
4 · Replicate
Using a stolen npm token, the worm authenticated as the compromised maintainer, found their other packages, injected itself, and published new poisoned versions. Each new victim became a new spreader. That is what made it a worm rather than a one-off breach.
Sit with step two for a moment, because it is the whole point. The worm did not break your encryption. It did not crack your vault. It simply read the environment variables and files of the process it was running in, and your secrets were right there, in plaintext, because that is where every secrets tool puts them so your code can use them.
Why your secrets manager didn't stop it
This is the part that stings, so let's be exact about it. A secrets manager, whether that's HashiCorp Vault, Doppler, AWS Secrets Manager, or any of the others, does several genuinely valuable things: it centralizes secrets, encrypts them at rest, gates access with policy, gives you an audit trail, and lets you rotate on a schedule. If you don't have one, get one.
But every one of those tools works on the same model: you authenticate, it hands you the plaintext secret, and your process uses it. Whether the delivery mechanism is doppler run injecting environment variables, an AWS SDK call to GetSecretValue, or a mounted Kubernetes Secret, the end state is identical. The plaintext credential is sitting in the memory, environment, or filesystem of your running process.
Shai-Hulud ran in that exact process. A malicious dependency executes with the full permissions of the build or application that imported it. Everything your process can read, the worm can read. The secrets manager did its job perfectly and delivered the key to your workload, and that delivery is precisely what the worm harvested. The lock on the vault is irrelevant when the attacker is standing inside the room the vault opens into.
The numbers bear this out. In its State of Secrets Sprawl 2025 report, GitGuardian counted 28.6 million new leaked secrets, up 34% year over year, and found that even organizations with a dedicated secrets manager still leaked at around a 5.1% rate. A secrets manager reduces sprawl; it does not close the loop, because the loop ends at the moment of plaintext delivery.
The install-time problem is getting worse, not better
Two trends make this the defining credential-security problem of the next few years.
First, CI runners are the richest target and the softest one. They hold deploy keys, cloud admin credentials, registry tokens, and signing keys, and they run npm install against a dependency graph nobody has fully audited. When 59% of compromised machines are CI runners, the attackers have found the shortest path to the most valuable secrets in your organization.
Second, AI coding agents have made the process boundary even leakier. An autonomous agent that can install packages, run builds, and read files is a new, permissive execution context sitting on top of your credentials. Prompt-injected agents have already been shown dumping ANTHROPIC_API_KEY and GITHUB_TOKEN straight out of their environment. The environment variable that a worm reads at install time is the same environment variable an agent can be talked into printing.
The common thread is that the number of things running inside your process, with access to your process's secrets, is going up. Dependencies, transitive dependencies, build tooling, and now agents. Defending each one individually is a losing game.
The only durable fix: the key never enters the process
If the problem is that the plaintext key is present in a process full of untrusted code, the solution is to remove the plaintext key from the process. Not encrypt it better. Not rotate it faster. Remove it.
Concretely, that means putting an egress proxy between your application and the third-party APIs it calls. Your code makes its request to Stripe, OpenAI, Twilio, or SendGrid through the proxy, and the real vendor credential is injected at the last hop, on the way out, after it has left your infrastructure. Your application, its dependencies, its CI runners, and its environment variables only ever hold a short-lived, scoped, revocable token that authenticates to the proxy. The vendor key itself is never present in the process at all.
Run the Shai-Hulud playbook against that architecture and it comes up empty. The worm scans the environment and finds a KnoxCall token instead of a Stripe key. That token is scoped to specific routes, expires quickly, and can be revoked the instant anything looks wrong, without touching, or even knowing, the underlying vendor credential. The blast radius of a poisoned dependency collapses from "every key this process can see" to "one revocable token that only works through our proxy."
This is the design principle behind KnoxCall. You route your outbound API calls through KnoxCall, and it injects the real vendor key at the egress wire. Your workload never holds it, so a poisoned dependency, a compromised CI runner, or a prompt-injected agent has nothing to steal. It runs alongside your existing secrets manager rather than replacing it: keep Vault or Doppler for your database passwords and internal secrets, and let KnoxCall keep the third-party vendor keys out of the process entirely.
What to do now, in order
- Rotate anything a build touched. If any credential was ever readable by a CI pipeline or a developer machine during the affected windows, treat it as exposed and rotate it. Start with npm tokens, GitHub PATs, and cloud keys.
- Pin and lock dependencies, and disable install scripts where you can. Use lockfiles, review dependency updates, and run
npm install --ignore-scriptsin CI where your build allows it. This shrinks the install-time attack surface but does not eliminate it. - Get every plaintext third-party key out of your environment. This is the structural fix. For the vendor APIs you call, move to egress injection so the real key never enters a process again. Everything above is damage control; this is the part that makes the next worm a non-event.
Shai-Hulud will not be the last self-replicating supply chain worm. The registries are too central and the install-time execution model too permissive for this to be a one-time event. The teams that come through the next one unscathed will be the ones whose secrets were never in the room when the attacker walked in.