Over the last twelve months, npm racked up more supply-chain incidents than in any comparable period before. September 2025: chalk and debug, eighteen packages with more than two billion combined weekly downloads; that same month, Shai-Hulud, the ecosystem’s first self-replicating worm, with @ctrl/tinycolor and 500+ other packages. November 2025: Shai-Hulud 2.0, a second wave, 796 infected packages. March–April 2026: axios, Bitwarden CLI, SAP. May 2026: TanStack — 84 malicious versions signed with valid SLSA Build Level 3 provenance.
My personal site, the one you’re reading, uses none of the victim packages. But it also didn’t have a single line of preventive defense against the next one. This afternoon I fixed that. The result: one commit, two files changed, thirty-nine lines. Most of the value is in a single line.
the common pattern
The attacks of the last twelve months resemble each other more than the headlines suggest. Phishing the maintainer, a stolen OIDC token or a hijacked account. A malicious version published to the registry. A preinstall script that runs the first time someone does pnpm install with that version live, steals tokens, propagates to the next maintainer’s packages.
There’s one constant in all of them: they were detected and pulled from npm in under seven days. chalk and debug, around two hours. Shai-Hulud 2.0, twelve hours. SAP, between two and four hours. Bitwarden CLI, an hour and a half.
That observation is the basis of the most effective defense that exists today against this class of attack. If your package manager waits seven days before accepting any new version, none of them touch you.
the audit
Before changing anything, I looked at what I had:
pnpm 10.33.0, Node 22.22.2.- Ten direct dependencies, two dev.
pnpm-lock.yamlwith 454 packages and SHA-512integrityon all of them..envin.gitignore— no secrets committed.
And on the defense side: none. No .npmrc, no pinned packageManager, no script allowlist.
A side note: the initial review found a preexisting HIGH vulnerability, unrelated to any of the attacks above. devalue@5.7.1, a denial of service via deserialization of sparse arrays (GHSA-77vg-94rm-hx3p), came in transitively via @astrojs/react. Flagged to fix in the same commit.
what I applied
.npmrc:
minimum-release-age=10080
audit-level=high
save-exact=true
package-manager-strict=true
engine-strict=true
fund=false
The line that matters is the first. Ten thousand and eighty minutes is seven days. pnpm rejects any version published less than a week ago. That single line would have blocked chalk, debug, axios, Shai-Hulud, SAP, Bitwarden CLI and TanStack — every relevant case of the last twelve months.
save-exact=true removes the ^ ranges. Each pnpm add pins the exact version. Combined with the cooldown, it guarantees no future pnpm update resolves to something unreviewed.
package-manager-strict=true validates that the local pnpm version matches the one pinned in package.json (next section). It defends against a downgrade of the manager itself.
engine-strict=true fails if Node doesn’t satisfy the engines range. Reproducibility.
audit-level=high sets the default threshold for pnpm audit. Without it, audit reports everything, including LOW. Noise.
fund=false silences the funding messages. Cosmetic.
package.json:
"packageManager": "pnpm@10.33.0+sha1.46a0bef28aad690765997ab6da6d5475bdd4148e",
...
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "sharp"],
"overrides": {
"devalue@>=5.6.3 <5.8.1": ">=5.8.1"
}
}
packageManager pins pnpm to an exact version with a hash verified against the registry. Corepack — included in Node 22 — runs exactly that binary; any other version or tampered binary fails.
pnpm.onlyBuiltDependencies is the most underrated piece of the setup. Since pnpm 10, preinstall and postinstall scripts don’t run by default. This allowlist explicitly authorizes esbuild and sharp — the only packages in the tree that need them to compile native binaries. Any other package trying to run scripts is blocked. That’s the vector Shai-Hulud uses to steal credentials.
pnpm.overrides fixes the preexisting HIGH vulnerability without waiting for Astro to ship a version that pulls it in transitively.
tanstack: a case worth understanding
There’s a detail in the 2026 timeline that deserves attention, because it challenges a common belief about supply-chain security.
On May 11, 2026, between 19:20 and 19:26 UTC, the Mini Shai-Hulud worm compromised the TanStack monorepo. Eighty-four malicious versions published across forty-two packages in the @tanstack/* namespace, plus Mistral AI and Guardrails AI. All with valid SLSA Build Level 3 provenance.
The Sigstore attestations were correct: they attested that the packages had been built by release.yml running on refs/heads/main of the TanStack/router repository. A verifiable truth. And yet, malicious code.
The vector was a Pwn Request in GitHub Actions — a pull_request_target event that checks out the PR’s SHA without approval protection. The attacker injected code into the runner while the legitimate pipeline was running, stole the OIDC token and published from the project’s own CI/CD.
SLSA does not attest that the workflow code was authorized to run, nor that the commit that triggered the build is legitimate. It only attests that a build ran in a repository. The signatures were legitimate and the packages still contained malware. The seven-day cooldown, by contrast, blocks them: the malicious versions were live for a few hours before being detected and pulled.
what remains a human responsibility
No configuration covers this:
- 2FA with WebAuthn — a passkey, not SMS, not TOTP — on npm and GitHub. Without it, phishing of the kind that hit chalk and debug works against anyone.
- Rotate old tokens that may have touched compromised versions:
npm token list,gh auth status, the deploy provider’s tokens. - For repos that publish packages: review GitHub Actions workflows against Pwn Requests. If there’s a
pull_request_targetwithcheckoutof the PR’s SHA without explicit approval, the risk is the same as the TanStack case.
The cooldown buys time for the community to detect the attack. But the attack is still possible if the victim account is your own.
the summary
The supply-chain attacks of the last twelve months share a common denominator their technical variety hides: all were pulled from npm in under a week, almost all within hours. That window is the one minimum-release-age=10080 closes — seven days in which npm has time to detect, maintainers to revert, and whoever installs to avoid pulling in the bad thing.
Blocking unauthorized lifecycle scripts with pnpm.onlyBuiltDependencies covers the specific vector the worms use to steal credentials. Pinning the manager with packageManager closes the rest of the install perimeter. The rest of the setup matters, but as backup. The core defense is those three pieces.