← Back to all posts
Published on · by Renaud Deraison

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.

On a laptop: the playbook's first step sets off the wipe1. npm install[email protected]preinstall: node lib/setup.mjsSteals the real onesGitHub token (push rights)npm token (publish rights)~/.aws ~/.kube ~/.sshInstallsgh-token-monitor (LaunchAgent).claude/settings.json in repos2. The monitor, every 60 sGET api.github.com/userAuthorization: real token200 OK → keep waitingattacker still holds yourpush rights401 → run the handler3. You revoke the tokenas every playbook saysrm -rf ~/checkouts, uncommitted work,dotfiles, and the evidence
On a laptop, the stolen token is real, so revoking it is urgent. The monitor turns that revocation into a wipe of the developer's home directory.

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.

In Bromure: the version is hidden, and the hostage is a stand-inMachine VM (Ubuntu)$ npm install tensorlake+ [email protected]If the payload runs anywayGH_TOKEN=ghp_… (stand-in)~/.ssh: no private keysthis machine's home onlygh-token-monitor polls GitHubwith the stand-ina wipe reaches /home/ubuntu in this VM,and Rewind home brings it backProxy, on the MacAge gate: latest → 0.5.1430.5.144 by name → 451, reason printedFirewall: iseekaigogo.com → denyEthereum RPC endpoints → denyevery verdict in the Security TimelinemacOS dialog: Allow write on "github.com"?POST /user/reposPOST /acme/app.git/git-receive-packReal token swapped in only for GitHubInternetregistry.npmjs.orggithub.commodel providerattacker's C2:never contactedKept on the Mac, outside the VMreal GitHub token · cloud keys · SSH private keys · your home folder · home checkpoints · Security Timeline
The same install in a Bromure machine. The age gate hides 0.5.144 from npm. If the payload runs anyway, it steals stand-ins, its pushes and repository creation wait for a dialog on the Mac, and the hostage monitor holds a token GitHub only accepts through Bromure's proxy.

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.