Analysis
The worm's propagation loop closed in 24 hours. On April 29, 2026, the Shai-Hulud campaign compromised four SAP CAP npm packages and used those victims' CI/CD pipelines to steal GitHub Actions OIDC publishing tokens. By 14:41 UTC on April 30, those stolen tokens published intercom-client@7.0.4 — a package with roughly two million weekly downloads — as the worm's next infected host. Each victim becomes the next launchpad; each stolen npm publish token extends the worm's reach to every package that token can access. A self-propagating npm supply chain attack is no longer theoretical. The April 29 wave we tracked as Mini Shai-Hulud wasn't the campaign's conclusion — it was its first propagation step.
What we know
- April 29, ~04:00 UTC —
mbt@1.2.48and@cap-js/sqlite@2.2.2published with Bun-based credential stealer payloads. GitHub Actions OIDC publishing tokens from these packages' CI pipelines are stolen by the payload as it executes on compromised environments. - April 30, 14:41 UTC —
intercom-client@7.0.4published via npm OIDC publishernpm-oidc-no-reply@github.comusing a token stolen from the April 29 victims. Package size jumps from 6 MB to 17.8 MB. SLSA v1 provenance attestations present in7.0.3are absent in7.0.4. Apreinstallhook appears for the first time in the package's history. - April 30 — StepSecurity identifies and reports the compromise. The malicious version is removed from the npm registry. Safe version:
intercom-client@7.0.3.
The SLSA attestation gap is the detection signal that mattered here. StepSecurity's tooling flagged the attestation absence before payload analysis confirmed the compromise. A package that has shipped provenance for every prior release dropping it on a patch bump is a high-confidence indicator — worth automating into CI gates.
How the worm propagates
The payload runs in four stages. Stages 1–3 are the credential theft; Stage 4 is what makes this a worm rather than a stealer.
- Stage 1 — Preinstall hook. A new
preinstallscript inpackage.jsonrunsnode setup.mjsbefore any install logic executes. - Stage 2 — Bun runtime loader.
setup.mjsdetects OS and architecture, downloads Bun v1.3.13 from GitHub releases, and uses it to executerouter_runtime.js— an 11.7 MB single-line obfuscated file (zero newlines, structural obfuscation). Running under Bun instead of Node evades EDR and SIEM rules tuned for suspiciousnodechild processes spawned during package install. The payload daemonizes itself by forking a detached child process with__DAEMONIZED=1, breaking process-tree correlation betweennpm installand the credential theft that follows. - Stage 3 — Multi-cloud credential sweep. The payload queries the AWS Instance Metadata Service at
169.254.169[.]254for IAM role credentials; queriesmetadata.google[.]internalfor GCP service account tokens; scans for Azure storage connection strings (AccountKey), client secrets, and access keys; extracts PEM-encoded private keys and common secret variable names. All stolen material is exfiltrated to a private repository created under the victim's own GitHub account — all traffic toapi.github.com, which is allowlisted in virtually every corporate firewall and CI/CD egress policy. - Stage 4 — Worm propagation. Every stolen npm publish token is used to enumerate packages the token can publish to, increment the patch version, inject the
preinstallhook and payload files into the tarball, and publish a new malicious version. This is the step that turned the April 29 SAP victims into the April 30 intercom-client launchpad.
What evolved from April 29
Two changes distinguish the intercom-client payload from the Mini Shai-Hulud wave, both in the direction of operational quietness:
- Exfiltration moved from public to private repos. The April 29 payloads created public GitHub repositories with the description
A Mini Shai-Hulud has Appeared— a detectable public signal. The intercom-client payload creates private repositories. Repository-monitoring approaches that worked against the April 29 wave no longer apply. The OPSEC iteration happened within a single propagation cycle, within 24 hours. - Multi-cloud credential sweep expanded. The April 29 campaign focused primarily on GitHub tokens, npm publish tokens, and GitHub Actions secrets. The intercom-client payload adds explicit AWS IMDS queries, GCP metadata service lookups, and Azure-specific connection string patterns. The target surface grew with the target audience: intercom-client users are more likely to be cloud-native SaaS operators than SAP CAP developers.
Targeting
- B2B SaaS and customer success teams — intercom-client is the official Intercom Node.js SDK, used across customer support, onboarding, and CX platforms. Roughly two million weekly downloads. Any team that installed it via
npm installornpm ciduring the April 30 window without a lockfile pinning7.0.3ran the payload. - CI/CD runners with npm publish access — environments that installed the malicious version and held npm publish tokens for other packages are the worm's next propagation targets. Every package those tokens can publish is the next candidate for a malicious patch release.
- Environments with cloud credential access — AWS EC2 instances without IMDSv2 enforcement, GCP workloads with reachable metadata, Azure environments with connection strings in environment variables.
Indicators of compromise
Compromised package:
intercom-client@7.0.4— malicious. Safe version:intercom-client@7.0.3.
File hashes (SHA-256):
setup.mjs—fe64699649591948d6f960705caac86fe99600bf76e3eae29b4517705a58f0e2router_runtime.js—5ae8b2343e97cc3b2c945ec34318b63f27fa2db1e3d8fbaa78c298aa63db52ed
Package-level detection signals:
- Package size: 6 MB → 17.8 MB in a single patch bump
- SLSA v1 provenance attestations present in
7.0.3, absent in7.0.4 preinstallhook introduced for the first time in the package's publish historyrouter_runtime.jsis a single 11.7 MB line with zero newlines
Behavioral signals:
- Bun v1.3.13 download from
github.com/oven-sh/bun/releasesduringnpm installon a host that doesn't legitimately use Bun - Private GitHub repository creation by CI service accounts in the window immediately following an install step
- Detached child processes with
__DAEMONIZED=1environment variable surviving beyondnpm installexit - Outbound connections to
169.254.169[.]254ormetadata.google[.]internalfrom a process not normally querying instance metadata
Related April 29 packages (same campaign):
mbt@1.2.48@cap-js/sqlite@2.2.2
Detection and mitigation
- Pin to the safe version. Downgrade to
intercom-client@7.0.3and runnpm cache clean --force. Verify lockfiles across every active repository. - Rotate all exposed credentials. If
intercom-client@7.0.4was installed in any environment, treat all credentials reachable from that environment as compromised: AWS IAM credentials, GCP service account tokens, Azure connection strings and client secrets, GitHub tokens (PAT, OAuth, and OIDC), npm publish tokens, SSH keys, and any API keys in environment variables or config files. - Audit for worm propagation. If the compromised environment held npm publish access to other packages, check the npm registry for unexpected patch bumps. Compare published tarballs to git tags for any package the environment's tokens could access.
- Review GitHub audit logs for private repository creation by service accounts in windows immediately following CI install steps. Cross-reference with install logs from April 30 onward.
- Enforce IMDSv2 on all EC2 instances and container workloads. Require IMDSv2 (hop limit = 1) to prevent unauthenticated IMDS credential queries. This blocks Stage 3 AWS harvesting without payload analysis.
- Enable
npm install --ignore-scriptsby default in CI. Explicitly allowlist packages that require lifecycle hooks. This is the structural defense that blocks the entire preinstall vector — CVE-2026-12091, Mini Shai-Hulud, and intercom-client all run through it. - Gate on SLSA attestations. Any patch release that drops provenance should trigger investigation before install. Automate this check in CI rather than treating it as a manual review item.
Attribution
The router_runtime.js payload carries the toolchain fingerprint tracked across the broader TeamPCP campaign: the __decodeScrambled PBKDF2 obfuscation cipher appears 232 times, the same count and structure as payloads in the Trivy, Bitwarden, Checkmarx, xinference, and April 29 SAP/Lightning compromises. That's shared tooling. Whether it means a single operator, a toolchain sold or shared between groups, or a fork is not settleable from public evidence. We use "Shai-Hulud" as the campaign name for the self-propagating npm worm and treat the TeamPCP toolchain overlap as a linking indicator, not a hard attribution.
Criminal-market signal
We ran comprehensive dark-web sweeps across 19+ live onion search engines for both shai-hulud worm and intercom-client npm on May 4, 2026 — five days after the intercom-client compromise was disclosed. Both returned clean negatives.
shai-hulud worm— 23 pages analyzed: 5 exploit forums, 16 marketplaces, 2 other. Zero findings. No broker pricing, no actor discussion, no IOC references.intercom-client npm— 76 pages analyzed: 13 exploit forums, 59 marketplaces, 3 news mirrors. Zero findings on the supply-chain compromise specifically. The unrelated CVE mentions surfaced in results (CVE-2023-38545, CVE-2024-3094, CVE-2021-44228) confirm the forums are active and indexed — the clean negative is genuine, not a search failure.
This matches the pattern established across every Shai-Hulud-adjacent campaign we've swept. The Mini Shai-Hulud wave (19 pages, 2 exploit forums, 16 marketplaces) returned clean in April. The TeamPCP campaign (18 pages, 2 exploit forums, 15 marketplaces) returned clean in March. Three sweeps, three clean negatives, same campaign cluster.
The contrast with CopyFail is instructive. CVE-2026-31431 crossed to a carding forum's Exploits section in seven days. Shai-Hulud, after five days and 76 pages of active forum coverage, has no criminal-market presence. The difference is the business model: CopyFail is a reusable primitive that any ransomware operator can compose with any foothold. Shai-Hulud is the operator's own delivery infrastructure — they're not selling the capability, they're using it. There's no market because the attacker and the tool are the same entity.
This shapes where defenders should look. The Shai-Hulud threat signal lives in SLSA attestation gaps, npm publish audit logs, package size anomalies, and CI egress to GitHub releases endpoints — not in .onion marketplaces. The news mirrors that did appear in the intercom-client sweep were clearnet security coverage reaching Tor relay nodes; the exploit forums returned nothing. Monitor the registry, not the forum.
What the 24-hour loop means
The propagation velocity is the finding. The worm iterated its own OPSEC — moving from public to private exfil repos — within a single propagation cycle. The multi-cloud sweep expanded to match the new victim profile. Neither change required human intervention; they were already in the payload, triggered by which environment the worm landed in next. That's an automated adversarial adaptation loop running faster than most incident response timelines.
The structural defense is the same one that applied to CVE-2026-12091 and Mini Shai-Hulud: --ignore-scripts by default, lockfile pinning, IMDSv2 enforcement, SLSA provenance as a publish-time gate. None of that is new. What changed is the timeline defenders need to operate on: 24 hours from initial compromise to two-million-download package infected is not a CVE-cadence problem. It's an automated response problem.
SLSA attestation gaps, Bun egress from CI runners that don't use it, and unexpected patch bumps on packages your tokens can publish are the signals worth automating. The worm's five-day-old presence across 76 dark-web pages left no trace. Its presence in the npm registry left size metadata, a missing provenance attestation, and a new preinstall hook — all detectable before payload execution.