What happened: Shai-Hulud is a self-replicating npm worm. In September 2025 it compromised 500+ packages (including CrowdStrike-scoped ones) and harvested credentials at install time. In November 2025 it came back as "The Second Coming," hit packages from Zapier, PostHog, Postman, and AsyncAPI, and published its victims' stolen secrets to 25,000+ public GitHub repositories. The lesson: every credential your process can read, your dependencies can read. The plaintext API keys in your environment variables were the prize both times.
Most breach stories are about someone getting in. Shai-Hulud is a story about what was lying around once something got in — and the answer, on hundreds of thousands of developer machines and CI runners, was everything: npm tokens, GitHub PATs, AWS and GCP keys, and the third-party API keys that power production systems.
Two waves, one design
The campaign starts with @ctrl/tinycolor, a package pulling roughly 2 million downloads a week, and spreads to 500+ packages, including packages under CrowdStrike's npm scope. A malicious postinstall script runs on install, harvests credentials from the environment, deploys TruffleHog to sweep the disk for more, writes a persistence workflow at .github/workflows/shai-hulud-workflow.yml, and exfiltrates to a webhook.site endpoint. (Unit 42's analysis, Sysdig's breakdown.)
A renewed campaign lands trojaned versions of hundreds of packages — this time including packages from Zapier, PostHog, Postman, and AsyncAPI — using payloads named setup_bun.js and bun_environment.js. Instead of a webhook, stolen secrets are published to public GitHub repositories branded "Sha1-Hulud: The Second Coming" — Wiz counted more than 25,000 such repos across roughly 350 accounts. (Datadog Security Labs, Microsoft's response guidance.)
Researchers spot modified variants testing payloads on the registry. The technique is now a template.
Why it spread: the worm loop
Shai-Hulud's engine is brutally simple. Step one: get one maintainer to install one compromised package — the lifecycle script runs before any of your code, with your user's full permissions. Step two: read the environment and sweep the disk for credentials. Step three: among the loot, find the maintainer's own