Why I'm Glad I Upgraded to pnpm 11 Before the TanStack Attack

I switched to pnpm a while back mostly for practical reasons, disk space and monorepo ergonomics. When you have as many Node.js projects as I do, the symlinked node_modules approach makes a real difference. Then in April I upgraded to pnpm 11. As of May 12, 2026, I’m genuinely relieved I did.
What happened
On May 12, a threat group called TeamPCP executed what’s being called the “Mini Shai-Hulud” attack, a worm that compromised over 80 TanStack artifacts and more than 400 packages across the broader ecosystem. TanStack published a detailed post-mortem that’s worth reading in full.
The short version: the attackers exploited weaknesses in GitHub Actions pipeline configuration to hijack OIDC tokens from legitimate CI runs. Those tokens were then used to publish malicious releases carrying valid SLSA Build Level 3 provenance attestations, meaning the packages looked fully trusted from a supply chain security standpoint. A prepare lifecycle script in a git-based optionalDependencies entry did the actual damage, executing an obfuscated payload during npm install that harvested credentials for AWS, GCP, Kubernetes, and GitHub. Stolen tokens were immediately used to self-propagate to other packages maintained by the same victims.
The core problem isn’t unique to TanStack: the npm registry’s publisher-side defenses can’t protect you during the window between a malicious publication and its removal. That window is typically a few hours, but it’s long enough for every CI/CD pipeline running npm install to get hit.
How pnpm 11 blocked it without me doing anything
When I upgraded to pnpm 11, I was mostly skimming the changelog. In hindsight, three defaults matter a lot here.
minimumReleaseAge: 1440, Packages less than 24 hours old are quarantined. Since most malicious packages are discovered and pulled within hours of publication, this single setting meant my pipelines never even saw the malicious release. The attack’s entire model depends on infecting machines during that window.
blockExoticSubdeps: true, Transitive dependencies are blocked from pulling from non-standard sources like git repositories or direct tarball URLs. This is exactly the vector used here, an optionalDependencies entry pointing to a GitHub fork. With this setting on, that install would have failed loudly before any script ran.
allowBuilds strict opt-in, Lifecycle scripts (preinstall, postinstall, prepare) are blocked by default unless you explicitly list the package in your allowBuilds map in pnpm-workspace.yaml. The payload was triggered by a prepare script. Under pnpm 11’s defaults, that script never executes.
Together these three form a coherent zero-trust model: don’t run scripts unless you said so, don’t fetch from unusual sources, and don’t install things that just appeared on the registry.
What to actually do if you’re not on pnpm 11 yet
If you’re still on npm or Yarn, you’re not completely without options, but the ergonomics are rougher. npm v11.10+ added min-release-age but lacks a clean exclusion mechanism for internal packages. Yarn v4.10+ has npmMinimalAgeGate and npmPreapprovedPackages, which is more flexible, but Plug’n’Play adds toolchain friction that not every project can absorb.
pnpm 11 has the most opinionated out-of-the-box stance, which is exactly what you want for security defaults, you shouldn’t have to think about it.
If you’re already on pnpm 11, a few things worth checking:
- Move security settings to
pnpm-workspace.yaml, pnpm 11 no longer reads them from registry-auth files. - Enable
minimumReleaseAgeStrict: trueto fail installations rather than silently falling back to older versions. - Populate
allowBuildsexplicitly, only packages that genuinely need install-time scripts (esbuild,sharp,canvas, etc.). - Use
pnpm install --frozen-lockfilein CI to close off lockfile injection as an attack surface. - Move to OIDC-based Trusted Publishing for your own packages if you haven’t already. Persistent tokens in build environments are a liability, as this attack demonstrated.
On provenance attestations and what they can’t do
Valid SLSA Build Level 3 provenance on a malicious package feels like a contradiction. It isn’t, and understanding why matters.
Provenance attestations answer a specific question: was this artifact built correctly from this source in this pipeline? They don’t and can’t answer: was the pipeline itself compromised? When attackers hijack OIDC tokens inside a legitimate GitHub Actions run, the attestation is technically accurate, the build did happen in that pipeline. The pipeline just wasn’t doing what the maintainer intended.
This is why behavioral defaults like pnpm 11’s are a different and complementary layer of defense. Blocking lifecycle scripts and exotic dependency sources doesn’t care how trusted the provenance looks. It asks a simpler question: should this code be allowed to run at all during install? In most cases, the answer is no, and making that the default rather than something you have to opt into is the shift pnpm 11 represents.
Provenance attestations are still valuable. They’re just not a substitute for this kind of hardening.
Have questions about securing your Node.js supply chain or managing dependencies across a monorepo?