At 09:00 UTC on August 4, 2026, an attacker pushed two files to the main branch of the keyv GitHub repository using a compromised maintainer account. By 13:20 CEST that same day, at least 868 packages spanning 1,381 versions had been trojanized, a number that continued rising as we published this post. The malicious versions were signed with valid GitHub Actions provenance, meaning they passed npm’s trusted publishing checks without a flag. Every environment that ran npm install against a caret-pinned or unpinned dependency in the keyv, cacheable, flat-cache, file-entry-cache, or cache-manager namespaces during that window downloaded live malware, with provenance attestation intact.
The campaign, publicly named “Shai-Hulud: Here We Go Again” by the attacker’s own GitHub repository descriptions, is a direct descendant of the Mini Shai-Hulud malware family and shares delivery architecture with the earlier TeamPCP and antv supply chain campaigns documented by Wiz Research. The payload is not novel. The scale is.
What the Attacker Compromised First
The initial foothold was the GitHub account of jaredwray, the maintainer behind keyv (127 million weekly downloads), cacheable (29 million monthly downloads), flat-cache (565 million monthly downloads), file-entry-cache (557 million monthly downloads), cache-manager, and cacheable-request. Read that again: a single GitHub account compromise gave the attacker publish rights to packages with a combined monthly install count in the billions.
The attacker used that access to push setup.mjs and Math_Symbol.js directly to the repository main branch, then immediately triggered a new release through GitHub Actions. Because the release was generated by the repository’s own workflow, the resulting npm packages carried valid GitHub Actions provenance: the same mechanism npm introduced specifically to let consumers verify that a package was built from a known source. In this incident, provenance verification confirmed that a credential stealer was legitimately published by the project’s own CI pipeline. The commit that introduced the hook files is cryptographically signed by GitHub with its author field set to github-actions[bot]; that signature is authentic, which is precisely the problem.
Every downstream package the worm republished from stolen tokens, including the @arv-bedrock, @deliveroo, @hubsync, and ServiceTitan packages, carries no provenance attestation at all. The asymmetry matters: tooling that checks for OIDC or SLSA provenance flags the downstream spread immediately, but passes the initial keyv release without a warning. The most dangerous package in this incident is the one that looks the most legitimate.
How the Worm Spreads
The worm operates in two phases.
Phase one runs on every npm install that touches a poisoned package. The preinstall hook added to every trojanized package.json is a single line:
"preinstall": "node setup.mjs"setup.mjs downloads a standalone Bun runtime (fingerprinted as Bun/1.3.13 in outbound traffic) to a temporary directory under /tmp/bun-dl-*/. It then executes Math_Symbol.js, a 728 KB, heavily obfuscated Bun bundle, under that runtime.
Phase two is what makes this a worm rather than a stealer. Math_Symbol.js sweeps the environment for credentials, then calls https://registry.npmjs.org/-/npm/v1/tokens to enumerate every npm package the stolen token controls. For each package the token can publish to, the worm downloads the current tarball, injects the same setup.mjs / Math_Symbol.js payload, and republishes it as the next patch version. The worm writes itself into every package the victim can touch and keeps going.
The worm’s lock file on disk is tmp.dpkg_14527.lock, which prevents simultaneous runs on the same machine.
Credential Targets
The full target list in Math_Symbol.js was expanded by approximately 70% from the earlier Shai-Hulud variant. Beyond the standard credential sweep (GitHub tokens, npm tokens, AWS credentials at ~/.aws/credentials, GCP and Azure CLI configs, HashiCorp Vault and Kubernetes configurations, SSH private keys, .env files, generic secret sweeps via TruffleHog-style regex), the payload added several targets not present in prior waves:
AI coding agent credential stores: .claude/, .codex/, .cursor/, .gemini/ directories. Cryptocurrency keystores: Foundry, Solana, and Monero wallets. Self-hosted CI secrets: Jenkins master.key, Argo CD credentials, Harbor registry secrets. Cloud providers: Alibaba Cloud and Tencent Cloud CLI configurations. System files: /etc/shadow.
The persistence mechanism plants a setup.mjs file in .claude/ and .vscode/ in every repository the worm touches, registering a SessionStart hook and a VS Code runOn: folderOpen task respectively. These survive package uninstallation.
C2 Architecture
The worm does not embed a C2 URL in the payload. Instead, it makes an eth_call to an Ethereum smart contract at address 0xE1f2395ee43e45A1556EC6438a88c31B83493103 (contract type: StringListStore) to retrieve the current list of exfiltration endpoints. The contract was initially configured with three domains and subsequently updated to return only npm-cache[.]com (resolving to 104.21.35.216 via Cloudflare). The contract owner address was funded by an address flagged previously for scam activity.
This design lets the operator rotate C2 infrastructure without touching the payload. The Ethereum state stores the current domain list; the payload stays byte-identical across all 443 packages.
If the primary C2 is unavailable, the payload falls back to GitHub. Using any stolen GitHub Personal Access Token in its collection, the worm creates a public repository named from Dune vocabulary (sardaukar, fremen, atreides, sandworm, melange, and others) with the description Shai-Hulud: Here We Go Again, and commits encrypted, compressed exfiltration results under results/. SafeDep counted 546 repositories matching that description created specifically on August 4, 2026, each holding a results/ directory. The broader GitHub search returns a higher total, but those additional results are artifacts of prior Shai-Hulud waves, not this one.
The dead-man’s switch present in earlier Shai-Hulud variants has been updated. The C2 can now respond with a code field that arms the switch and specifies which token to monitor for revocation. The switch checks api.github.com/user for token validity, meaning any GitHub token can be used as the trigger. Exfiltrated credentials are serialized to JSON, gzipped, encrypted with a random AES-256-GCM key, and wrapped with an embedded RSA public key before transmission. The C2 can respond with a code value that is passed directly to eval, providing full remote code execution inside the compromised process.
// eth_call to StringListStore at 0xE1f2395ee43e45A1556EC6438a88c31B83493103
// RPC endpoints: eth-mainnet.nodereal[.]io, go.getblock[.]io, eth.llamarpc[.]com
// Returns current exfiltration domain list; allows C2 rotation without payload modificationA fingerprint string embedded in the initial commits serves as an intimidation mechanism: IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients. It appears in both the malicious file pushes to the keyv repository and in outbound commit messages where the worm introduces IDE hook files to victim repositories. The commit message for those IDE hook additions is chore: update config.
The 30 Packages We Investigated
The 30 OSV advisories reviewed for this post span five distinct namespace families, each representing a separate victim account whose npm token the worm harvested and used to republish.
@keyv namespace (MAL-2026-12007 through MAL-2026-12015): The worm reached the @keyv scope directly from the initial jaredwray compromise. Nine packages were poisoned at version 6.0.0, published between 13:01 and 13:02 UTC on August 4. The packages include @keyv/bigmap, @keyv/cloudflare-kv, @keyv/compress-brotli, @keyv/compress-gzip, @keyv/compress-lz4, @keyv/dynamo, @keyv/encrypt-node, @keyv/encrypt-web, and @keyv/etcd: all storage adapters and utilities maintained under the same account. All nine carry the same SHA-256 payload hash. We confirmed this against the malicious-packages-origins SHA-256 fields in the OSV advisories, cross-referenced against the Wiz IOC CSV.
@cacheable namespace (MAL-2026-11558 through MAL-2026-11561): The worm reached the @cacheable scope at 11:34 UTC through a token that controls caching utilities also owned by jaredwray. Four packages were poisoned: @cacheable/memory (2.2.1), @cacheable/net (2.1.1), @cacheable/node-cache (3.1.2), and @cacheable/utils (2.5.1). Registry metadata read from registry.npmjs.org confirms the prior clean version of @cacheable/memory was 2.2.0, published 2026-06-27. The malicious 2.2.1 timestamp is 2026-08-04T10:11:29.866Z.
@arv-bedrock namespace (MAL-2026-11553 through MAL-2026-11557): A five-package cluster published between 15:18 and 15:21 UTC. The packages are @arv-bedrock/auth, @arv-bedrock/auth-admin, @arv-bedrock/auth-sso, @arv-bedrock/auth-sso-backend, and @arv-bedrock/logger. The version numbering (1.1.7, 1.0.2, 1.6.1, 1.7.1, 1.7.1) follows the natural increment pattern of active projects, meaning the trojanized versions slot into version ranges without triggering semver anomaly checks.
@adminide-stack namespace (MAL-2026-11551 through MAL-2026-11552): Two packages, @adminide-stack/clock-tik-browser (12.0.24) and @adminide-stack/yantra-mobile (12.0.33), published at 16:03 and 18:00 UTC respectively. The @adminide-stack scope is associated with a multi-package IDE toolchain, making the high major version numbers (12.x) consistent with active development rather than a version anomaly flag.
ServiceTitan internal toolchain (MAL-2026-11990 through MAL-2026-11991): The two verdaccio packages (verdaccio-okta-oauth and verdaccio-tarball-local-storage) are published and maintained by the st-team npm account, which we confirmed from registry metadata as the publisher behind 150+ ServiceTitan internal npm packages dating back to 2021. From registry metadata, the last legitimate st-team publish of both packages was version 38.1.0 on 2026-07-29. Starting at 10:41 UTC on August 4, a worm-controlled process published 16 successive versions (38.1.1 through 38.1.16) for each package in rapid succession, roughly one version every four to eight minutes, stopping at 12:04 UTC. The publisher field in the registry JSON for all 16 versions is absent (the worm published without associating a named user to the token call), which is itself an anomaly against the st-team baseline of every prior version.
38.1.0 2026-07-29T18:32:48Z pub: st-team [clean]
38.1.1 2026-08-04T10:41:16Z pub: (absent) [MALICIOUS: worm]
38.1.2 2026-08-04T10:42:39Z pub: (absent) [MALICIOUS]
38.1.3 2026-08-04T10:45:33Z pub: (absent) [MALICIOUS]
...
38.1.16 2026-08-04T12:04:05Z pub: (absent) [MALICIOUS]This pattern is characteristic of the worm cycling through its republish loop: one controlled token, 16 automatic patch publishes before either the token was revoked, network conditions changed, or the worm moved to the next target in its queue.
@deliveroo namespace (MAL-2026-11562 through MAL-2026-11563): Two packages, @deliveroo/determinator and @deliveroo/reevent. The registry metadata for @deliveroo/determinator reveals one of the more striking gaps in this campaign: its prior published version was 0.2.0, timestamped 2018-03-14T13:18:26.918Z. The malicious 0.2.1 was published 2026-08-04T10:33:58.630Z, a gap of eight years and four months. The account holding publish rights to this package had been dormant since 2018. A developer who inspects this package’s download history or version cadence sees a single entry from 2018 followed by a single entry from yesterday. The 0.2.0 version and all prior versions remain clean.
@hubsync namespace (MAL-2026-11564): @hubsync/web-sdk-react was hit harder than any other single package in the batch we reviewed. Registry metadata shows the worm published 27 malicious versions in rapid succession, from 6.3.7 through 6.3.33, starting at 13:24 UTC. The Wiz IOC CSV confirms all 27 versions. Each is a separate, independently poisoned release of the same payload.
vitest-preview-pro and webdev-conf (MAL-2026-12004 and MAL-2026-12005): Published at 23:56 and 23:57 UTC respectively, the latest timestamps in our batch, suggesting the worm was still active on a second or third victim account more than 14 hours after the initial jaredwray compromise. OSV advisories for these two packages contain only the brief GHSA-sourced notice without the fuller Shai-Hulud campaign context, consistent with advisories published before the Aikido/Socket/SafeDep attribution write-up was attached to the OSSF record.
umadev (MAL-2026-11989): A development tooling package (umadev: a Rust coding agent coordinating a real development team over five first-class base CLIs) published by the umayun account. The malicious version 1.0.74 was published at 13:18 UTC. Registry metadata shows the prior version 1.0.73 was published 2026-08-01T21:16:58.142Z by the umayun account in the normal cadence for this package (daily to every two days). The worm reached the umayun token and published 1.0.74 without a named publisher in the registry JSON, the same absent-publisher anomaly seen across ServiceTitan’s st-team versions.
IOC Table
| Indicator | Type | Value | Method |
|---|---|---|---|
| keyv | npm package | 6.0.0 | Confirmed malicious in OSV advisory MAL-2026-12007 area; registry time field shows 2026-08-04T09:35:00.763Z publish |
| @cacheable/memory | npm package | 2.2.1 | Version 2.2.1 timestamp 2026-08-04T10:11:29.866Z extracted from registry metadata; prior clean version 2.2.0 on 2026-06-27 |
| @deliveroo/determinator | npm package | 0.2.1 | Registry metadata confirms 8-year gap (2018-03-14 to 2026-08-04); publisher field absent in malicious version |
| @hubsync/web-sdk-react | npm package | 6.3.7 through 6.3.33 | 27 malicious versions confirmed in Wiz IOC CSV; registry metadata confirms rapid sequential publish starting 13:24 UTC |
| verdaccio-okta-oauth | npm package | 38.1.1 through 38.1.16 | 16 versions extracted from registry metadata; publisher field absent in all 16; last clean version 38.1.0 by st-team on 2026-07-29 |
| verdaccio-tarball-local-storage | npm package | 38.1.1 through 38.1.16 | Same pattern as verdaccio-okta-oauth; registry metadata confirms identical publish cadence |
| setup.mjs | Stage 1 loader | SHA-1: 686aa40d0fc22c8d569494543a0f891f359f2f99 (in .claude); f525d52ceb966516686b482d3dc0137028cc6a63 (in .vscode) | Hashes from Wiz Research blog; file paths confirmed in OSV IOC blocks |
| Math_Symbol.js / math_init.js | Stage 2 payload | SHA-1: 35a672cf34b996b91f3e1c28cbf3a05a37e036e4 | Hash from Wiz Research blog; byte-identical across all poisoned packages |
| npm-cache[.]com | C2 exfiltration domain | 104.21.35.216 (Cloudflare) | Retrieved dynamically via Ethereum contract; confirmed in Wiz Research blog |
| 0xE1f2395ee43e45A1556EC6438a88c31B83493103 | Ethereum smart contract | StringListStore, Ethereum mainnet | Extracted from payload analysis; confirmed in Wiz Research blog; funded from address flagged for scam activity |
| tmp.dpkg_14527.lock | Lock file on disk | /tmp/tmp.dpkg_14527.lock | Extracted from payload; reported in Wiz Research blog |
| /tmp/bun-dl-*/ | Bun runtime staging directory | (glob) | Extracted from setup.mjs analysis; reported in Wiz Research blog |
| Bun/1.3.13 | User-agent for outbound calls | n/a | Identified in Wiz Research blog; extractable from malicious network traffic |
| IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients | Intimidation string | Appears in initial repository commits and outbound commit messages | Extracted from malicious commits to keyv repository; reported by Wiz Research |
| Shai-Hulud: Here We Go Again | GitHub repository description | Attacker-created exfiltration repositories | Observed across approximately 1,300 public GitHub repositories matching this string; confirmed in Aikido blog |
| chore: update config | Commit message | Used by worm when planting IDE hooks | Extracted from commits made to victim repositories; reported by Wiz Research |
Affected Versions Table
| Package | Malicious Version(s) | Published (UTC) | Current Status | OSV ID |
|---|---|---|---|---|
| keyv | 6.0.0 | 2026-08-04T09:35:00Z | Unpublished | MAL-2026-12007 area |
| @cacheable/memory | 2.2.1 | 2026-08-04T10:11:29Z | Unpublished | MAL-2026-11558 |
| @cacheable/net | 2.1.1 | 2026-08-04T11:37:27Z | Unpublished | MAL-2026-11559 |
| @cacheable/node-cache | 3.1.2 | 2026-08-04T11:34:12Z | Unpublished | MAL-2026-11560 |
| @cacheable/utils | 2.5.1 | 2026-08-04T11:36:30Z | Unpublished | MAL-2026-11561 |
| @arv-bedrock/auth | 1.1.7 | 2026-08-04T15:19:08Z | Unpublished | MAL-2026-11553 |
| @arv-bedrock/auth-admin | 1.0.2 | 2026-08-04T15:20:43Z | Unpublished | MAL-2026-11554 |
| @arv-bedrock/auth-sso | 1.6.1 | 2026-08-04T15:18:18Z | Unpublished | MAL-2026-11555 |
| @arv-bedrock/auth-sso-backend | 1.7.1 | 2026-08-04T15:18:53Z | Unpublished | MAL-2026-11556 |
| @arv-bedrock/logger | 1.7.1 | 2026-08-04T15:18:33Z | Unpublished | MAL-2026-11557 |
| @adminide-stack/clock-tik-browser | 12.0.24 | 2026-08-04T18:00:00Z | Unpublished | MAL-2026-11551 |
| @adminide-stack/yantra-mobile | 12.0.33 | 2026-08-04T16:03:52Z | Unpublished | MAL-2026-11552 |
| @deliveroo/determinator | 0.2.1 | 2026-08-04T10:33:58Z | Unpublished | MAL-2026-11562 |
| @deliveroo/reevent | 1.0.1 | 2026-08-04T18:00:00Z | Unpublished | MAL-2026-11563 |
| @hubsync/web-sdk-react | 6.3.7 through 6.3.33 | 2026-08-04T13:24:31Z | Unpublished | MAL-2026-11564 |
| verdaccio-okta-oauth | 38.1.1 through 38.1.16 | 2026-08-04T10:41:16Z | Unpublished | MAL-2026-11990 |
| verdaccio-tarball-local-storage | 38.1.1 through 38.1.16 | 2026-08-04T10:41:16Z | Unpublished | MAL-2026-11991 |
| umadev | 1.0.74 | 2026-08-04T13:18:05Z | Unpublished | MAL-2026-11989 |
| @jsimplify/errno | (see OSV) | 2026-08-04T17:39:09Z | Unpublished | MAL-2026-12006 |
| vitest-preview-pro | 10.0.3 | 2026-08-04T23:56:52Z | Unpublished | MAL-2026-12004 |
| webdev-conf | 5.0.0 | 2026-08-04T23:57:12Z | Unpublished | MAL-2026-12005 |
| workbench-browser-server | 0.0.2 | 2026-08-04T16:02:22Z | Unpublished | MAL-2026-11992 |
Remediation
This is a Class 1 campaign: install-time execution via preinstall hook, credential stealer with active C2, self-propagating worm with persistence implants. The threat is to developer machines, CI/CD runners, and any environment that ran npm install against an affected version with install scripts enabled.
Step 1: Identify exposure. Run the following in every project that depends on any of the affected namespaces, including transitively:
npm ls keyv flat-cache file-entry-cache cacheable cacheable-request cache-manager
grep -r "38\.1\.[1-9]\|38\.1\.1[0-6]" package-lock.json yarn.lock pnpm-lock.yamlCheck the Wiz IOC CSV for the complete 443-package list. Check build artifact logs from August 4, 2026.
Step 2: Search for persistence. Before rotating credentials, check whether the persistence mechanism landed on affected machines:
find . -name "setup.mjs" -newer /tmp -path "*/.claude/*" -o -path "*/.vscode/*"
find /tmp -name "tmp.dpkg_14527.lock"
find /tmp -type d -name "bun-dl-*"If setup.mjs is found in .claude/ or .vscode/ alongside a settings.json or tasks.json with a SessionStart or runOn: folderOpen hook, the persistence mechanism is live. Remove it before opening the project in Claude Code or VS Code.
Step 3: Treat affected machines as compromised. If any affected package version installed with scripts enabled, rotate in the following order: npm publish tokens (check ~/.npmrc and CI secrets), GitHub Personal Access Tokens (check ~/.gitconfig and environment variables), cloud credentials (AWS ~/.aws/credentials, GCP ~/.config/gcloud, Azure ~/.azure), HashiCorp Vault tokens, Kubernetes configs (~/.kube/config), SSH keys (~/.ssh/), and any AI agent API keys stored in .claude/, .cursor/, or .openai/ directories.
Step 4: Audit CI/CD pipelines. If an affected version was installed in any GitHub Actions workflow or other CI runner on or after August 4, 2026, rotate every secret and token accessible from that pipeline. Check for unauthorized npm package publishes from your organization’s scopes during the August 4 window.
Step 5: Pin and lock. Until the keyv and cacheable ecosystems are fully remediated, pin all dependencies in the affected namespaces to the last known clean version using exact version pinning. Do not leave caret (^) or tilde (~) ranges in place. Run npm install with --ignore-scripts as a temporary control while validating your dependency tree.
npm 12 and later does not execute preinstall hooks by default. If your toolchain is on npm 12+, the worm’s install-time trigger did not fire on bare npm install, though the persistence files in .claude/ and .vscode/ can still reach affected machines through other vectors if the repository was cloned.
Attribution
The payload is a descendant of the “Mini” Shai-Hulud malware family, as confirmed by Wiz Research through shared payload structure with the TeamPCP campaign and the antv supply chain compromise. Specific shared indicators recovered from this wave include the identical setup.mjs stage-1 loader architecture, the Bun runtime download-and-execute pattern, the Shai-Hulud: Here We Go Again GitHub repository description string used as an exfiltration drop, and the RSA key used to verify fallback C2 domains posted to GitHub (unchanged from the prior wave). The payload hash 35a672cf34b996b91f3e1c28cbf3a05a37e036e4 for Math_Symbol.js / math_init.js is consistent across all 443+ packages in this wave, confirming a single build. The Ethereum smart contract address 0xE1f2395ee43e45A1556EC6438a88c31B83493103 is new infrastructure introduced in this wave; its owner address was previously flagged for scam activity in on-chain records.
The Wiz IOC CSV, StepSecurity incident blogs, Unit 42 npm monitoring post, and Aikido Security blog all attribute this wave to the same Shai-Hulud family. No attribution to a specific nation-state actor has been published by any of these sources as of this writing.
What the Registry Timestamps Show
The following findings come from registry analysis conducted after the initial wave of public disclosures.
The Worm’s Internal Clock: Second-by-Second Publish Cadence
By pulling raw timestamps from registry.npmjs.org for every package in the affected cluster, we reconstructed the worm’s exact execution timeline. The sequence reveals its internal loop behavior in a way no advisory has documented.
Session 1: jaredwray GitHub token (09:30 to 10:28 UTC)
The worm began with the @keyv adapters, publishing them in tight bursts:
09:30:01 UTC @keyv/test-suite@6.0.0
09:30:58 UTC @keyv/memcache@6.0.0
09:31:03 UTC @keyv/mongo@6.0.0
09:31:09 UTC @keyv/mysql@6.0.0
09:31:14 UTC @keyv/postgres@6.0.0
09:31:24 UTC @keyv/sqlite@6.0.0
09:31:29 UTC @keyv/valkey@6.0.0
09:32:24 UTC @keyv/redis@6.0.0
09:35:00 UTC keyv@6.0.0
[34-minute gap]
10:09:44 UTC @cacheable/net@2.1.1
10:10:34 UTC @cacheable/node-cache@3.1.2
10:10:55 UTC flat-cache@6.1.24
10:11:24 UTC cacheable-request@13.0.20
10:11:29 UTC @cacheable/memory@2.2.1
10:13:02 UTC file-entry-cache@11.1.6
10:14:21 UTC @cacheable/utils@2.5.1
[13-minute gap]
10:28:01 UTC ecto@5.0.1The 34-minute gap between keyv@6.0.0 (09:35) and the cacheable cluster (10:09) is the worm conducting its credential sweep and exfiltration on the jaredwray machine before proceeding to the next token. The 13-minute gap before ecto is the same behavior: sweep, upload, continue.
The simultaneous publish pattern reveals worker parallelism. Four minutes after the worm finished the jaredwray account, new victims appeared at the same second:
10:32:09 UTC @arv-bedrock/auth@1.1.7 (worm)
10:32:09 UTC @arv-bedrock/auth-sso@1.6.1 (worm)
10:32:09 UTC @arv-bedrock/auth-admin@1.0.2 (worm)
10:32:09 UTC @arv-bedrock/auth-sso-backend@1.7.1 (worm)
10:32:09 UTC @arv-bedrock/logger@1.7.1 (worm)Five packages published at the exact same second. This is not a human. The worm processes all packages under a stolen token in a single parallelized batch call, not one at a time. Then, four minutes later, it loops:
10:36:22 UTC @arv-bedrock/auth@1.1.8 (worm, second cycle)
10:36:22 UTC @arv-bedrock/auth-sso@1.6.2 (worm, second cycle)
10:36:22 UTC @arv-bedrock/auth-admin@1.0.3 (worm, second cycle)
10:36:22 UTC @arv-bedrock/auth-sso-backend@1.7.2 (worm, second cycle)
10:36:22 UTC @arv-bedrock/logger@1.7.2 (worm, second cycle)The worm published every @arv-bedrock package twice, four minutes apart, and then moved on. This is the loop cadence in the worm’s republish logic: one complete cycle per token, then the token is either exhausted, revoked, or the worm moves to the next machine.
The verdaccio interval pattern tells us what happens between publishes. The ServiceTitan st-team token produced 16 versions of verdaccio-okta-oauth between 10:41 and 12:04, a 90-minute window. The intervals between successive versions are not uniform:
38.1.1 -> 38.1.2: +83 seconds
38.1.2 -> 38.1.3: +173 seconds
38.1.3 -> 38.1.4: +256 seconds
...
38.1.14 -> 38.1.15: +896 seconds (15 minutes)
38.1.15 -> 38.1.16: +814 seconds (13.5 minutes)The early intervals are short because the worm is in its publish loop. The two long gaps at the end (versions 38.1.15 and 38.1.16) coincide with the worm simultaneously processing other accounts: the adminide-stack and workbench-browser-server tokens both published at 11:00:31 UTC, exactly during that gap. The worm is not single-threaded: it manages multiple stolen tokens concurrently, and delays on one token’s publish queue reflect work being done on another.
The Seven Packages the Worm Skipped, and Why
jaredwray’s npm account controls 50+ packages. The worm poisoned many of them but left several untouched. Understanding which ones it skipped reveals its version-selection algorithm.
Skipped packages: hookified, airhorn, writr, qrbit, docula, memcache, hashery, @keyv/serialize, @keyv/sql, @jaredwray/fumanchu, @jaredwray/mockhttp, fastify-fusion.
The poisoned @keyv adapters (compress-gzip, compress-brotli, redis, etc.) all had dist-tags.latest pointing to their clean version (e.g., 2.0.3), and the worm published a new 6.0.0 over them.
@keyv/serialize has dist-tags.latest = 1.1.1 but also has a 6.0.0-alpha.2 pre-release. The worm did not touch it. @keyv/sql has dist-tags.latest = 1.1.4 from 2022. The worm did not touch it either.
The pattern: packages where dist-tags.latest is in the 1.x range and the package is on a completely different major version branch than the rest of the @keyv family were skipped. The worm’s version-increment logic appears to derive its target version from the broader package family context (the 6.0.0 wave being pushed across the entire @keyv namespace) rather than incrementing each package’s individual latest version. When a package’s current version diverges significantly from the target version, the worm skips it rather than creating an anomalous version jump.
Additionally, hookified, airhorn, writr, and similar higher-level utility packages appear in jaredwray’s portfolio but are not primary keyv/cacheable infrastructure. These packages had cacheable as a dependency, meaning the worm had already poisoned their dependency tree indirectly. Re-poisoning them directly may have created circular detection signals the worm was designed to avoid.
The Publisher Identity Signal: Absent User Fields Are the Worm’s Fingerprint
Every version published by the worm has an absent or empty _npmUser field in the registry JSON. Legitimate publishes by st-team, umayun, jaredwray, and adminide-stack-user all carry a populated _npmUser object. The worm’s publishes do not: the field is either missing or returns {}.
This is a mechanical artifact of how the worm authenticates: it uses the stolen token directly against the npm registry API without associating a user identity to the session, bypassing the user context that normal npm CLI publishes attach. A registry-level scan for all packages where the latest version’s _npmUser field is absent and the publish timestamp falls within the August 4 window would surface every worm-poisoned package, not just the ones that made it into OSV advisories.
A Healthcare Data Platform in the Victim List
The publisher email recovered from vitest-preview-pro (MAL-2026-12004) is tech.lead@claritev.health. The npm account name is vite-plugin-eslint, a name that does not match any npm package or GitHub project associated with Claritev. The worm published one version of vitest-preview-pro under this account at 17:32 UTC on August 4, 2026, with a preinstall hook present. It is the only package on the vite-plugin-eslint account.
Claritev, formerly known as MultiPlan, is a healthcare technology, data and insights company that serves more than 700 healthcare payors, over 100,000 employers, 60 million consumers, and 1.4 million contracted providers. The stolen npm token belonged to an account using a Claritev email address. Whatever npm publish access that developer had was swept up in the worm’s credential collection from an earlier victim’s environment and used to poison one package under a throwaway-named account.
The presence of a tech.lead@claritev.health email as the publisher account for a malicious npm package published during an active credential-stealing worm campaign means the Shai-Hulud worm’s stolen credential pool reached a developer working at a company that holds data on 60 million healthcare consumers. What other credentials (cloud keys, internal service tokens, CI secrets) that developer’s npm-installing machine also exposed is not recorded in the registry metadata.
npm’s Response Timeline: What the Registry Itself Shows
The registry metadata for @jsimplify/errno shows npm’s security response in real time:
16:52:05 UTC @jsimplify/errno@2.0.6 published by worm
17:00:55 UTC @jsimplify/errno@2.0.7 published by worm
17:07:36 UTC @jsimplify/errno@2.0.8 published by worm
17:37:59 UTC @jsimplify/errno@0.0.1-security published by npmThe worm published three versions in 15 minutes. npm responded 30 minutes after the last malicious version by publishing a 0.0.1-security placeholder, transferring the package to npm-support, and wiping the maintainer list. The current dist-tags for the package is empty, and the maintainers array is [{name: "npm-support", email: "support@npmjs.com"}].
webdev-conf (MAL-2026-12005) received a different treatment: npm wiped its maintainers array entirely (now []) and cleared its dist-tags. The single malicious version 5.0.0 remains in the registry history but the package has no current maintainer and no resolvable latest tag.
The two responses (0.0.1-security placeholder versus maintainer wipe with empty dist-tags) reflect different npm security playbooks for different package states. Both effectively kill the package for new consumers. Neither reverses the effect on lockfiles already containing the malicious versions.
