Original Shai-Hulud campaign
First worm toolchain; shared payload architecture.
Shai-Hulud has evolved again, the worm starting with TanStack and OpenSearch spread to 400+ packages by no longer needing npm installs to spread.
May 12th, 2026This is a follow-up to our customer advisory yesterday with details following the detection of the TanStack Router malware.
To anybody who's been following the recent malware in the NPM ecosystem, Shai-Hulud is, at this point, a very familiar name. These Dune-themed attacks have brought worms back from computer history into the modern JavaScript ecosystem. The most recent one being the so-called Mini Shai-Hulud, which affected the PyTorch Lightning package on PyPi. Now they are back again, and this time they have a lot more tricks up their sleeve by compromising the TanStack Router packages, and starting a brand new campaign based on Mini Shai-Hulud.
Our Security Research team shipped Semgrep Supply Chain rules for customers 30 minutes after the first published article, and we have been tracking the worm's activity and actively updating the list as it spreads. If you’re a Semgrep customer you can start a new scan now or use the advisories page to see if any projects have installed these package versions recently. As of today, 2026-05-12, the worm appears to be slowing down, whether this is because the storm has passed, or we have just reached the eye, remains to be seen. However, we thought we’d take advantage of the relative peace to have a closer look at the malware and the campaign itself. If you’re interested in the history around the first few campaigns you can read a similar breakdown for the original Trivy compromise on our blog back in March.
This malware is a likely successor to the original Mini Shai-Hulud campaign. The fingerprints are the same, Dune-themed C2 channels, the same cipher family, and roughly the same spreading mechanism, though this has been updated in this variant.
Encryption - a third obfuscation layer (AES-256-GCM encrypted inner modules) makes it significantly harder for researchers to reverse engineer, and harder for victims to understand what was actually stolen.
New secrets - the secrets collector has also had an upgrade, with better support for AWS and AI tooling, in addition to the familiar cloud/infrastructure tokens and crypto wallets
C2 architecture - the familiar GitHub-based dead drop is still there, but the primary C2 domain is now resolved dynamically via RSA-signed commits, making domain takedowns ineffective against active infections, meaning this attack can adapt quickly
GitHub CI/CD spreading - the biggest upgrade. The worm now spreads not just through npm package injection but through GitHub Actions cache poisoning, repo commits, and IDE persistence hooks, meaning victims don't need to run npm install at all to get hit
As has become standard with Shai Hulud, the malware's primary function is to exfiltrate credentials, and it targets a lot of credentials, across cloud environments and developer specific tools. In this latest version we see another upgrade, with deeper support for AWS, the addition of communication like Slack, and not just your AI API keys, but now your MCP servers and configuration too.
Category | Target | Files / Paths / Env Vars |
Cloud / Infra | AWS | ~/.aws/credentials~/.aws/configAWS_SESSION_TOKEN, AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILEAWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKENAWS_SHARED_CREDENTIALS_FILEhttp://169.254.169.254 (IMDS v2)http://169.254.170.2 (ECS) |
GCP | ~/.config/gcloud/application_default_credentials.jsonaccess_tokens.db, credentials.db | |
Azure | ~/.azure/accessTokens.json~/.azure/msal_token_cache.* | |
Kubernetes | KUBERNETES_SERVICE_HOST/PORT~/.kube/config/var/run/secrets/kubernetes.io/serviceaccount/token/etc/rancher/k3s/k3s.yaml/var/lib/docker/containers/*/config.v2.json | |
Vault | VAULT_ADDR, VAULT_API_TOKEN, VAULT_TOKEN_PATHVAULT_TOKEN_FILE, VAULT_AWS_ROLEhttp://vault.*.svc.cluster.local:8200 | |
Terraform | ~/.terraform.d/credentials.tfrc.json | |
Helm | ~/.config/helm/* | |
Dev Secrets | SSH | ~/.ssh/id_rsa, id_ed25519, id_ecdsa, id_dsa, id*/etc/ssh/ssh_host_*_key |
Git | .git-credentials, ~/.git-credentials~/.gitconfig, ~/.config/git/credentials, .git/config | |
npm / yarn | .npmrc, ~/.npmrc, ~/.yarnrchttps://registry.npmjs.org/-/npm/v1/tokens | |
PyPI | ~/.pypirc | |
Docker | ~/.docker/config.json/root/.docker/config.json | |
Env files | /.env, /.env.local, **/.env.production | |
Database | /config/database.yml, /wp-config.php | |
Misc | ~/.netrc, ~/.ansible/*, ~/.config/filezilla/sitemanager.xml | |
Shell history | ~/.bash_history, ~/.zsh_history, ~/.python_history~/.node_repl_history, ~/.mysql_history, ~/.psql_history~/.history, ~/.lesshst, ~/.viminfo | |
AI Tools | Claude | ~/.claude.json, .claude.json, ~/.claude/mcp.json |
Kiro | ~/.kiro/settings/mcp.json, .kiro/settings/mcp.json | |
Crypto | Wallets | ~/.bitcoin/wallet.dat, ~/.ethereum/keystore/*~/.monero/*, ~/.zcash/wallet.dat, ~/.dash/wallet.dat~/.dogecoin/wallet.dat, ~/.litecoin/wallet.dat~/.electrum/wallets/*, ~/.electrum-ltc/wallets/*~/.config/Exodus/exodus.wallet/*~/.config/Ledger Live/*, ~/.config/atomic/Local Storage/leveldb/* |
VPN | Configs | NordVPN, ProtonVPN, CyberGhost, PIA, Windscribe, EarthVPN, OpenVPN Linux/macOS/Windows paths |
Comms | Apps | ~/.config/Slack/CookiesDiscord, Telegram Desktop, Signal, Element (Matrix), WeeChat IRC~/.purple/accounts.xml (Pidgin)KDE Wallet, GNOME Keyring |
Of course this is still, at its core, a worm, and therefore the goal of this malware is to spread. The initial delivery is clever: the attacker staged a fake @tanstack/setup package in a fork of the legitimate TanStack/router repository, referenced as an optionalDependencies entry. The URL appears to point at the real TanStack repo, but resolves to the attacker's fork via GitHub's shared object storage. When TanStack's own build pipeline installs this dependency, a prepare hook fires and executes the initial payload, this is how the worm got into TanStack in the first place.
The re-spreading mechanism into downstream victims works differently, and actually has two distinct paths depending on what credentials the worm manages to steal. On the CI/CD path, it injects the same optionalDependencies entry pointing back to the attacker's fork and bumps the version by three patch increments, so every downstream npm install triggers the payload again. On the npm token path, it copies the main payload as router_init.js, drops setup.mjs at the package root, injects preinstall: "node setup.mjs" into package.json, and bumps the version by one patch increment instead. In both cases the result is another infected downstream package, but the mechanism, hook, and version bump differ depending on which credential type was available.
The code is not particularly readable...
In the previous Mini Shai-Hulud attack the malware used one obfuscation layer (obfuscator.io) plus one cipher layer. This new malware adds a third layer: 11 inner modules are AES-256-GCM encrypted and gzip-compressed. You need to crack beautify() first, use it to decrypt the AES keys, then decrypt each module. So it's clear with this updated Shai-Hulud that the attackers wanted to make it a bit more challenging for us to investigate their work. But we'll put this aside for a moment and go back to the spreading mechanism.
When we last saw Mini Shai-Hulud, it had wormed its way into PyPI and Packagist, but fundamentally the payload itself stayed rooted in the npm/JavaScript ecosystem, relying on those package managers to run npm install. The authors though seemed pleased with the efficacy of the switch-up, and this version includes a few GitHub-specific surprises.
If the malware is running inside a GitHub Actions environment, the malware has another twist. It will abuse the GitHub OIDC token in order to push the malware to the new repositories via npm’s Trusted Publisher system, attesting the validity of the publishing pipeline. Before going back to scooping up tokens from the GitHub Action environment too. The necessity of this is somewhat ironic, after the original Shai-Hulud attack, npm introduced a requirement that packages which had previously used Trusted Publishers must continue to use it going forward. The idea was to prevent this kind of token-theft attack: even if someone steals your npm token, they can't publish without also controlling the designated workflow. But that also means no npm token was stolen either.
By using short-lived, scoped credentials that are generated on-demand during your CI/CD workflow, Trusted Publishing removes the need for long-lived tokens, meaning you reduce potential security risks associated with credential management
And GitHub Actions and Trusted Publisher for npm is also how the attacker got into TanStack's publishing pipeline in the first place. Three vulnerabilities, chained together.
Bundle-size.yml used the pull_request_target trigger also called a “Pwn Request”, this is well known enough flaw that we have an open source rule for it (which would have alerted at this point).
The Pwn Request runs with base repository permissions but checks out fork-controlled code. The attacker opened a PR from a renamed fork (zblgg/configuration, deliberately obscured to evade fork-list searches), force-pushed a malicious commit containing vite_setup.mjs, and triggered the workflow to execute it.
That execution poisoned the GitHub Actions pnpm cache under the exact key TanStack's release workflow would later look up. A technique across the fork/base trust boundary first documented by Adnan Khan in 2024.
When a legitimate maintainer merged an unrelated PR later that day, release.ymlrestored the poisoned cache.
The attacker's code ran inside a workflow with id-token: write permissions, dumped the runner process memory, extracted the OIDC token, and used npm's Trusted Publisher system to publish 84 malicious packages in under six minutes.
If you’re actively using GitHub Actions, we strongly recommend reading through the full write up published by TanStack, as each variant of Shai-Hulud gets its initial foothold using the same few methods. Anyway, back to the victims.
It’s not just npm either: with a valid GitHub token, the malware will also commits itself into every repository the victim has write access to, planting .claude/router_runtime.js (a verbatim copy of the malware), a poisoned .claude/settings.json, and a malicious .vscode/tasks.json. These last two are the persistence: every time anyone opens an infected repository in Claude Code, a SessionStart hook fires and re-executes the payload, infecting their machine. VS Code's folderOpen does the same. So the worm spreads not just through package installs but through the ordinary act of opening a project. That means that any developer who clones an infected repo and opens it in their IDE becomes the next victim, whether or not they ever run npm install or even have npm installed in the first place. This is probably also why it spread so quickly, no longer reliant on just npm and with the experience they gained with their PyPi and Packagist.
So now the malware has your tokens, what does it do with them? The exfiltration mechanism for GitHub specifically hasn’t got much of an update here but it’s important to note this is a fall back, active only when the initial C2 server and backup are both down. The change here is if the victim has no GitHub orgs, it adds the ominous commit message: IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner:<token>, using GitHub’s public, anonymous API, the attackers could then search for those tokens and retrieve them later.
These two examples are likely tests by the malware authors, as the original C2 infrastructure is still up and running
The new intimidation method isn’t just to scare the victim. The malware installs a dead-man's switch: gh-token-monitor.sh, running as a systemd user service on Linux or a LaunchAgent on macOS. It polls api.github.com/user with the stolen token every 60 seconds. If the token is revoked and the API returns a 40x, it runs rm -rf ~/. If a user is in a GitHub Org, the malware takes a more sneaky approach, failing back to “Add files”. And then if the dead drop fails for any reason, it will retry a few times, and if the credential file is too big, split it up and chunk it. The attackers are now adding more resilience to the malware than the previous attacks, and this is a big theme of the next few changes, because GitHub dead drops are not the only exfiltration method, they’re not even the main exfiltration mechanism.
The primary exfiltration channel though is not GitHub, it’s a domain called git-tanstack[.]com, a legitimate-enough looking version of the TanStack domain. But what makes this infrastructure resilient is how the malware finds it. Rather than hardcoding the domain git-tanstack[.]com and, it being taken down by the attackers, there’s a new mechanism in this attack. If the original URL fails, the malware resolves the live C2 address dynamically at runtime by searching GitHub's public commit search API for commits containing the string thebeautifulsnadsoftime. Each matching commit is checked against a regex that extracts a base64-encoded domain and an signature, verified against a public key embedded in the malware. Unfortunately we can’t do a Marcus Hutchins here and stop it in its tracks. Only the attacker, holding the corresponding private key, can publish valid beacons. If the decoded domain validates, it overrides the current version completely.
This means a domain takedown does not kill active infections. The attacker can silently rotate to a new C2 domain at any time by pushing a single GitHub commit to any public repository. Infected machines will pick up the new address on their next execution cycle.
For resilience on top of resilience, there is a second exfiltration channel for when the original channel is down, Session messenger, a decentralised encrypted messaging platform. Data is batched into 100 KB envelopes,encrypted, and dispatched across the Session network as a fallback if the primary channel is unavailable.
And if that Session down too it moves to the original Shai-Hulud GitHub exfil method. It creates a brand new public repo in the victim's account using their stolen token. The repo gets a random name with the description "Shai-Hulud: Here We Go Again". The random name combined Dune factions and characters (of course): fedaykin-melange-500.
The result is a distributed, dynamic C2 architecture with no single point of failure: one domain can be taken down and seized, but the malware keeps communicating. So far we’ve not seen any changes so you should keep an eye out for git-tanstack[.]com still.
And it's clear that these changes have been extremely successful for this campaign. After the original TanStack compromise, the worm's self-propagation mechanic did the heavy lifting. While the initial breach just covered TanStack, the attackers launched a parallel attack targeting UiPath, starting with @uipath/apollo-core, a design token and icon library for the UiPath platform, and from there burned through the entire @uipath/* namespace, 57 packages in total. After that, Shai-Hulud ended up in @squawk/* likely also via TanStack. Squawk is a TypeScript aviation library suite covering everything from airport data and navaids to instrument approach procedures and FAA CIFP flight planning.
The Python credential stealer was replaced shortly after researchers found it with an attribution message h/t Wiz
From there it appeared in @mistralai/mistralai, the official TypeScript client for the Mistral AI platform, and hit PyPI with mistralai@2.4.6 and guardrails-ai@0.10.1. The Python variant this time around doesn’t just use npm and the original payload, instead the malicious package fetches a python file from the C2, a standard, unobfuscated credential stealer (which has since been removed and replaced with an attribution message from TeamPCP).The Python variant's distinct payload architecture and Mistral AI's position entirely outside the npm ecosystem suggest that this is another parallel attack, likely via the same GitHub Actions attack vector or via credentials harvested in earlier campaigns.
This isn’t to say that the worm was ineffective, the npm worm was busy at work too reaching the popular @opensearch-project/opensearch. This package is the official Node.js client for the OpenSearch distributed search and analytics engine, with multiple versions compromised in a few hours. The scatter of smaller victims in the list tells the rest of the story: @beproduct/nestjs-auth across 18 consecutive patch versions, @tallyui/* spanning nine e-commerce UI packages, safe-action, ts-dna, git-branch-selector, each almost certainly a developer whose CI runner installed a compromised package from the initial victims, handed the worm their credentials, and became the next hop in the chain without ever knowing it.
First worm toolchain; shared payload architecture.
Aqua Security scanner; OIDC token extraction debut.
Same actor; preinstall hook + CI/CD credential theft.
mbt@1.2.48, @cap-js/sqlite@2.2.2 + 2 others. Preinstall hook; Claude Code abused for CI.
lightning@2.6.2 + 2.6.3; import-time hook; 97M total downloads.
361K weekly downloads. Worm propagated via stolen SAP CI/CD OIDC tokens.
Renamed to zblgg/configuration to evade fork-list searches. Draft PR “WIP: simplify history build” opened.
Identity spoofed as claude@users.noreply.github.com. [skip ci] prefix suppresses checks.
Attacker code executes; malicious pnpm store injected into GitHub Actions cache.
42 packages. OIDC tokens lifted from /proc/<pid>/mem; release pipeline hijacked. First documented case of valid SLSA Build Level 3 provenance on malicious packages.
Step Security and Socket confirm this is a true positive. We start writing rules for Semgrep Supply Chain customers
Unpublish scripts run. Maintainer Tanner Linsley confirms the orphaned-commit vector; npm team alerted.
Coordinated disclosure: npm + GitHub Security + StepSecurity push registry-wide removal of compromised versions.
apollo-core + CLI + agent SDKs. Same C2 infra as TanStack, different campaign key.
Python variant: 13 lines in __init__.py downloads transformers.pyz from git-tanstack.com.
LLM guardrails library. Same Python dropper; wipes home dir if IL/IR timezone.
Worm propagated via stolen tokens from prior victims. CI/CD pivot chain.
Self-propagation: worm enumerates all packages each victim maintains, poisons each one.
518M+ cumulative downloads. 359 GitHub repos created with encrypted stolen credentials — “Shai-Hulud: Here We Go Again.”
CVSS 9.6 critical. Mitre, CISA, and major registry operators issue coordinated advisories.
If you were to diff this version of Mini Shai-Hulud and the one that affected the Lightning package on PyPi, you’d be forgiven for thinking they were completely different attacks that just happened to use the same Dune theme. The Dynamic C2 rotation. The third encryption layer to slow analysis. Sigstore attestation abuse. GitHub Actions cache poisoning. Runner memory extraction to bypass secret masking. A dead-man's switch that wipes your home directory if you revoke the stolen token. IDE hooks for both VS Code and Claude Code. Yes this version of the malware is more complex, but those additions map onto something defenders did or something npm shipped after the previous attack. They are reading the postmortems.
We don’t really know how bad the blast radius of this one actually is (nor honestly, the previous attacks either), as the GitHub deadrops are the final fallback, so all credentials have gone directly to the attackers and do not appear in commits. The malicious versions were live for roughly twenty minutes before detection, but twenty minutes of access to packages in the TanStack ecosystem, combined with the double whammy of an attack that is designed for CI/CD runners, and that can adapt and spread across GitHub now? Between this attack and all the previous attacks, the attackers may be sitting on a goldmine of credentials and access, and yes: Some of those tokens are probably already revoked, especially from the earlier attacks. But some are probably still live, even today. Thanks to their new gh-token-monitor daemon, for this one they’ll know which is which, every sixty seconds.
The thing that should worry you isn't this single attack. It's that this actor is clearly iterating, has demonstrated they can get into CI pipelines without stealing credentials, and is now sitting on whatever they harvested. What they do with that is the question. Because the answer to that question may never come from this version of the payload.
Of course further supply chain attacks, with this payload are the obvious play and the list of affected packages from this specific version of the attack is growing, but cloud account access from harvested AWS keys, given their new in depth AWS crawler? Or perhaps they’ll pivot from IDEs into more generic tooling like their new AI or communication crawlers next?
It’s hard to say, but If you installed any affected @tanstack/* packages or the downstream victims on May 11, find and kill gh-token-monitor.sh first, then rotate everything. In that order.