One PR Hijacked the Entire NPM Registry
A single pull request compromised 169 npm packages—no phishing, no stolen passwords. Here's how the TanStack supply chain attack actually worked.
Written by AI. Zara Chen

Photo: AI. Castor Belov
Okay so I was mid-coffee when I saw the Fireship breakdown of this attack and genuinely had to put the mug down. Not for dramatic effect. Because the moment I understood what the poisoned cache was doing while it just... sat there and waited — patient, invisible, already inside the house — my brain needed a second to catch up.
No passwords stolen. No phishing. No leaked tokens. Just a PR that nobody reviewed, opened and immediately closed, that quietly rewrote the future of 84 packages used by millions of developers. That's the attack. And it worked.
What actually happened (and when)
Here's where I have to be precise, because the timeline is the thing that makes this so disturbing.
The attacker didn't compromise anything in six minutes. That framing — which Fireship uses in reference to the publishing step — gets a little lost when you zoom out. The actual operation played out over hours, maybe longer. The attacker forked the TanStack repo, opened a PR, and immediately closed it. No one saw it. No one reviewed it. But just the act of creating that PR was enough to trigger TanStack's GitHub Actions publishing workflow, because the team had configured their workflow trigger as [pull_request_target](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/) — a setting that runs any incoming PR in the context of the main repository, with main repository permissions, even when the PR comes from a fork.
That's the crack in the foundation. The attacker's code — never merged, never approved — ran with full CI privileges. It wrote a poisoned file into GitHub Actions' shared dependency cache and then just... waited.
Hours later, a totally unrelated, completely legitimate PR got merged into main. That triggered a new workflow run. The workflow pulled from the shared cache — which was already compromised — grabbed the npm publish token, and used it to push 84 poisoned TanStack packages to the registry. The six minutes Fireship references is how long the publishing blast took. The setup took much longer. The attacker wasn't rushing. They were patient.
I keep coming back to that waiting period. The malicious payload sitting in the cache, dormant, while TanStack's team went about their normal day. Nobody saw a breach. Nothing looked wrong. That is so much scarier than a smash-and-grab.
The "trusted publishing" irony
The poisoned packages weren't sketchy lookalikes with misspelled names. They were signed, verified, and shipped through npm's trusted publishing feature — the thing that was specifically built to make this kind of attack impossible. As Fireship puts it: the tokens "live for a few minutes in the CI's cache and then quickly become stale, so there's nothing for traditional phishing attacks to steal."
The defense was real. It just wasn't built for this.
TanStack's CI system was the vector, not a compromised developer account. By the time the poisoned packages landed on the registry, they looked legitimate by every metric npm had available to check. Signed. Verified. Published from the right repo. The trust signals all fired green.
When a TanStack problem becomes everybody's problem
Once the malware was inside a developer's environment, it did what worms do: it looked for more keys to copy. If it found any npm publishing tokens, it used them to push new poisoned versions of whatever packages those tokens had access to. So the blast radius expanded fast.
Fireship reports that the first wave of collateral damage hit maintainers at Mistral AI, UiPath, OpenSearch, Guardrails AI, and Squawk. Their packages got compromised. Through Python SDKs, the infection jumped from npm to PyPI. By the next morning, security firm Aikido was tracking 373 poisoned versions across 169 packages.
And then it got creative. The worm started forging commits signed by the Claude Code GitHub app.
I need to sit with that for a second, because this detail is doing a lot. We are living in a moment where AI-generated commits are normal. Maintainers see Claude Co-authored-by lines and don't blink. The worm figured that out and used it as camouflage. It didn't need to be invisible — it just needed to look like AI slop, which is now so ambient in codebases that nobody questions it. That's not a technical exploit. That's a cultural exploit. The attacker understood the vibe of modern dev workflows better than a lot of security tooling does.
According to Fireship's reporting — sourcing Aikido's analysis — the malware also embedded itself directly into VS Code and Claude Code on infected machines. Meaning: you uninstall the bad package, you think you're clean, and then you open your editor the next morning and the worm just runs again. That persistence mechanism is vicious.
And then there's the part of this that I genuinely cannot stop thinking about. The dead man's switch. The malware installed a background process that pinged every 60 seconds to check whether your stolen GitHub token was still valid. The moment that token expired — maybe because you rotated it, maybe because you detected the breach and tried to remediate — the process activated what Fireship calls "war crime mode" and deleted your root directory.
Trying to clean it up is the trigger. You defending yourself is what sets it off. That's not just clever. That's mean.
What to actually do about this
I'm going to tell you what I'd tell a developer friend right now, not what the release notes say.
Switch to PNPM 11 if you haven't. Like, this week. It ships with three things that would have stopped this exact attack cold for most people:
Minimum release age — PNPM won't install any package published less than 24 hours ago. This matters because malicious packages tend to be detected and pulled quickly after they land (Fireship cites this pattern, and it tracks with how this incident unfolded — Aikido was tracking infections by the next morning). The 24-hour buffer is a simple, dumb, effective fence.
Block exotic subdependencies — nothing in your dependency tree is allowed to point at a random GitHub repo or some attacker's S3 bucket. Everything has to come through a proper registry. This closes a whole category of "legitimate package, surprise malware inside" attacks that are genuinely underappreciated.
Approved builds — this one's a little more friction, but worth it. PNPM 11 blocks install scripts by default. You have to explicitly whitelist packages that can run code at install time. Most packages don't need to run code at install time. The ones that do, you probably already know about.
None of these make you bulletproof. A sufficiently motivated attacker with enough time and patience can route around almost anything. But these three features would have blocked this specific attack for most of the people it hit, and they're not hard to enable.
The harder question — and the one I don't have a clean answer to — is what we do about the pull_request_target problem at scale. TanStack misconfigured a workflow trigger. So have thousands of other maintainers, almost certainly, for the exact same reason: the GitHub docs present it as a convenient option, and the security implications aren't obvious until you're in an incident review. The security community has known this was risky for years. The tooling still doesn't shout loud enough warnings when you use it.
Open source runs on trust and volunteer labor, and the attack surface is every repo, every CI config, every cached dependency. The question isn't whether this will happen again. It's whether the infrastructure we all depend on is going to get serious about making the safe configuration the default one — before the next attacker with this level of patience and creativity shows up.
By Zara Chen, Tech & Politics Correspondent
We Watch Tech YouTube So You Don't Have To
Get the week's best tech insights, summarized and delivered to your inbox. No fluff, no spam.
More Like This
Axios RAT Attack: What Happened and How to Check If You're Hit
A sophisticated remote access trojan infiltrated Axios through a rogue dependency. Here's how the attack worked and what developers need to do now.
Malware Now Uses Blockchain for Command and Control
Sophisticated malware campaign uses invisible Unicode characters and Solana blockchain transactions to evade detection and communicate with attackers.
React Doctor Scans Your Code for Anti-Patterns in Milliseconds
React Doctor is a Rust-powered CLI tool that detects common React anti-patterns and performance issues in milliseconds. Here's what it actually finds.
TanStack Got Hacked—And Nobody Made a Mistake
A sophisticated NPM worm hit TanStack and 160+ packages—with a deadman switch that wipes your PC if you revoke stolen credentials. Here's what actually happened.
Bambu Lab Is Picking Fights It Doesn't Need to Win
Bambu Lab threatened an open-source developer over a slicer fork. Jeff Geerling breaks down why that move says everything about where the company is headed.
Dirty Frag: Linux Zero-Day With No Patch Yet
Dirty Frag is a Linux kernel privilege escalation exploit with no patches yet. Here's what it does, who's at risk, and how to mitigate it now.
I Tested Claude Design: Here's What Happened to My UI
Developer OrcDev spent hours testing Anthropic's Claude Design AI tool. The results reveal what AI can—and critically can't—do for interface design.
Framework 13 Gets ARM—But Should You Actually Want It?
MetaComputing's new ARM mainboard for Framework 13 promises modular computing's future. Tech journalist Jeff Geerling tests whether it delivers.
RAG·vector embedding
2026-05-15This article is indexed as a 1536-dimensional vector for semantic retrieval. Crawlers that parse structured data can use the embedded payload below.