Skip to content

How One PR Hijacked 169 npm Packages in 6 Minutes

A
Alex Chen
May 14, 2026
11 min read
Science & Tech
How One PR Hijacked 169 npm Packages in 6 Minutes - Image from the article

Quick Summary

A single pull request compromised 169 npm packages downloaded 50M times weekly. Here's exactly how the Shydhalu attack worked — and how to protect yourself.

In This Article

A Supply Chain Attack With No Stolen Passwords

On May 14th, 2026, a supply chain attack quietly unfolded across the npm ecosystem — and it didn't need a single stolen credential to succeed. No phishing emails. No leaked tokens. No brute-forced passwords. In roughly six minutes, an attacker compromised over 100 packages collectively downloaded more than 50 million times per week, all using a subtle misconfiguration in a GitHub Actions workflow that most development teams would never think to audit.

By the following morning, security firm Aikido was tracking 373 poisoned package versions across 169 packages. The malware had leapt from npm to PyPI, forged commits signed by the Claude Code GitHub app, embedded itself into VS Code and Claude Code editors, and installed a background watchdog process that — if your stolen GitHub token expired — would detonate and wipe your root directory.

This is what a modern, well-engineered supply chain attack looks like. And understanding exactly how it worked is the first step to not becoming its next victim.

How the GitHub Actions Misconfiguration Created the Opening

The attack exploited how Tanstack, one of the most widely used React ecosystem projects, had configured its automated release pipeline. Like most professional open-source projects, Tanstack uses GitHub Actions to handle publishing. When a pull request is merged into main, a CI workflow fires, authenticates with npm, and pushes the new package version to the registry.

The authentication step is the clever part. Rather than storing a long-lived npm token as a secret — which is a known risk — Tanstack used npm's trusted publishing feature. Here's how trusted publishing works: the CI server requests a publish token from npm, GitHub cryptographically signs a statement attesting to which workflow is running, in which repository, and on which branch. npm validates that signed statement against an allowlist and, if everything checks out, issues a short-lived token. The token expires within minutes. There's nothing persistent to steal.

This setup is considered best practice and has been npm's recommended configuration for nearly two years. The problem wasn't in the concept — it was in one specific configuration parameter: pull_request_target.

When you configure a GitHub Actions workflow to trigger on pull_request_target rather than the standard pull_request event, the critical difference is context. The pull_request event runs workflows in the context of the fork, with the fork's limited permissions. But pull_request_target runs the workflow in the context of the base repository — the main repo — complete with the main repo's secrets and elevated permissions. This is documented behavior, not a GitHub bug. It exists for legitimate use cases like allowing forks to access labels or post comments. But when a publish workflow is attached to it, the blast radius becomes enormous.

The attacker forked the Tanstack repository, opened a pull request from that fork, and immediately closed it. The PR was never reviewed. No human being read it. But simply creating the PR was enough to trigger the pull_request_target workflow, which now ran with full repository permissions — including write access to the shared GitHub Actions cache.

The Cache Poisoning Technique

GitHub Actions caches are designed to speed up workflows by storing dependencies between job runs. The attacker's malicious workflow didn't need to publish anything directly. It just needed to write a poisoned file into that shared cache and wait.

A few hours later, an entirely unrelated pull request from a legitimate contributor was merged into main. The standard publish workflow kicked off, pulled from the cache as usual, and executed the poisoned file it found there. That file grabbed the npm publish token — which was now live in the CI environment — and used it to publish 84 compromised versions of Tanstack packages to the npm registry. All of them signed. All of them verified. All of them carrying malware.

This technique is sometimes called a cache poisoning attack, and it's particularly insidious because it introduces a time delay between the malicious action and the visible damage. Standard security monitoring looks for anomalies at the moment of attack. When the attacker's activity and the damage are hours apart and appear to come from a legitimate workflow, automated detection has very little to grab onto.

How the Worm Spread Beyond Tanstack

Once a developer installed one of the compromised Tanstack packages, the malware activated. It scanned the local system for npm authentication tokens — the kind stored in .npmrc files, which is standard practice for maintainers who publish packages. When it found them, it used those tokens to publish new poisoned versions of those maintainers' packages, propagating itself outward.

Continue Reading

Related Guides

Keep exploring this topic

How One PR Hijacked 169 npm Packages in 6 Minutes

The first wave of collateral victims included engineering teams at Mistral AI, UiPath, OpenSearch, Guardrails AI, and Squawk. Each of those organizations pushed compromised packages to npm without knowing it. From there, the worm detected Python SDK dependencies and jumped to PyPI, crossing the registry boundary — something most security tooling isn't configured to detect as a linked event.

What made the second-generation attacks harder to spot was deliberate camouflage. The malware began forging commits signed by the Claude Code GitHub App, mimicking the AI-generated commit signatures that many development teams now see routinely. It's a sharp insight into how attackers adapt to developer behavior: when AI-assisted commits become normalized noise, malicious commits disguised as AI output become near-invisible.

On infected machines, the worm embedded itself into VS Code and Claude Code, meaning that simply uninstalling the compromised npm package wasn't enough. The next time a developer opened their editor, the worm re-executed. And a background process polled every 60 seconds to check whether the stolen GitHub token was still valid. The moment that token expired — triggering what researchers called "war crime mode" — the process wiped the root directory. This dead-man's-switch design isn't just destructive; it's a deterrent. It punishes attempts to clean up without fully understanding the infection.

What 50 Million Weekly Downloads Actually Means

The scale here deserves a moment of genuine attention. These weren't obscure packages. Tanstack Query, Tanstack Router, and related libraries are production dependencies in thousands of commercial applications. The 50 million weekly download figure isn't inflated by bots or mirrors — it reflects real CI pipelines, real developer machines, and real production builds running every day.

Supply chain attacks are effective precisely because the trust model of open-source package management is optimistic by design. When you run npm install, you are implicitly trusting every maintainer, every CI pipeline, and every automated publishing workflow in your entire dependency tree — not just your direct dependencies, but their dependencies, and their dependencies' dependencies. The average production Node.js application has hundreds of transitive dependencies. Auditing all of them manually is not a realistic ask.

This is why the attack surface for supply chain exploits is structurally larger than almost any other class of vulnerability. You don't need to compromise the developer writing the application. You need to compromise one link in a chain that most developers never look at.

How to Harden Your npm Workflow Right Now

The honest answer is that no configuration makes you immune. But there are concrete, practical steps that would have meaningfully reduced — or in some cases entirely blocked — this specific attack vector.

Upgrade to pnpm 10 or higher. pnpm ships with three features that are directly relevant here, and two of them are on by default:

  • Minimum release age: pnpm can be configured to refuse packages published less than 24 hours ago. The window between malicious publication and detection is typically measured in hours — sometimes minutes for high-profile packages. A 24-hour delay means most poisoned packages are flagged and pulled before your install even runs.

  • Block exotic subdependencies: Standard packages should have all their dependencies hosted on the npm registry. Nothing stops a malicious package from listing a dependency pointing at an arbitrary Git repository or a tarball hosted on an attacker-controlled S3 bucket. This feature refuses to install anything that doesn't come through a proper registry, which closes off one of the most elegant malware distribution mechanisms available.

  • Approved builds: npm install scripts run automatically during installation, and the majority of npm-based malware does its damage through exactly this mechanism. pnpm 11 blocks all install scripts by default and requires explicit whitelisting. This is a significant workflow change for projects that rely on native binaries, but the security tradeoff is worth seriously evaluating.

Audit your GitHub Actions workflow triggers. Search every workflow file in your repositories for pull_request_target. If a workflow using that trigger has access to secrets, publish tokens, or write permissions, it is a potential attack vector unless you have explicitly scoped the permissions and validated that the trigger is necessary. In most cases, pull_request is the correct event.

Free Weekly Newsletter

Enjoying this guide?

Get the best articles like this one delivered to your inbox every week. No spam.

How One PR Hijacked 169 npm Packages in 6 Minutes

Scope your npm tokens tightly. Automation tokens should have the minimum permissions required and should be scoped to specific packages where the registry supports it. Trusted publishing is still the right approach — but it needs to be paired with correctly configured workflow triggers.

Treat your CI cache as a security boundary. Most teams don't. Cache entries should be scoped to specific branches and should not be writable by workflows triggered from forks unless that access is explicitly required and audited.

The Uncomfortable Truth About Open-Source Security

The Tanstack attack is a well-executed illustration of a problem that security researchers have been documenting for years: the open-source supply chain is a high-value, under-defended attack surface, and the tools we've built to secure it often create new attack vectors when misconfigured.

Trusted publishing was designed to eliminate long-lived secrets from CI environments. It works exactly as designed. The vulnerability wasn't in the feature — it was in how the workflow trigger was configured around it. That distinction matters because it means the solution isn't to abandon trusted publishing; it's to understand the full security model of every configuration choice in your pipeline.

The most sophisticated aspect of this attack is how well it understood developer psychology. It waited for a legitimate workflow event. It used the trusted publishing infrastructure. It disguised commits as AI-generated output. It embedded itself in editors rather than just processes. It punished cleanup attempts. Every one of those choices reflects a detailed understanding of how modern development teams actually work.

Defending against that kind of adversary requires the same depth of understanding. Not just what your tools do, but exactly how they interact — and what happens when someone feeds them unexpected input.

Frequently Asked Questions

What is a supply chain attack in the context of npm?

A supply chain attack targets the tools, dependencies, or infrastructure used to build software rather than the software itself. In the npm ecosystem, this typically means compromising a package that many developers install, so that malicious code runs on developer machines or in production environments without targeting any individual developer directly.

What is the difference between pull_request and pull_request_target in GitHub Actions?

The pull_request event runs workflows in the context of the fork that submitted the PR, with the fork's limited permissions and no access to the base repository's secrets. The pull_request_target event runs workflows in the context of the base repository, with full access to its secrets and permissions. This distinction becomes a critical security boundary when the workflow involves authentication tokens or publishing credentials.

Why did the malware target VS Code and Claude Code specifically?

Embedding in editors provides persistence beyond package uninstallation. When a developer removes a compromised npm package, the worm re-executes the next time the editor opens, because the editor itself has been modified to act as a loader. It also means the malware runs with the developer's full local permissions every time the editor starts.

How does pnpm's minimum release age feature work in practice?

pnpm can be configured with a minPublishAge setting that instructs the package manager to reject any package version published less than a specified number of hours ago. Setting this to 24 hours means that even if a malicious version is published to the registry, your install command will refuse to fetch it during the window when most active malware campaigns are operating — before the package is flagged and removed.

Does using trusted publishing on npm make you immune to this type of attack?

No. Trusted publishing eliminates long-lived secret tokens from CI environments, which reduces one category of risk. But as this attack demonstrated, if the GitHub Actions workflow trigger is misconfigured — specifically using pull_request_target instead of pull_request — an attacker can still cause a workflow to run with elevated permissions from a forked repository. Trusted publishing is a valuable security layer, but it is not a substitute for correctly scoped workflow permissions.

Frequently Asked Questions

A Supply Chain Attack With No Stolen Passwords

On May 14th, 2026, a supply chain attack quietly unfolded across the npm ecosystem — and it didn't need a single stolen credential to succeed. No phishing emails. No leaked tokens. No brute-forced passwords. In roughly six minutes, an attacker compromised over 100 packages collectively downloaded more than 50 million times per week, all using a subtle misconfiguration in a GitHub Actions workflow that most development teams would never think to audit.

By the following morning, security firm Aikido was tracking 373 poisoned package versions across 169 packages. The malware had leapt from npm to PyPI, forged commits signed by the Claude Code GitHub app, embedded itself into VS Code and Claude Code editors, and installed a background watchdog process that — if your stolen GitHub token expired — would detonate and wipe your root directory.

This is what a modern, well-engineered supply chain attack looks like. And understanding exactly how it worked is the first step to not becoming its next victim.

How the GitHub Actions Misconfiguration Created the Opening

The attack exploited how Tanstack, one of the most widely used React ecosystem projects, had configured its automated release pipeline. Like most professional open-source projects, Tanstack uses GitHub Actions to handle publishing. When a pull request is merged into main, a CI workflow fires, authenticates with npm, and pushes the new package version to the registry.

The authentication step is the clever part. Rather than storing a long-lived npm token as a secret — which is a known risk — Tanstack used npm's trusted publishing feature. Here's how trusted publishing works: the CI server requests a publish token from npm, GitHub cryptographically signs a statement attesting to which workflow is running, in which repository, and on which branch. npm validates that signed statement against an allowlist and, if everything checks out, issues a short-lived token. The token expires within minutes. There's nothing persistent to steal.

This setup is considered best practice and has been npm's recommended configuration for nearly two years. The problem wasn't in the concept — it was in one specific configuration parameter: pull_request_target.

When you configure a GitHub Actions workflow to trigger on pull_request_target rather than the standard pull_request event, the critical difference is context. The pull_request event runs workflows in the context of the fork, with the fork's limited permissions. But pull_request_target runs the workflow in the context of the base repository — the main repo — complete with the main repo's secrets and elevated permissions. This is documented behavior, not a GitHub bug. It exists for legitimate use cases like allowing forks to access labels or post comments. But when a publish workflow is attached to it, the blast radius becomes enormous.

The attacker forked the Tanstack repository, opened a pull request from that fork, and immediately closed it. The PR was never reviewed. No human being read it. But simply creating the PR was enough to trigger the pull_request_target workflow, which now ran with full repository permissions — including write access to the shared GitHub Actions cache.

The Cache Poisoning Technique

GitHub Actions caches are designed to speed up workflows by storing dependencies between job runs. The attacker's malicious workflow didn't need to publish anything directly. It just needed to write a poisoned file into that shared cache and wait.

A few hours later, an entirely unrelated pull request from a legitimate contributor was merged into main. The standard publish workflow kicked off, pulled from the cache as usual, and executed the poisoned file it found there. That file grabbed the npm publish token — which was now live in the CI environment — and used it to publish 84 compromised versions of Tanstack packages to the npm registry. All of them signed. All of them verified. All of them carrying malware.

This technique is sometimes called a cache poisoning attack, and it's particularly insidious because it introduces a time delay between the malicious action and the visible damage. Standard security monitoring looks for anomalies at the moment of attack. When the attacker's activity and the damage are hours apart and appear to come from a legitimate workflow, automated detection has very little to grab onto.

How the Worm Spread Beyond Tanstack

Once a developer installed one of the compromised Tanstack packages, the malware activated. It scanned the local system for npm authentication tokens — the kind stored in .npmrc files, which is standard practice for maintainers who publish packages. When it found them, it used those tokens to publish new poisoned versions of those maintainers' packages, propagating itself outward.

The first wave of collateral victims included engineering teams at Mistral AI, UiPath, OpenSearch, Guardrails AI, and Squawk. Each of those organizations pushed compromised packages to npm without knowing it. From there, the worm detected Python SDK dependencies and jumped to PyPI, crossing the registry boundary — something most security tooling isn't configured to detect as a linked event.

What made the second-generation attacks harder to spot was deliberate camouflage. The malware began forging commits signed by the Claude Code GitHub App, mimicking the AI-generated commit signatures that many development teams now see routinely. It's a sharp insight into how attackers adapt to developer behavior: when AI-assisted commits become normalized noise, malicious commits disguised as AI output become near-invisible.

On infected machines, the worm embedded itself into VS Code and Claude Code, meaning that simply uninstalling the compromised npm package wasn't enough. The next time a developer opened their editor, the worm re-executed. And a background process polled every 60 seconds to check whether the stolen GitHub token was still valid. The moment that token expired — triggering what researchers called "war crime mode" — the process wiped the root directory. This dead-man's-switch design isn't just destructive; it's a deterrent. It punishes attempts to clean up without fully understanding the infection.

What 50 Million Weekly Downloads Actually Means

The scale here deserves a moment of genuine attention. These weren't obscure packages. Tanstack Query, Tanstack Router, and related libraries are production dependencies in thousands of commercial applications. The 50 million weekly download figure isn't inflated by bots or mirrors — it reflects real CI pipelines, real developer machines, and real production builds running every day.

Supply chain attacks are effective precisely because the trust model of open-source package management is optimistic by design. When you run npm install, you are implicitly trusting every maintainer, every CI pipeline, and every automated publishing workflow in your entire dependency tree — not just your direct dependencies, but their dependencies, and their dependencies' dependencies. The average production Node.js application has hundreds of transitive dependencies. Auditing all of them manually is not a realistic ask.

This is why the attack surface for supply chain exploits is structurally larger than almost any other class of vulnerability. You don't need to compromise the developer writing the application. You need to compromise one link in a chain that most developers never look at.

How to Harden Your npm Workflow Right Now

The honest answer is that no configuration makes you immune. But there are concrete, practical steps that would have meaningfully reduced — or in some cases entirely blocked — this specific attack vector.

Upgrade to pnpm 10 or higher. pnpm ships with three features that are directly relevant here, and two of them are on by default:

  • Minimum release age: pnpm can be configured to refuse packages published less than 24 hours ago. The window between malicious publication and detection is typically measured in hours — sometimes minutes for high-profile packages. A 24-hour delay means most poisoned packages are flagged and pulled before your install even runs.

  • Block exotic subdependencies: Standard packages should have all their dependencies hosted on the npm registry. Nothing stops a malicious package from listing a dependency pointing at an arbitrary Git repository or a tarball hosted on an attacker-controlled S3 bucket. This feature refuses to install anything that doesn't come through a proper registry, which closes off one of the most elegant malware distribution mechanisms available.

  • Approved builds: npm install scripts run automatically during installation, and the majority of npm-based malware does its damage through exactly this mechanism. pnpm 11 blocks all install scripts by default and requires explicit whitelisting. This is a significant workflow change for projects that rely on native binaries, but the security tradeoff is worth seriously evaluating.

Audit your GitHub Actions workflow triggers. Search every workflow file in your repositories for pull_request_target. If a workflow using that trigger has access to secrets, publish tokens, or write permissions, it is a potential attack vector unless you have explicitly scoped the permissions and validated that the trigger is necessary. In most cases, pull_request is the correct event.

Scope your npm tokens tightly. Automation tokens should have the minimum permissions required and should be scoped to specific packages where the registry supports it. Trusted publishing is still the right approach — but it needs to be paired with correctly configured workflow triggers.

Treat your CI cache as a security boundary. Most teams don't. Cache entries should be scoped to specific branches and should not be writable by workflows triggered from forks unless that access is explicitly required and audited.

The Uncomfortable Truth About Open-Source Security

The Tanstack attack is a well-executed illustration of a problem that security researchers have been documenting for years: the open-source supply chain is a high-value, under-defended attack surface, and the tools we've built to secure it often create new attack vectors when misconfigured.

Trusted publishing was designed to eliminate long-lived secrets from CI environments. It works exactly as designed. The vulnerability wasn't in the feature — it was in how the workflow trigger was configured around it. That distinction matters because it means the solution isn't to abandon trusted publishing; it's to understand the full security model of every configuration choice in your pipeline.

The most sophisticated aspect of this attack is how well it understood developer psychology. It waited for a legitimate workflow event. It used the trusted publishing infrastructure. It disguised commits as AI-generated output. It embedded itself in editors rather than just processes. It punished cleanup attempts. Every one of those choices reflects a detailed understanding of how modern development teams actually work.

Defending against that kind of adversary requires the same depth of understanding. Not just what your tools do, but exactly how they interact — and what happens when someone feeds them unexpected input.

Frequently Asked Questions

What is a supply chain attack in the context of npm?

A supply chain attack targets the tools, dependencies, or infrastructure used to build software rather than the software itself. In the npm ecosystem, this typically means compromising a package that many developers install, so that malicious code runs on developer machines or in production environments without targeting any individual developer directly.

What is the difference between pull_request and pull_request_target in GitHub Actions?

The pull_request event runs workflows in the context of the fork that submitted the PR, with the fork's limited permissions and no access to the base repository's secrets. The pull_request_target event runs workflows in the context of the base repository, with full access to its secrets and permissions. This distinction becomes a critical security boundary when the workflow involves authentication tokens or publishing credentials.

Why did the malware target VS Code and Claude Code specifically?

Embedding in editors provides persistence beyond package uninstallation. When a developer removes a compromised npm package, the worm re-executes the next time the editor opens, because the editor itself has been modified to act as a loader. It also means the malware runs with the developer's full local permissions every time the editor starts.

How does pnpm's minimum release age feature work in practice?

pnpm can be configured with a minPublishAge setting that instructs the package manager to reject any package version published less than a specified number of hours ago. Setting this to 24 hours means that even if a malicious version is published to the registry, your install command will refuse to fetch it during the window when most active malware campaigns are operating — before the package is flagged and removed.

Does using trusted publishing on npm make you immune to this type of attack?

No. Trusted publishing eliminates long-lived secret tokens from CI environments, which reduces one category of risk. But as this attack demonstrated, if the GitHub Actions workflow trigger is misconfigured — specifically using pull_request_target instead of pull_request — an attacker can still cause a workflow to run with elevated permissions from a forked repository. Trusted publishing is a valuable security layer, but it is not a substitute for correctly scoped workflow permissions.

Z

About Zeebrain Editorial

Our editorial team is dedicated to providing clear, well-researched, and high-utility content for the modern digital landscape. We focus on accuracy, practicality, and insights that matter.

More from Science & Tech

Explore More Categories

Keep browsing by topic and build depth around the subjects you care about most.