Back to all posts
Published on · by Renaud Deraison

The worm went open source

Sometime in the week of May 11, 2026, the people behind Shai-Hulud — the self-replicating npm supply-chain worm that has been eating maintainer accounts since September 2025 — leaked their own source code. By the weekend, OX Security had found four typosquatted npm packages under one account, one of which is a near-verbatim copy of the leaked worm, another of which is a Golang DDoS bot, and the other two are plain infostealers shipping SSH keys and crypto wallets to bargain-bin C2s. The fork floor of supply-chain attacks just got a lot lower, and the people most likely to install one of these packages are no longer human.

Sometime last week, the people behind Shai-Hulud — the npm worm that has been chewing through maintainer accounts since September 2025 — leaked their own source code. By the weekend, an npm account called deadcode09284814 had published four typosquats reusing that code. One was the worm, almost verbatim. One was a Golang DDoS bot. Two were plain infostealers that POST your SSH keys, your ~/.aws/credentials, and your MetaMask vault to a rented IP. Two thousand six hundred and seventy-eight installs later, the question is no longer "is somebody going to weaponize the leak." The question is which of the agents typing npm install in your terminal this afternoon picks one of them up.

There is a thing that happens, periodically, in offensive software, where a closed tool gets dumped on a forum and the population of people who can run it goes from "the one crew that wrote it" to "any teenager with a VPS." Mimikatz. EternalBlue. Conti's source. Each time, the capability did not change — it just stopped being scarce. The headline last week, reported by BleepingComputer on the strength of research from OX Security, is that Shai-Hulud — the worm we wrote about when it ate forty-two @tanstack packages in six minutes — joined that list.

The fork is not theoretical. By Sunday, OX had documented four malicious packages published from a single npm account named deadcode09284814:

  • chalk-tempalte — a typosquat of chalk-template, carrying a near-verbatim copy of the leaked Shai-Hulud source, including Shai-Hulud's signature behavior of uploading stolen credentials as public auto-generated GitHub repositories. OX's read: "the Shai-Hulud malware code is an almost exact copy of the leaked source code, with no obfuscation techniques," suggesting a new threat actor who did not bother to even sand the serial numbers off.
  • axois-utils — a typosquat of axios-utils, shipping a Golang payload OX calls Phantom Bot: HTTP, TCP, UDP, and reset floods, persistence via the Windows Startup folder and scheduled tasks, and a C2 at b94b6bcfa27554.lhr.life. Your dev box, drafted.
  • @deadcode09284814/axios-util — a different typosquat, a different payload: SSH keys, environment variables, AWS/GCP/Azure credentials, shipped to 80.200.28.28:2222. "Pretty straightforward," in OX's words.
  • color-style-utils — a plain infostealer that grabs your IP, geolocation, and crypto wallet data and POSTs to edcf8b03c84634.lhr.life.

Combined weekly downloads at the time of OX's writeup: 2,678.

There are two stories tangled together here that deserve to be pulled apart. The boring one is that npm has typosquats. It does; it always has; it always will, for the same reason there are dogs named Bench at the dog park — names are cheap and the namespace is flat. The interesting one is that the barrier to running a Shai-Hulud-class worm just dropped. Until last week, you needed the original crew's tooling, the original crew's infrastructure, the original crew's discipline about not getting caught. Today, you need a GitHub clone of a public repo, a lhr.life tunnel, and the patience to type npm publish. The four packages OX found are, collectively, the proof.

Why the actor count matters more than the package count.

A single sophisticated worm is, in some sense, a tractable adversary. It has tells. It has infrastructure. It has habits. Detection signatures can be written against it. Aikido, Socket, Snyk, Wiz — the npm-supply-chain monitoring shops who jumped on the May 11 @tanstack incident — caught it inside hours, specifically because they had been watching the same family for eight months.

A family of derivative worms written by people who downloaded the source from a paste site is a different shape. Each one will exfil to a different C2, embed a different RSA key, choose a different combination of files to read, and pick a different typosquat space to live in. Some will be careful; most will be sloppy in a way that gets them caught quickly; one of them, the next time we write a post like this one, will be sophisticated enough that we won't catch it in a week. The defenders' detection problem widens from "spot Shai-Hulud" to "spot anything that wants to read ~/.ssh/id_ed25519 from inside a prepare script." That is a much, much bigger surface.

The shape of the install path has also changed, and this is the part that should worry anyone running a coding agent. A human developer who wanted chalk-template would, in 2024, have read the package name out of a tutorial, typed it, and noticed the typo when chalk-tempalte came back with two hundred downloads and a stranger as the publisher. A coding agent asked to "add some color to my CLI output" in 2026 will install whatever the package manager hands back. The agent does not see the publisher field. The agent does not notice that the README is three lines long. The agent is doing thirty npm installs this hour because the user is doing the work of a small team and the agent is paid by the task, not by the install.

What an unobfuscated Shai-Hulud clone actually does.

The chalk-tempalte package is, OX writes, "almost without any change at all" from the leaked source. That means the same mechanics we walked through in The worm that writes itself into .claude apply, with one significant new wrinkle: the exfiltration channel is GitHub itself.

Shai-Hulud's signature trick — preserved by the copycat because copying is easier than rewriting — is that the stolen credentials do not go to a hidden drop server. They go to a freshly created public GitHub repository, published using a GitHub token the malware just stole from the victim. The victim's secrets sit in a public repo, owned by the victim's own GitHub account, for anyone in the world to scrape, until somebody notices and the repo gets nuked. The defender's standard playbook — block the exfil domain, look for unusual outbound DNS — does not catch this, because the outbound DNS is api.github.com, which your developer machine talks to two hundred times an hour anyway. The credential bundle leaves your laptop disguised as a git push.

Once the secrets are public, anyone who watches the GitHub firehose — and several people watch the GitHub firehose for exactly this reason — can scoop them up. The worm does not need to keep its C2 alive. GitHub is the C2.

ONE npm ACCOUNT — deadcode09284814 — FOUR PAYLOADSnpm USERdeadcode09284814 signed up: 2026-05 packages: 4 weekly DLs: 2,678PUBLISHEDchalk-tempalte ↳ typo: chalk-templateaxois-utils ↳ typo: axios-utils@…/axios-util ↳ scoped lookalikecolor-style-utils ↳ generic-soundingall four ship a postinstallor prepare scriptchalk-tempalte → SHAI-HULUD CLONEreads: ~/.npmrc, ~/.config/gh, $GH_TOKEN, ~/.aws, ~/.sshcreates: public GitHub repo under victim's accountcommits: secrets, encrypted with embedded RSA pubkeyaxois-utils → PHANTOM BOT (Golang DDoS)persistence: Windows Startup folder + scheduled taskcapability: HTTP/TCP/UDP flood, TCP reset attacks@…/axios-util → STRAIGHT INFOSTEALERreads: SSH keys, env vars, AWS/GCP/Azure credentialsPOSTs: raw bundle, no encryption layercolor-style-utils → WALLET STEALERreads: IP, geolocation, browser-extension walletstargets: MetaMask, Phantom, Keplr on-disk vaultsWHERE THE LOOT GOESchalk-tempalte: api.github.com (victim's token) new PUBLIC repo on victim acct + 87e0bbc636999b.lhr.lifeaxois-utils: b94b6bcfa27554.lhr.life Golang bot, takes orders@…/axios-util: 80.200.28.28:2222 raw TCP, no TLScolor-style-utils: edcf8b03c84634.lhr.life HTTPS POSTthree different .lhr.life tunnels,one bare IP, one api.github.com:block-the-domain doesn't fit.
Where the four `deadcode09284814` packages take the secrets they read. chalk-tempalte uses the victim's own stolen GitHub token to publish a fresh public repository containing the loot; the C2 at 87e0bbc636999b.lhr.life is incidental. axois-utils enrolls the host into a Golang DDoS botnet (Phantom Bot) with persistence via Windows Startup and scheduled tasks, taking orders from b94b6bcfa27554.lhr.life. The other two POST a credential bundle to a rented IP and a tunneled host respectively. All four packages run their payload from inside `prepare` or post-install, which means they execute the moment the agent types `npm install`, not the moment the user reads the diff.

There is a tiny detail in that picture that deserves to be lingered on. The Shai-Hulud clone, chalk-tempalte, does not even rely on its own infrastructure to receive the loot. It uses the victim's own GitHub identity to publish the victim's own stolen secrets into a public repository on GitHub. The C2 at lhr.life is backup. The primary channel is git push. To a defender watching egress, this is indistinguishable from the developer's normal CI. To GitHub, it is — until somebody flags the repo — a legitimate public project owned by a real user. The exfil is laundered through the victim's identity.

What the agent is doing while you scroll past.

If you have a coding agent in your terminal — Claude Code, Cursor's CLI, Codex CLI, Aider, take your pick — there is a non-zero chance that, in the time it took you to read the last paragraph, the agent ran an npm install on your behalf. Maybe two. Coding agents do not pause to admire dependency trees. The whole reason you bought one is that it does not pause.

The packages OX caught are typosquats, which is a category of mistake very specifically suited to machine speed. A human who has to type chalk-template will get the letters in the right order because they have done it a hundred times. A model that has ingested every Stack Overflow post on earth has seen chalk-template and chalk-tempalte in the same training corpus — the latter typically inside a screenshot of somebody else's mistake — and given a prompt like "add colored output to my CLI," will sometimes emit the typo verbatim. The agent does not flinch. The package manager does not flinch. The prepare script runs.

This is not a hypothetical failure mode. It is the failure mode the Shai-Hulud family was designed for. The original worm spread by stealing maintainer tokens and using them to republish more compromised versions of legitimately popular packages. The copycats do not have the maintainer tokens yet; what they have is the typosquat namespace, which agents are uniquely good at falling into.

Where Bromure sits in this story.

Bromure Agentic Coding is the configuration in which the coding agent runs inside a per-task disposable Linux VM, with the project folder mounted in, the egress brokered, and the credentials held on the macOS host side of the hypervisor. We walked through the architecture in detail in the Bitwarden CLI writeup and the @tanstack writeup. What follows is what specifically happens to each of these four packages inside that boundary.

BROMURE PER-TASK VM — what the four payloads seeCODING AGENT$ claude> add color to my clitool: bashnpm i chalk-tempalte↳ postinstall runs↳ inside the guest↳ trace records itGUEST FILESYSTEM — stubs and absences~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc$GH_TOKENghp_stub_…~/.ssh/id_ed25519No such file or directoryMetaMask / Phantom / Keplr vaultsnot installedbrowser profileno host browser in guest/etc/cron.d, Startup folderon disposable diskPAYLOAD OUTCOMESchalk-tempalte: reads stubs, tries api.github.com → 401axois-utils: bot writes to guest Startup → wiped at resetcolor-style-utils: no wallets presentHYPERVISOR — credential broker + egress proxymacOS HOST — real secrets and the real browser, never crossed the boundaryREAL CREDENTIAL VAULTmacOS Keychainid_ed25519 (private)~/.aws/credentialsAKIA… real~/.config/gh/hosts.ymlghp_real (scoped to this task)~/.npmrcnpm_… real publish token~/Library/.../MetaMask/vault.json (real wallet)host Chromium profileyour real browsernone of the above is reachable from the guestEGRESS PROXY — whitelisted onlygit push → api.github.com/your/repo stub ghp_… ⇒ real ghp_… (whitelisted)POST api.github.com/user/repos (CREATE) not whitelisted ⇒ 401 to the guestPOST 87e0bbc636999b.lhr.life not whitelisted ⇒ blocked, trace logs itPOST 80.200.28.28:2222 not whitelisted ⇒ blocked, trace logs itBlast radius = one ephemeral VM + whatever was already in the mounted project folder. Reset, and the next install starts from zero.
The same npm install of one of the deadcode09284814 typosquats, but the agent is running inside a per-task Bromure VM. chalk-tempalte reads ~/.aws, ~/.npmrc, ~/.ssh inside the guest and finds stubs and missing files. It then tries to use $GH_TOKEN to create a public repo — the stub token returns 401 at the egress proxy, because the proxy only swaps in the real token for whitelisted endpoints the task asked for, and 'create-a-fresh-public-repo' is not on that list. axois-utils enrolls itself in Phantom Bot's C2; the Golang persistence binary writes itself into the guest's Startup directory, which lives on the disposable disk. color-style-utils looks for a MetaMask vault that is not installed. The blast radius is one ephemeral VM, plus whatever was already in the project folder, period.

Walk this against the four packages, one at a time, because the specifics are where the architecture earns its keep.

chalk-tempalte reaches for $GH_TOKEN.

The Shai-Hulud clone's signature move is to take the developer's GitHub token and use it to create a public repository on the developer's own account. Inside a Bromure VM, the $GH_TOKEN it reads is a stub — a syntactically valid string that starts with ghp_ and exists for exactly this reason. The runner's first action is POST /user/repos against api.github.com. The egress proxy on the host side recognizes api.github.com as a whitelisted endpoint, but only for the operations the current task actually asked for — git push to the repo the task is working on, gh pr create against that same repo, gh api repos/that/repo/issues. "Create a fresh public repository on the user's account" is not on that list, because the user did not ask for it. The proxy refuses to substitute the real token, and the stub goes out as a stub. GitHub returns 401. The worm's exfil channel — the clever channel, the one designed to bypass DNS egress filtering — never opens.

The backup channel, the lhr.life tunnel at 87e0bbc636999b.lhr.life, is also not whitelisted. The trace records the attempt. The bytes do not leave.

axois-utils installs Phantom Bot for persistence.

The Golang bot tries to write itself into the Windows Startup folder and create a scheduled task. The Bromure VM is a Linux guest, so the Windows-specific persistence is, for free, a no-op. On a Linux variant of the same payload — which somebody will ship, shortly — the bot would write itself into /etc/cron.d/ or ~/.config/systemd/user/. Both of those paths are inside the guest's disposable copy-on-write disk. The next bromure reset, or the natural end of the current task, drops the disk. The persistence is gone without any hunting.

Meanwhile, the bot's outbound connection to b94b6bcfa27554.lhr.life is not on the task's egress whitelist, because no legitimate coding task talks to a freshly registered lhr.life tunnel. The bot phones home into a closed socket. The session trace logs the attempt — useful tomorrow morning when an IOC list is published.

@deadcode09284814/axios-util POSTs raw credentials.

The simplest of the four payloads is also the one with the least to grab. The runner reads the guest's ~/.ssh, ~/.aws, env vars, and POSTs them to 80.200.28.28:2222. The SSH directory is empty. The AWS file is a stub. The environment variables are either stubs or unset. The destination IP is not whitelisted. Either the connection is blocked at the proxy, or it leaves the host carrying a payload of placeholders. Either result is fine.

color-style-utils looks for wallets that aren't there.

The crypto stealer is the package whose threat model most clearly assumes the developer's own browser is on the same machine. It reads paths like ~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/<MetaMask-id>/ and the equivalents for Phantom and Keplr. None of those paths exist on the Bromure VM. The VM does not have your Chrome profile in it. The VM does not have a wallet extension installed. The VM is, by design, nobody's main browser. The runner finds an empty directory and moves on.

This is the part that is not a story about credentials living on the host. The wallets live on the host because, on a normal laptop, the developer's browser and the developer's coding agent share a filesystem. Bromure does not make the wallet stronger; it makes it unreachable from the place the worm is running. The worm cannot read what is not on its disk.

What still hurts.

There are corners of this story where Bromure's per-task VM is not a fix, and they deserve to be named out loud.

The project folder is mounted.

Files the worm writes into the project folder — including the .claude/router_runtime.js-style persistence we covered in the @tanstack post — are durable across task resets, because that is the whole point of mounting the project folder. The defense there is not the VM. It is git status and a five-second look at the diff before you push. The trace makes it easier to spot which sessions added unexpected files.

The egress whitelist is narrow on purpose.

Bromure's credential broker works because the whitelist is narrow. If you whitelist npm publish to your own scope because you are publishing a release today, and you happen to install one of these four packages today, the worm will publish under your scope. Whitelist what the task needs. Not a byte more.

A token good for this task is still a real token.

The stub GitHub token gets swapped for a real one at the wire for operations the task whitelisted. If chalk-tempalte could talk the agent into doing a git push to the project's own repo, that push would go through with a real token. The boundary protects the credentials. It does not review the diff. Read the diff.

Detection is downstream of the trace.

The session trace records every shell command, file write, and outbound request. It does not, on its own, classify 87e0bbc636999b.lhr.life as bad. It records that the request was made. When OX publishes a fresh IOC list tomorrow morning, your search is two seconds. That is the value the trace adds — not magic, just receipts.

One last thing.

The leak is not the news. The news is what the leak makes likely over the next year, which is a lot of small, half-competent forks of a worm that, even in its competent form, the npm ecosystem barely caught in time. Some of the forks will be loud enough to get a writeup. Most will sit in the registry for a week, collect a couple of thousand npm installs, and disappear when somebody finally files an abuse report. The two thousand six hundred and seventy-eight installs OX clocked on the deadcode09284814 packages are not an outlier. They are the average.

The honest question is not "will my team avoid every poisoned typosquat in npm." The agents type fast. The names are cheap. The honest question is: when the agent installs one — and over the next year, on a team that uses agents, it will — does the post-install script find the developer's hands, or does it find a disposable Linux box with stubs in the credential files and a proxy that does not know who it is.

Bromure Agentic Coding is the second one. It is free, open-source, and shipped today. The fork tree is going to get worse before it gets better.