Skip to content

Build Reports disagree2 publishers 3 min read Published Updated

Shai-Hulud spreads by bumping a version number your dependency range already accepts

September's wave altered more than 500 npm package versions and November's backdoored 796, both by republishing under a fresh version number, which is exactly the thing an exact pin declines to fetch.

The Engineer · Build desk

Send a correction

What happened

  • Between September 14 and 18, 2025, more than 500 npm package versions changed with no maintainer running npm publish, each new version carrying the code that published further versions from infected machines.
  • Two months later, on November 24, a larger variant backdoored 796 packages, ran earlier in the install process, and deleted the user's home directory when it found neither credentials to steal nor a way to spread.
  • Given a stolen npm token, the payload listed every package the maintainer owned, downloaded them, injected itself into the postinstall step, bumped each version number and republished.
  • Given a GitHub token, it wrote the harvested credentials to a new public repository and flipped reachable private repositories public under the original name with a "-migration" suffix appended.

Why it matters

  • Who has to decide decision Whether your build reads a recorded pin or re-resolves a range is the control that decides if a self-republished version can enter, because no diff ever reaches a reviewer.
  • Who's exposed exposure Everything the install shell can read is in scope, cloud instance metadata included, so the credential genuinely at risk is the CI role rather than one developer's token.
  • What it limits constraint A pipeline that verifies attestations and stops there has bought assurance about who published and none about what shipped, which narrows what signing can be asked to do.
  • What it sets up precedent With coding-agent credentials named as the objective rather than collected incidentally, teams running agents in CI have to treat those tokens as publish-capable secrets.

"Earlier in the install process" is the phrase in that account doing the most work, and it is the one you cannot act on. The New Stack says the November variant moved its execution earlier to render developer triggers irrelevant [16]. It does not name the hook. If the new entry point is still a lifecycle script, installing with scripts disabled covers it. If it is not, that control covers the September payload and not the thing that replaced it. One sentence of a writeup does not settle which, and the difference is the difference between a fix and a speed bump.

The propagation step needs one thing to work downstream: a version number that something will resolve. It bumps the version and republishes under the maintainer's own name [5], so a consumer whose manifest carries a range picks up the poisoned build on its next resolve, while a consumer installing from a recorded exact version does not until someone re-resolves [13]. A lockfile inspects nothing. It declines to select a version that was not recorded when it was written, and that is the whole of its contribution here.

The same account describes the limit case. On August 4, 2026, a variant called ChainDrop compromised more than 400 packages in under four hours by riding a legitimate, cryptographically signed release pipeline, so each poisoned version arrived with a valid SLSA provenance attestation [10]. That is better than 100 packages an hour [15]. Its command channel sat inside an Ethereum smart contract, which renders domain blocklists moot [11]. The attestation was accurate, and that is the problem: provenance answers who built the artifact, not what the artifact does.

Treat 500-plus and 796 as counts from someone else's dependency graph. They are not even the same unit, since September counted package versions altered and November counted packages backdoored [3][16], so dividing them gives 1.59 and means nothing as a growth rate [14]. For either figure to reach your pipeline, one of those names has to sit in your tree, directly or transitively, and your install has to re-resolve a range rather than read a pin.

Two smaller details are worth keeping. The payload downloaded Trufflehog, a legitimate open source secret scanner, to confirm that the credentials it harvested were live [2]; the worm ships with a validation step that some internal secret audits skip. Palo Alto Networks' Unit 42 is moderately confident that parts of the script were written by an LLM, going on comments and emoji embedded in the payload [8].

The generalization in that piece holds up: swap npm publish for terraform apply and the mechanics hardly change [12]. Install-time execution with the permissions of whatever ran the install is a property of registries, not of JavaScript [6]. The worm never has to persuade a reviewer, because it only ever needed one credential and an install script [7].

What to watch

  • Whether anyone publishes the specific lifecycle hook the November variant used, since that decides whether disabling install scripts covers it at all.
  • Whether npm changes the default so that publishing needs a factor a token in the environment cannot supply.
  • Whether consumers of SLSA attestations add content screening on top of signature verification after the ChainDrop case.

Clarity's read

What the record supports and how the coverage leans. The claims behind it follow.

Reality

Evidence62
Adoption
Insufficient
Hype gap+15
Incentives
Insufficient
Confidence58
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [1]

    The compromised package runs a postinstall script (a single file called bundle.js) that searches the infected machine for anything resembling credentials, including npm and GitHub personal access tokens, AWS or GCP secrets, and whatever it can extract from a cloud instance's metadata service.

  2. [2]

    The payload downloads Trufflehog, a legitimate open source secret-scanning tool, and uses it to confirm the validity of any credentials it finds.

  3. [3]

    Beginning September 15, 2025, npm packages started updating themselves with no maintainer running npm publish and no pull request merged; between September 14 and 18, more than 500 package versions were altered, each carrying a payload that published more versions of more packages on more machines without human involvement.

Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.

Add to ChatGPTAdd to Claude

Sources

2 independent publishers whose own reporting we read for this story.

  1. dev.to

    1 article · September 1, 2026

    The npm Worm That Brought Its Own Interpreter
  2. thenewstack.io

    1 article · August 31, 2026

    Shai-Hulud: Whoever controls your package registry controls your pipeline

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Loading related stories