Shai-Hulud 2.0: the worm came back, faster, and it has Zapier

Three months after the original Shai-Hulud npm worm, a new wave hit in November 2025. It runs earlier in the install lifecycle, harvests more credentials, and produced over 25,000 malicious GitHub repositories. Zapier, PostHog, and Postman were among the named victims.

TL;DR

The Shai-Hulud worm that hit npm in September 2025 is back. The November 2025 wave, which Unit 42 is calling Shai-Hulud 2.0, is faster, more automated, and broader in scope. It moved execution earlier in the install lifecycle (from postinstall to preinstall, which gives it more reliable execution). It also widened the credential harvesting scope. And the propagation produced over 25,000 malicious GitHub repositories across about 350 distinct user accounts.

Among the named victims of the new wave: maintainer accounts for Zapier, PostHog, and Postman. These are large publishers. Their packages sit under a lot of automation, analytics, and API tooling.

What changed since September

Three months ago, I wrote up the original Shai-Hulud. The pattern then was: stolen credentials → public GitHub repos named “Shai-Hulud” → self-propagation via stolen npm tokens. The 2.0 wave keeps that pattern but tightens it in several ways.

Preinstall, not postinstall

The original variant ran in the postinstall lifecycle phase. That phase runs after npm has finished installing the package, which is usually after the rest of the dependency tree has been resolved. Some hardening setups skip postinstall scripts globally (--ignore-scripts or npm config set ignore-scripts true).

The 2.0 variant moved to preinstall. That fires before the package contents are even fully extracted to disk. It also gives the script earlier access to the running npm process, which matters for some of the credential-grabbing steps. Per the Microsoft Security Blog, this also means more environments with partial --ignore-scripts configurations are still vulnerable, because their script blocking only covers postinstall.

Broader credential scope

The September variant focused on npm tokens and a handful of cloud-credential paths. The 2.0 variant goes wider: it scans for GitHub Actions secrets, Vercel deployment tokens, more cloud-provider credentials, and credentials in package-manager configs like .npmrc / .yarnrc / pip.conf / .gemrc. Datadog’s analysis goes into the specific paths and patterns.

Faster propagation

The original variant published one or two malicious versions per compromised account before being detected. The 2.0 variant publishes many in parallel: within hours of a successful credential theft, the worm has already pushed malicious versions into other packages controlled by the same maintainer. Several of the early-November infections went from initial publish to fifteen-plus downstream packages compromised in under three hours.

Aggressive repo creation

The exfiltration GitHub repos are not just data dumps. The 2.0 variant creates many repos per compromised account, with patterns that make them harder to take down all at once. The 25,000+ figure cited by Unit 42 reflects this fan-out: many of those repos correspond to a single underlying credential theft.

Who got hit

The maintainer accounts publicly named so far include:

  • Zapier: at least one maintainer account with publish rights to packages in the @zapier scope. Zapier’s product is integration plumbing, which means their npm packages are dependencies in a long list of automation workflows.
  • PostHog: a maintainer account with publish rights to several posthog-js packages and adjacent SDKs. PostHog ships analytics that lands in many web apps.
  • Postman: a maintainer account with access to packages in the @postman scope, including SDK and CLI packages used by API teams.

The pattern is clear enough: the worm moved from popular mid-tier packages to publisher accounts with thousands of dependent applications downstream. These three vendors are not the only ones; they are the ones whose names have been confirmed publicly so far.

Critically, in each case, the vendor accounts did not have a security failure in the sense that their corporate practices were lax. The compromise was an individual maintainer account, and from there the worm jumped to organization-owned packages because the maintainer had publish scope. This is a real argument for narrower scoping of automation tokens.

Defenses revisited

When I wrote about the original Shai-Hulud, I listed two categories of defense: detection time and release-age cooldowns. Three months later, those still look like the most effective levers, but the way they apply has shifted.

Detection: faster, but the worm is faster too

Socket, Wiz, Snyk, Sysdig, Sonatype, and a half-dozen smaller scanners now have specific Shai-Hulud signatures. The 2.0 variant was flagged by several scanners within the first hour of the new pattern appearing. That is good.

But the propagation is also faster. Once a single credential is stolen, the next wave of malicious publishes can land in under a minute. The “we caught it within an hour” wins of the scanners do not fully close the gap when the worm gets fifteen packages out per hour.

Release-age cooldowns: more urgent

Three months ago, this was a “consider setting up”. Today, it is closer to a “you should already have this”.

  • pnpm has minimumReleaseAge (in minutes).
  • bun has install.minimumReleaseAge (in seconds).
  • uv has exclude-newer (in days).
  • npm did not yet have a native release-age setting when this wave hit. There was active discussion in the RFC repo, and the maintainers were signaling that it was on the roadmap.

The Datadog post linked above makes the case for cooldowns more forcefully than I did in September: a four-day hold puts you outside the window for essentially every published Shai-Hulud variant so far. The worm needs to land and be installed inside the detection window to do damage. Cooldowns invert that: you only install once detection has had time to do its job.

Trusted publishing with OIDC

In September, this was beta. In November, npm generally available trusted publishing for npm for any package that opts in. The mechanic is that publishes happen via a short-lived OIDC token issued per workflow run, instead of via long-lived automation tokens stored in CI secrets.

Trusted publishing helps against the theft part of the worm. It does not help against an attacker who has compromised a maintainer’s GitHub account and hijacked a workflow run, but it does eliminate the “I stole an automation token and used it to publish” path that Shai-Hulud relies on most heavily.

The catch: maintainers have to opt in. Most existing packages still use long-lived automation tokens. The migration is non-trivial because trusted publishing requires a GitHub Actions workflow that publishes (not all maintainer setups do), and the OIDC configuration on the npm side has to match. The November wave should push more maintainers through that migration.

What I am doing

My setup after this wave: cooldowns where the manager supports them, fewer publish tokens, and --ignore-scripts=true globally with explicit exceptions for packages like esbuild and sharp. The config drift got annoying enough that it became @happyberg/pkg-quarantine: write the cooldown settings, then audit that the package manager is actually enforcing them.

What we should expect next

The next obvious moves are cross-registry propagation, provenance-aware variants, and more targeted credential theft from CI. The posture does not change: cooldowns where you can, trusted publishing where you can, ignore-scripts on by default, and no belief in single-layer defenses.

References