Revoking the token was the trigger
On October 8, tensorlake 0.5.144 reached npm with a Shai-Hulud worm inside. It steals GitHub, npm, cloud and SSH credentials, plants .claude/settings.json in the victim's repositories, and installs a monitor that checks the stolen GitHub token every 60 seconds and runs rm -rf ~/ the moment it stops working. Revoking the token, the first step of the standard incident playbook, wipes the victim's home directory. In Bromure Agentic Coding, the default age gate hides that version from npm, and the GitHub token in the machine was never real.
The worm left a string in its code for whoever found it: "IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner." The code does what the string says.
At 01:12 UTC on Wednesday, October 8, the release workflow of
tensorlakeai/tensorlake published version 0.5.144 of the tensorlake SDK to
npm. Tensorlake is a serverless sandbox platform for AI agents, and its npm
package gets about 12,000 downloads a week. The new version came from the
project's own repository, built by the project's own workflow, and npm showed a
provenance attestation for it.
Socket flagged it eleven minutes later. StepSecurity, Aikido and The Hacker News followed the same day. The version carries a new variant of the Shai-Hulud worm, and this one comes with a hostage.
How it got in
StepSecurity reconstructed the timeline. At 01:20 UTC on October 7, someone pushed a commit straight to the repository's main branch under a maintainer's name. Seven more commits followed over the next hours. Nobody caught them, and a day later the release workflow built the code on main and shipped it. The provenance attestation was accurate. It proved where the package was built, and the place it was built was already in the attacker's hands.
The package's package.json gained one line:
"preinstall": "node lib/setup.mjs"
npm runs preinstall before the package is even unpacked into place. The
script downloads the Bun runtime and uses it to run lib/Math_Symbol.js, an
obfuscated 856 KB payload. It skips itself on CI. StepSecurity's summary:
"developer machines are the target."
What it takes, and where it goes
The payload collects GitHub and npm tokens, cloud keys for AWS, Azure and GCP,
Kubernetes and HashiCorp Vault secrets, SSH keys, .env files, saved browser
logins, crypto wallets, and the configuration of AI coding tools: .claude,
.cursor, .kiro, Windsurf and Zed. It encrypts the haul and sends it to
iseekaigogo.com, or, failing that, to a public repository it creates in the
victim's own GitHub account, with the description "Shai-Hulud: Here We Go
Again." When the domain goes down, it looks up a new one in an Ethereum
contract through about 30 public RPC endpoints.
Then it spreads. With the victim's npm token, it downloads each of their
packages, adds itself, bumps the version and republishes. With the GitHub
token, it commits .claude/settings.json and .vscode/tasks.json into the
victim's repositories, as [email protected], with the message
chore: update dependencies. The next developer who opens one of those
projects in Claude Code or VS Code runs the worm again. As StepSecurity's
Ashish Kurmi put it, the malware "runs again when someone opens the project in
Claude Code or VS Code."
The hostage
The new part is a service called gh-token-monitor. On Linux it's a systemd
user unit in ~/.config/systemd/user/, on macOS a LaunchAgent at
~/Library/LaunchAgents/com.user.gh-token-monitor.plist, and on Windows a
logon task running monitor.ps1. StepSecurity describes what it does:
Every 60 seconds, for up to 24 hours, the service checks that token against the GitHub API. If GitHub rejects it, the service runs
rm -rf ~/.
Say you're the victim. You learn that a package you installed
stole your GitHub token. The standard playbook says to revoke
it at once. You revoke it, and within a minute your home directory is gone: your
checkouts, your uncommitted work, your dotfiles, and the evidence of what the
worm did. Socket found the threat spelled out in the code as a literal string:
IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner.
So the advisories now come with a sequencing rule. StepSecurity's step 3 is to find and remove the monitor, and to back up anything you can't lose first. Its warning in bold: "Don't revoke any GitHub token until step 3 is done." Socket says the same. Every hour you spend hunting for the monitor is an hour the attacker holds a working token with your push rights.
The same install in a Bromure machine
In Bromure Agentic Coding, Claude Code, Codex and the other agents run in a
Linux VM on your Mac, one VM per machine. Every connection the VM opens,
including every npm fetch, goes through a proxy on the Mac. Replay October 8
in one of those machines: the agent adds tensorlake to a project and runs
npm install.
The version didn't exist yet
Every new machine has the supply-chain age gate on, with a two-day
minimum. When npm asks the registry for tensorlake's version list, the proxy
removes every version published less than two days ago and points latest at
the newest one that's old enough. On October 8, that was 0.5.143. npm
installed it and moved on. From inside the machine, 0.5.144 never existed, and
by the time it would have aged past the cutoff, it was gone from npm.
If a lockfile or the agent asked for 0.5.144 by name, the proxy refused the download with an HTTP 451 and a reason npm prints as is:
Bromure Supply-Chain Security blocked this request: npm package
[email protected] published 3 hours ago — policy requires 2 days minimum
The agent reads that line too, and pins the previous version on its own.
Two more layers sit in the same Supply Chain pane. With socket.dev
selected and Block compromised packages on, the download is refused once
socket.dev has flagged the package, which took Socket eleven minutes on
October 8. With Strip preinstall / install / postinstall / prepare from npm
tarballs on the fly on, the proxy rewrites the tarball and the
"preinstall": "node lib/setup.mjs" line is gone before npm sees it. That
line is the worm's only way in.
Say it runs anyway
Take every layer away and let the payload run. It runs as the VM's user, in the machine's own home, and goes looking for the things on its list.
The tokens are stand-ins. No file, environment variable or process in the
VM holds a real GitHub token, cloud secret or SSH private key. GH_TOKEN and
~/.git-credentials hold a ghp_ token with the right shape, and the proxy on
the Mac swaps in the real one only when a request leaves for GitHub. ~/.ssh
has no private key: SSH_AUTH_SOCK is a bridge to an ssh-agent on the Mac,
which signs and never hands over the key. Your Mac's home, its Keychain and
its browser profiles aren't in the VM at all. The worm encrypts and ships a
bundle of stand-ins that GitHub, AWS and Kubernetes reject anywhere but
through that proxy.
The way out runs through the Mac. A machine with Unmatched traffic
set to Deny in its Guardrails pane, with rules for its model provider,
its registries and GitHub, never connects to iseekaigogo.com or to any of
the 30 Ethereum RPC endpoints. Whatever the settings, each of those
connections appears in the Security Timeline, which is always on for
every machine.
The writes wait for a click. New machines use Prompt before write for
every git forge. The worm's GitHub fallback creates a public repository in
your account: that's a POST /user/repos, a write, so macOS shows Allow
write on "github.com" from workspace "…"? with the request as it appeared on
the wire. Its spread commits .claude/settings.json into your repositories
and pushes them: the same dialog, this time with
POST /acme/app.git/git-receive-pack. You didn't ask the agent to create a
repository called "Shai-Hulud: Here We Go Again." You click Don't allow,
and the worm's propagation ends on your Mac.
The hostage holds a stand-in
The gh-token-monitor unit still installs itself into the VM's
~/.config/systemd/user/ and starts calling api.github.com/user every 60
seconds with the token it found. That token is the stand-in. The proxy swaps
in the real one for GitHub, GitHub answers 200, and the monitor waits.
That reverses the response order. On a laptop, the real token is in the
attacker's hands and the race is to revoke it before they use it, without
setting off the wipe. In Bromure, the real token never entered the VM, so the
attacker has nothing to race you with. Nothing outside your proxy accepts the
ghp_ value they took.
Shut the machine down, and the monitor stops with it. Open Rewind home…
and pick the checkpoint from the boot before the install: Bromure takes one at
every successful boot. The unit file, the script in ~/.local/bin, and the
.claude/settings.json files planted in the machine's checkouts all go back
to how they were. The home as it was a moment ago is kept as a new rollback
point first, so the rewind can itself be undone. For a clean slate, Reset
disk re-clones the system disk from the signed base image.
And if the switch fires anyway, rm -rf ~/ deletes /home/ubuntu inside one
VM. Your Mac's home directory is out of its reach, and Rewind home… brings
the machine's home back from the last checkpoint.
Who installed it?
The first question in an incident like this one is whether any machine on the team pulled the bad version. On a laptop, you search lockfiles, npm caches and shell history, and hope the worm didn't clean up after itself.
In Bromure, every package fetch is a row in the Security Timeline, whether
or not any supply-chain layer is on: npm · [email protected], with the
machine, the time and the decision. The rows are kept on the Mac for 90 days,
where nothing inside a VM can edit them. Filter on Supply chain, search for
the version, and you have the answer. Export… saves it as a CSV for the
incident ticket. On Macs enrolled with bromure.io, the same fetch goes to the
organization's event stream as a supply_chain.fetch event, so a security team
can ask the question once for every developer.
The bad version never showed up
The age gate, on by default with a two-day minimum, removes fresh versions
from npm's version list. On October 8, npm install tensorlake resolved to
0.5.143.
The stolen token was a stand-in
GitHub, npm, cloud and SSH credentials stay on the Mac. The worm's
hostage was a ghp_ value that works only through Bromure's proxy.
Spreading needs your click
Creating a repository and pushing a commit are writes. With Prompt before write, the default on new machines, both wait for a macOS dialog the VM can't answer.
The wipe hits one VM
rm -rf ~/ reaches the machine's home, not yours. Rewind home restores it
from the checkpoint taken at the last boot.
The next playbook
Shai-Hulud has come back again and again this year, and its authors shape each version around how defenders handled the last one. Defenders revoked tokens within hours, so this one punishes revocation. The next one will punish something else: running a cleanup script, deleting a LaunchAgent, opening the project again.
On a laptop, each of those twists works because the attacker holds something real on the same machine you're trying to clean. In a Bromure machine, the real credentials, your home directory and the record of what happened all sit on the Mac, behind the proxy. Cleanup is a rewind, and you can do it before you rotate anything. For more on the wire boundary behind it, read The install was clean and Forty thousand dollars per escape.