The September 2025 Shai-Hulud wave taught the npm ecosystem that a single phished maintainer can become 500 backdoored packages before the security teams finish their morning coffee. The May 2026 sequel taught a harder lesson: you do not need to phish anyone. On 11 May 2026, a threat cluster that researchers track as TeamPCP turned a project's own continuous-integration pipeline into the propagation engine. Within six minutes of the initial trigger, 84 malicious package artifacts were published across 42 @tanstack/* packages. By the next day the campaign — dubbed Mini Shai-Hulud by Aikido Security — had touched more than 160 npm and PyPI packages, including releases tied to Mistral AI, UiPath, Guardrails AI, and OpenSearch.
This is a reconstruction written from the public TanStack postmortem, vendor analyses from Snyk, Wiz, Microsoft, and Orca, and the GitHub remediation figures. A number of mechanics are best-effort interpretation of those sources and parallel 2025 attack patterns. We flag what is reported versus inferred throughout. The headline is not the package count. It is that the attack never needed a maintainer's password, a TOTP code, or a long-lived token. It needed a misconfigured workflow trigger and a token that lived in process memory for a few seconds.
TL;DR
- On 11 May 2026, the TeamPCP-linked Mini Shai-Hulud worm compromised TanStack's npm publishing pipeline and pushed 84 malicious artifacts across 42
@tanstack/*packages in roughly six minutes. - The entry point was a
pull_request_target"Pwn Request" in TanStack's GitHub Actions configuration, chained with Actions cache poisoning across the fork-to-base trust boundary and in-memory extraction of an OIDC token from the runner process. - By 12 May the campaign had reached 160+ npm and PyPI packages, including releases linked to Mistral AI, UiPath, Guardrails AI, and OpenSearch. A related cluster hit the
@antvorg with downstream impact onecharts-for-react(1M+ weekly downloads). - The payload steals credentials (GitHub, npm, AWS, HashiCorp Vault, Kubernetes, 1Password), self-propagates by republishing packages the compromised token can write to, and in some variants stages a destructive component.
- GitHub responded by removing 640 malicious packages and invalidating 61,274 npm granular access tokens that carried write permissions plus 2FA bypass.
- Monday-morning actions: pin and audit
@tanstack/*versions against the known-bad list, rotate any token that touched a CI runner since 11 May, and replacepull_request_targetpatterns and long-lived publish tokens with trusted publishing.
What happened
TanStack maintains a widely-used family of headless UI and data libraries — TanStack Query, Router, Table, Form, Start, and others — published under the @tanstack/* npm scope. Several of these packages sit deep in the dependency trees of React and Vue applications across the industry.
According to TanStack's own postmortem and corroborating analyses from Snyk and Wiz, the compromise began on 11 May 2026 through the project's GitHub Actions CI pipeline rather than through a stolen maintainer credential. The attacker abused a workflow that ran on the pull_request_target event. Once code execution was achieved in the trusted CI context, the worm extracted a publishing credential and pushed 84 malicious artifacts across 42 packages. Snyk and the TanStack team place the publish burst at roughly six minutes — fast enough that the malicious versions were on the registry and being installed before any human noticed.
The campaign did not stop at TanStack. Aikido Security, which coined the "Mini Shai-Hulud" name, reported that the broader campaign published malicious versions of more than 170 npm packages and a smaller set of PyPI packages, including mistralai==2.4.6. Microsoft documented a closely related cluster hitting the @antv npm organization, where a compromised maintainer account seeded malicious versions that propagated downstream to echarts-for-react and size-sensor. LiteLLM and Truesec separately confirmed and analyzed the Mistral AI PyPI leg of the campaign. The common thread tying these together is shared TeamPCP infrastructure, including the C2 domain t.m-kosche[.]com.
The name "Mini Shai-Hulud" is deliberate. The September 2025 Shai-Hulud worm was larger by package count, but the May 2026 variant is more dangerous in mechanism: it weaponizes CI identity rather than human identity, which removes phishing and 2FA from the kill chain entirely.
How the attack worked
The attack chain has three distinct stages, and each one defeats a different control most teams assume protects them.
Stage 1: the Pwn Request
GitHub Actions has two pull-request triggers that look almost identical and behave very differently. The pull_request trigger runs workflow code from the base branch with a read-only token and no access to repository secrets when the PR comes from a fork. The pull_request_target trigger runs the base branch's workflow code but in the context of the base repository, with access to secrets and a read-write token, while checking out untrusted PR code. When a workflow uses pull_request_target and then checks out and executes code from the incoming fork, an external contributor can run arbitrary code in a trusted, secret-bearing context. This is the well-documented "Pwn Request" pattern.
# Illustrative — NOT functional exploit code.
# The dangerous shape: pull_request_target + checkout of untrusted PR head.
on:
pull_request_target: # runs in the BASE repo's trusted context
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # attacker-controlled
- run: npm ci && npm run build # executes attacker code with repo secrets in scope
A malicious pull request that modifies a build script, a config file, or a transitive dev dependency invoked during npm ci/build runs inside the trusted job. From there the attacker has the runner's environment and, critically, network egress.
Stage 2: cache poisoning and OIDC token theft
Two techniques amplified the foothold. First, GitHub Actions cache poisoning across the fork-to-base trust boundary: a cache entry written from a less-trusted context is later restored into a more-trusted workflow run, smuggling attacker-controlled content into the publishing job. Second, and more important, the worm extracted an OIDC token directly from the GitHub Actions runner process memory.
OIDC tokens are the modern, short-lived alternative to long-lived publish secrets. They are minted on demand and expire in minutes. That is normally a security win. But a token that exists in the runner's memory during a job is readable by any code running in that job. The worm scraped it from process memory and used it within its validity window to authenticate to the registry and publish. Short-lived tokens reduce the value of exfiltration to a remote attacker, but they do nothing against in-process theft followed by immediate use.
Stage 3: payload, propagation, and persistence
The published artifacts carried an obfuscated JavaScript payload. In the @antv cluster Microsoft analyzed, the payload was roughly 499 KB, layered Base64 (over 1,700 encoded strings) with PBKDF2/SHA-256-derived encryption, and executed only on GitHub Actions Linux runners while skipping main, master, dependabot/*, renovate/*, and gh-pages branch contexts to avoid noisy detonation.
Once running, the payload harvested credentials from GitHub (tokens, PATs, org secrets), AWS (instance metadata and Secrets Manager), HashiCorp Vault (a dozen-plus token paths), npm, Kubernetes, and 1Password. It then propagated by enumerating packages the compromised identity could write to and republishing them with the payload attached — the defining behavior of a worm rather than a one-shot backdoor. Exfiltration used HTTPS to an encrypted C2 on port 443, with a fallback that abused the GitHub Git Data API to write blobs, trees, and commits. The campaign's signature artifact was the creation of 2,200+ public repositories carrying the reversed description string niagA oG eW ereH :duluH-iahS.
What detection looks like
There are several high-signal indicators, ordered roughly by reliability.
Known-bad versions in lockfiles. The most concrete signal is the presence of a malicious @tanstack/* version published during the 11 May burst. Diff your lockfile against the version set you trusted before 11 May 2026 and treat any @tanstack/*, mistralai, @antv/*, echarts-for-react, or size-sensor version published on or after that date as suspect until confirmed clean.
# Surface @tanstack/* and @antv/* versions resolved in your lockfile for manual review.
jq -r '
.packages | to_entries[]
| select(.key | test("node_modules/(@tanstack|@antv)/"))
| .key + "@" + (.value.version // "?")
' package-lock.json
New large blobs in package tarballs. A @tanstack/* minor or patch release that suddenly ships a several-hundred-KB obfuscated JS file that did not exist in the prior version is a strong tell. Compare tarball contents version-over-version.
The Shai-Hulud repo signature. The reversed-description public repositories are a campaign fingerprint. In a GitHub Enterprise audit log, hunt for repo.create events in the blast window and inspect descriptions.
# Hunt for campaign-signature repos created in the blast window.
gh api -H "Accept: application/vnd.github+json" \
"/search/repositories?q=Shai-Hulud+created:2026-05-11..2026-05-20" \
--jq '.items[] | .full_name + " | " + (.description // "")'
Network IOCs. Outbound connections to t.m-kosche[.]com on port 443 from CI runners or developer endpoints are a TeamPCP indicator across the May 2026 clusters. Treat any runner that contacted it since 11 May as compromised.
CI egress to the GitHub Git Data API from unexpected jobs. Because the worm used blob/tree/commit creation as an exfiltration fallback, anomalous git/blobs or git/trees POST activity from a build job that does not normally write Git objects is worth alerting on.
What to do Monday morning
- Quarantine and pin. Freeze
@tanstack/*,@antv/*,echarts-for-react,size-sensor, andmistralaito known-good pre-11-May versions in your lockfiles, and block resolution of any post-11-May version until you have confirmed it clean. Rebuild from a clean lockfile, not fromnpm install. - Rotate every credential that touched a CI runner since 11 May. This is the load-bearing step. Assume OIDC tokens, npm publish tokens, GitHub PATs, AWS keys, Vault tokens, and Kubernetes service-account tokens that were live in any affected runner are burned. Rotate them and invalidate sessions. GitHub already invalidated 61,274 npm granular tokens with write+2FA-bypass; do not assume that covered yours.
- Audit GitHub Actions triggers. Search every repo for
pull_request_target. Where it is combined with a checkout ofgithub.event.pull_request.head.*, either remove the untrusted checkout, split the privileged work into a separateworkflow_runjob, or gate it behind a maintainer approval. This is the single most valuable structural fix. - Hunt for the repo signature and C2 contacts. Run the audit-log and network queries above across your org. Any developer account that created a Shai-Hulud-signature repo is compromised.
- Move publishing to trusted publishing. Replace long-lived npm publish tokens with OIDC-based trusted publishing scoped to a specific, hardened workflow, and require provenance attestations on publish so consumers can verify origin.
- Re-scan developer endpoints. The payload targeted local credential stores (1Password, AWS, npmrc). Treat any engineer who ran an affected install locally as needing endpoint credential rotation, not just CI rotation.
Why this keeps happening
The September 2025 wave moved through phished human maintainers. The May 2026 wave moved through CI identity. The ecosystem hardened the first attack surface — mandatory 2FA, granular tokens, phishing-resistant keys — and the attackers simply walked around it.
Three structural facts make CI-native supply chain attacks durable. First, pull_request_target is genuinely useful (it is how you label PRs, run privileged checks, and comment on forks) so it will not be removed, and the dangerous variant looks almost identical to the safe one in a YAML diff. Second, OIDC tokens, while a real improvement, are readable by any code in the job that holds them; short lifetime protects against later replay, not against immediate in-process abuse. Third, the worm shape — harvest a write-capable identity, enumerate everything it can publish, republish with payload — turns a single foothold into geometric growth, and registries still publish first and detect later. The gap between "malicious version is live and installable" and "malicious version is yanked" is where the entire campaign lives.
The structural fix
You cannot prevent a maintainer's CI from being targeted, but you can compress the window between a malicious publish and your knowledge of it, and you can shrink the blast radius of any single compromised identity. A continuously-updated malicious-package feed sourced from multiple research vendors plus OSV means a known-bad @tanstack/* or mistralai version surfaces in your inventory in minutes rather than after a CVE lands. Reachability analysis tells you whether the compromised code path is actually invoked in your application, so triage starts with the projects that are genuinely exposed instead of every lockfile that mentions the package. A current SBOM plus maintainer-takeover detection cross-references compromised scopes against your portfolio and flags downstream packages that share an owner with a known-victim account — the second-order signal that matters most when one CI compromise becomes 160 backdoors overnight. None of this prevents the upstream publish; it shortens dwell time and cuts the radius.
What we know we don't know
- The precise OIDC extraction technique and whether it relied on a runner-specific quirk or a generic memory-read primitive has not been fully published.
- The total count of organizations that installed a malicious
@tanstack/*version during the live window is not public. - Whether the TanStack,
@antv, Mistral AI, and UiPath legs were executed by the same operators or by a shared toolkit reused across actors is inferred from infrastructure overlap, not confirmed by attribution. - How many of the 61,274 invalidated npm tokens were actually abused versus invalidated as a precaution is not broken out in GitHub's figures.
References
- TanStack postmortem: npm supply-chain compromise postmortem
- Snyk: TanStack npm Packages Hit by Mini Shai-Hulud
- Wiz: Mini Shai-Hulud Strikes Again: TanStack + more npm Packages Compromised
- Microsoft Security Blog: Mini Shai-Hulud: Compromised @antv npm packages enable CI/CD credential theft
- Orca Security: TanStack and 160+ npm/PyPI Packages Compromised in Supply Chain Worm Attack
- NHS England Digital alert: Supply Chain Attack Affecting Numerous npm and PyPI Packages
- TanStack/router GitHub issue: Several npm latest releases were compromised (#7383)
Internal reading: