Incident · self-spreading npm malware
The Shai-Hulud npm worm: what it did on a developer’s Mac, and what Kilobat would have shown
In September 2025 a worm spread through npm: each infected package stole the developer’s or CI system’s tokens, used any npm token it found to publish infected versions of that maintainer’s other packages, and leaked secrets through GitHub. A second, larger wave in November ran before installation even finished.
In short
- Protection: Wouldn’t help
Nothing to block on the Mac. Its traffic went to npm, GitHub and cloud APIs; webhook.site was called from GitHub’s runners, not from developers’ machines.
- Agent guard: Wouldn’t help
The webhook.site rule never came into play, because the request came from a GitHub Actions runner. The other destinations were well-known hosts or local addresses.
- Agent network log: Limited
For an install an agent ran, it would have shown TruffleHog, curl calling the GitHub API, an
npm publishand first-time cloud hosts, all under one install.
What happened
GitHub says it was told of the attack on 14 September 2025. Socket and StepSecurity first reported @ctrl/tinycolor 4.1.1 and 4.1.2 and some 40 other packages; within a week the count passed 500, including packages published by CrowdStrike. Each infected version ran a 3.6 MB bundle.js from a postinstall hook, on macOS and Linux only.
The script dumped the environment, downloaded the open-source secret scanner TruffleHog from its GitHub releases and ran it over the whole file system, and tried the cloud metadata endpoints that hand out credentials on cloud machines. With AWS or Google Cloud credentials it listed and read the secrets in those accounts’ secret managers.
With an npm token, from ~/.npmrc or the environment, it checked the token with npm’s whoami endpoint, searched for the maintainer’s other packages, added itself to each with a patch version bump and published them. That is what made it a worm: one developer’s token infected every package they could publish.
With a GitHub token, it created a public repository named Shai-Hulud holding the collected data as double-base64 data.json, pushed a shai-hulud branch with a workflow, .github/workflows/shai-hulud-workflow.yml, to the repositories the token could reach, and in some cases copied private organisation repositories into public ones marked “Shai-Hulud Migration”. That workflow ran on GitHub and posted the repository’s secrets to a webhook.site address. Wiz found the webhook had been deactivated for excessive use, but the workflow also printed the secrets into its logs.
The second wave, uploaded between 21 and 23 November, used new files, setup_bun.js and bun_environment.js, ran in the preinstall phase, and kept running in the background on developer machines so that installs didn’t look slow. It published secrets to repositories with Shai-Hulud in their description, more than 25,000 of them by Wiz’s count, and registered infected machines as GitHub Actions self-hosted runners named SHA1HULUD, a way back in.
Timeline
- 14 Sep 2025
GitHub is notified of a self-replicating worm in npm packages (GitHub).
- 15 Sep 2025
Socket and StepSecurity report
@ctrl/tinycolorand over 40 more packages. - 16 Sep 2025
Socket counts nearly 500 affected packages, including CrowdStrike’s; Aikido and others track it spreading.
- 22 Sep 2025
GitHub has removed over 500 packages, makes npm block uploads carrying the worm’s indicators, and announces 2FA-only local publishing, 7-day granular tokens and trusted publishing.
- 23 Sep 2025
CISA’s alert advises rotating developer credentials and blocking outbound connections to webhook.site.
- 21–23 Nov 2025
The second wave’s packages are uploaded to npm (Wiz, Aikido).
- 24–26 Nov 2025
Repositories with leaked secrets appear by the thousand; GitHub starts removing them and revoking leaked tokens on 26 November.
What it looked like on the network
On a developer’s Mac, almost everything went to hosts a developer uses all day. From registry.npmjs.org it fetched the infected package, then called /-/whoami and /v1/search, downloaded the maintainer’s other packages and published new versions: an upload from npm in the middle of an install. On api.github.com it read the account, created the Shai-Hulud repository and pushed branches and workflow files; StepSecurity’s deobfuscated code shows the backdoor module doing this with curl. TruffleHog came from GitHub’s release downloads.
With cloud credentials, it also reached the AWS Secrets Manager and Google Secret Manager APIs. It tried the metadata address 169[.]254[.]169[.]254 and metadata[.]google[.]internal, which only answer on cloud machines. The process tree ran from npm through a shell to node bundle.js, which started trufflehog, a bash script running curl, and npm publish.
The one unusual host, webhook[.]site, wasn’t contacted from the Mac at all. It sat in the workflow the worm pushed to GitHub, so the request came from a GitHub-hosted runner when the workflow ran.
Indicators
As the sources document them. Domains are text, never links, and are defanged ([.]) where the source defangs them. Most of this infrastructure is long gone: indicators help you check old logs, files and accounts, not block today’s campaigns.
| Indicator | Type | Where it shows | Note |
|---|---|---|---|
webhook[.]site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7 | URL | GitHub Actions runner | Where the injected workflow posted each repository’s secrets. Deactivated by the platform. |
api.github.com | Host (legitimate) | Your Mac | Calls to /user and /user/repos, and pushes of branches and workflow files, made during an npm install. |
registry.npmjs.org/-/whoami, /v1/search?text=maintainer:… | npm API calls | Your Mac | Token check and the search for the maintainer’s packages, followed by npm publish. |
secretsmanager.*.amazonaws.com, secretmanager.googleapis.com | Cloud APIs | Your Mac, CI | Only where cloud credentials were present. |
46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09 | SHA-256 of bundle.js | node_modules | The September payload. |
shai-hulud branch, .github/workflows/shai-hulud-workflow.yml | Branch, file | GitHub repositories | The workflow that sent secrets to the webhook and into the logs. |
Shai-Hulud repository with data.json; “Shai-Hulud Migration” | GitHub repositories | GitHub account | Collected data, and private repositories copied public with a -migration suffix. |
setup_bun.js, bun_environment.js, SHA1HULUD | Files, runner name | node_modules, GitHub | The November wave’s payload, and the self-hosted runner it registered. |
What Kilobat would have shown or blocked
- Agent network log. For an install an agent ran, the session would group the hosts by process:
trufflehogstarting under the install,curlcalling api.github.com,npmsending to registry.npmjs.org, and, with cloud credentials on the Mac, AWS or Google API hosts flagged first time. Processes that carried on in the background after the install are followed, since processes handed over to launchd stay the agent’s. - App traffic. Even without an agent, Kilobat’s app list shows command-line tools that aren’t part of macOS with their executable path, so a
trufflehogyou never installed, or a node process uploading to GitHub, is visible while it runs. - Protection. Nothing on the Mac. npm, GitHub and the cloud providers aren’t threats, and the one host defenders were told to block was contacted from GitHub’s runners.
- Agent guard. Its request-catcher rule names webhook.site and would refuse it for an agent and anything it starts, but here the request came from GitHub’s runners, never from a Mac. The metadata address is link-local, which Kilobat treats as the local network.
Where Kilobat wouldn’t have helped
- The traffic that did the damage, on npm and GitHub, used the developer’s own valid tokens and looks like ordinary publishing and pushing.
- The webhook exfiltration and the secrets printed into workflow logs happened on GitHub’s runners. Blocking webhook.site, as CISA advised, matters most where CI runs.
- Kilobat records hosts and byte counts, never contents, so it can’t say what TruffleHog found or what a repository received.
- Installs run in Docker containers, or by an editor’s extension host in VS Code, aren’t attributed to an agent.
- Infected packages have since been removed and the 30-day agent log can’t reach back to 2025; what’s left to check is your lockfiles, your caches and your GitHub account.
Check your Mac now
Check your lockfiles against the published package lists from Socket, StepSecurity and Wiz. For one package:
npm ls @ctrl/tinycolorIn a project folder, look for the payload files of either wave.
find . -path "*/node_modules/*" \( -name bundle.js -o -name setup_bun.js -o -name bun_environment.js \) -printCompare any
bundle.jswith the September hash. A match is the worm; other bundle.js files are common and usually harmless.find . -path "*/node_modules/*" -name bundle.js -exec shasum -a 256 {} + | grep 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09List your GitHub repositories whose name or description mentions Hulud.
gh repo list --limit 1000 --json name,description --jq '.[] | select((.name + " " + (.description // "")) | test("hulud"; "i")) | .name'In each organisation’s and repository’s Settings › Actions › Runners, remove any self-hosted runner you didn’t set up, especially one named
SHA1HULUD. On the Mac, if you don’t run a runner yourself, this should print nothing.pgrep -fl "Runner.Listener|Runner.Worker"If anything matched, rotate npm tokens, GitHub tokens and cloud keys, then list your npm tokens to delete old ones.
npm token listTo keep install scripts from running unless you ask, turn them off for npm and run them per project when needed.
npm config set ignore-scripts trueFor the next one, read what each install an agent ran reached. Watch for uploads to the npm registry and GitHub API calls during an install.
kilobat agent-log --all
Sources
- GitHub blog: Our plan for a more secure npm supply chain
- CISA: Widespread supply chain compromise impacting npm ecosystem
- StepSecurity: Shai-Hulud, self-replicating worm compromises 500+ npm packages
- Socket: Popular tinycolor npm package compromised in supply chain attack affecting 40+ packages
- Wiz: Shai-Hulud npm supply chain attack
- Aikido: S1ngularity/nx attackers strike again
- Wiz: Sha1-Hulud 2.0 supply chain attack
- Unit 42: “Shai-Hulud” worm compromises npm ecosystem in supply chain attack
Checked on 1 November 2026.
See what your agents and apps connect to
Kilobat is a Mac menu-bar app that names the app, process and agent behind every connection, keeps them away from known-bad hosts, and logs what each agent session reached.
brew install seifip/kilobat/kilobat