LiteLLM wurde gehijackt. Ich ließ meinen KI-Agenten 8 Monate lang installieren, was er wollte. Das habe ich gefunden.

8 min read

Letzten Donnerstag bat ich Claude Code, mir ein Transkriptions-Tool zu bauen. In 47 Minuten installierte es 30 Python-Pakete auf meinem Rechner. torch, scipy, huggingface_hub, numba, tiktoken. Und dutzende transitive Abhängigkeiten darunter, deren Namen ich nicht mal kenne. Ich genehmigte jede einzelne Installation, ohne hinzuschauen. Wie immer.

Dann sehe ich, dass litellm gehijackt wurde. 97 Millionen Downloads pro Monat, zwei kompromittierte Versionen auf PyPI, eine .pth-Datei, die bei jedem Python-Start ohne Import ausgeführt wird. SSH-Schlüssel gestohlen, AWS-Token, GCP-Credentials, Umgebungsvariablen. Entdeckt nur, weil die Malware einen Bug hatte, der die Maschine zum Absturz brachte.

Ich nutzte litellm nicht. Aber 59 Pakete in globalem pip ohne venv, ohne Lockfile, ohne Audit, nach 8 Monaten täglicher Entwicklung mit einem KI-Agenten? Das nächste litellm war bereits auf dem Weg zu mir. Ich öffnete mein Terminal und schaute, was tatsächlich auf meinem Rechner war. Hier ist, was ich fand und was ich in 45 Minuten reparierte.

TLDR: KI-Coding-Agenten haben die letzte menschliche Reibung zwischen dir und Supply-Chain-Attacken beseitigt. Sie installieren, du genehmigst, niemand prüft. Ich scannte meinen Rechner, fand 59 ungeprüfte globale Pakete, migrierte zu uv mit hash-gesperrten Abhängigkeiten und fügte vier Zeilen zu meiner CLAUDE.md hinzu, die die Reibung wiederherstellen. Das Ganze dauerte 45 Minuten. Öffne dein Terminal und führe pip freeze aus, bevor deine nächste Coding-Session beginnt.

Zwei Entwickler erkunden Software-Abhängigkeitsrisiken mit Comic-Illustration
Wenn dein KI-Agent zum ultimativen Paketmanager-Rebellen wird 🤖🏴‍☠️

Was mit LiteLLM passierte (Und warum es dir Angst machen sollte)

Am 24. März veröffentlichte eine Bedrohungsgruppe namens TeamPCP die litellm-Versionen 1.82.7 und 1.82.8 auf PyPI über einen gestohlenen Maintainer-Account. Dieselbe Gruppe hatte zuvor Trivy kompromittiert, einen Sicherheitsscanner (ja, die Leute, die Sicherheitstools hacken, um deine Tools zu hacken). Die Payload war eine .pth-Datei. Falls du nicht weißt, was das ist: Python lädt .pth-Dateien automatisch beim Start. Kein Import nötig. Keine Spur in deinem Code. Allein die Installation des Pakets reichte aus.

Die Malware sammelte SSH-Schlüssel, AWS-Credentials, GCP-Token, Kubernetes-Configs, Umgebungsvariablen und Krypto-Wallets. Sie funkte bei jedem Python-Start nach Hause zu einem externen Server. 97 Millionen Downloads pro Monat für dieses Paket.

Es wurde entdeckt, weil der Angreifer einen Fehler machte. Die Malware hatte einen Bug, der eine Fork-Bombe verursachte, RAM-Verbrauch schoss hoch, Maschinen stürzten ab. Ein Entwickler, der Cursor nutzte, bemerkte, dass sein Rechner starb, verfolgte es zurück zu einem MCP-Plugin, das litellm als transitive Abhängigkeit zog. Transitiv. Er installierte litellm nie selbst. Sein KI-Tool tat es, über ein Plugin, über eine Abhängigkeitskette, die er nie überprüfte.

Ein wichtiger Vorbehalt: Das offizielle litellm-Docker-Image war nicht betroffen (gepinnte Abhängigkeiten). Das GitHub-Repo blieb sauber. Nur die PyPI-Distribution war kompromittiert. Aber dort holen sich die meisten Leute ihre Pakete.

Hätte der Angreifer die Maschine nicht zum Absturz gebracht, hätte es niemand bemerkt. Wochenlang. Vielleicht monatelang.

Ich bat meinen KI-Agenten, meinen eigenen Rechner zu scannen

Als ich über litellm las, war meine erste Reaktion zu prüfen, ob ich getroffen wurde. Meine zweite Reaktion war die Erkenntnis, dass ich null Tooling dafür hatte.

Also tat ich, was ich immer tue. Ich fragte Claude Code. "Scanne meinen Rechner nach kompromittierten Python-Paketen." Was folgte, war etwas peinlich anzusehen. Der Agent startete eine rekursive Suche, lief sofort in Timeouts, weil iCloud die Hälfte meines Dateisystems synchronisierte. Symlinks verlangsamten ripgrep überall. Er musste um macOS-Sandboxing navigieren, nur um meine eigenen site-packages anzuschauen. Wie jemandem zuzusehen, der versucht, sein eigenes Haus zu inspizieren, aber die Türen verschließen sich ständig selbst.

Der Scan fand 59 Pakete in globalem pip installiert. Neunundfünfzig. Ich erkannte vielleicht die Hälfte davon. Der Rest waren transitive Abhängigkeiten aus diesen 47 Minuten Transkriptions-Tool-Bau, plus Monate anderer "klar, installier das"-Momente, die sich darüber angesammelt hatten.

Dann bat ich Claude Code, pip-audit auszuführen. Stellte sich heraus, pip-audit war nicht installiert. Brauchte es vorher nie (dachte vorher nie daran, genauer gesagt). Und hier wird es richtig ironisch: Um pip-audit zu installieren, musste Claude Code eine temporäre venv erstellen, weil PEP 668 globale Installationen auf modernem Python blockiert. Dasselbe PEP 668, das die 30 whisper-Pakete drei Wochen früher mit --break-system-packages umgangen hatten.

Das Audit-Tool wurde vom System blockiert. Die 30 ungeprüften Pakete waren glatt durchgesegelt.

pip-audit fand eine CVE. Pygments 2.19.2, niedrige Schwere, eine Rendering-Bibliothek, die nicht dem Netzwerk ausgesetzt ist. Kein litellm auf meinem Rechner. Sauber.

Aber sauber durch Glück, nicht durch Design. Und da beginnt das eigentliche Problem.

Dein KI-Agent entfernte die letzte Reibung, die dich schützte

Vor KI-Coding-Agenten tipptest du pip install selbst. Es war ein bewusster Moment. Du sahst den Paketnamen. Du hattest wenigstens eine Sekunde, um dich zu fragen, ob das der richtige war, ob du ihn tatsächlich brauchtest, ob du vielleicht zuerst die Download-Zahlen prüfen solltest. Meistens prüftest du nicht. Aber die Reibung war da. Eine Bremsschwelle zwischen dir und einem potenziellen Supply-Chain-Kompromiss.

KI-Agenten entfernten diese Bremsschwelle vollständig.

Auf meinem Rechner kann ich die zwei Installationswellen vom Transkriptions-Projekt zurückverfolgen. Erste Welle um 14:42, Claude Code probierte openai-whisper. Zweite Welle um 15:45, es wechselte zu mlx-whisper, weil das erste auf Apple Silicon nicht gut funktionierte. Zwei vollständige Abhängigkeitsbäume übereinander geschichtet. Ich überprüfte keinen von beiden. Ich machte wahrscheinlich Kaffee während des zweiten.

Das ist Zustimmungsmüdigkeit angewandt auf Abhängigkeiten. Der Agent schlägt vor, du drückst "y", die Pakete fließen rein. Derselbe Mechanismus, der dich Cookie-Banner akzeptieren lässt, ohne sie zu lesen, außer dass der Cookie-Banner deine SSH-Schlüssel nicht stehlen kann.

Andrej Karpathys Take nach litellm war direkt: "yoink mit LLMs, weniger Abhängigkeiten." Und er hat prinzipiell recht. Weniger Angriffsfläche bedeutet weniger Risiko. Aber yoink löst nicht das Problem von Abhängigkeiten, die du nicht eliminieren kannst. litellm ist kein 50-Zeilen-Utility, das du copy-pasten kannst. torch auch nicht. scipy auch nicht. Manche Abhängigkeiten existieren, weil die Alternative ist, ein Jahrzehnt optimierten C-Code neu zu schreiben, und das macht niemand zwischen zwei Meetings an einem Donnerstag.

Ich erforschte das 3-Schichten-CLAUDE.md-Modell vor ein paar Monaten (Review, Durchsetzung, Absicht). Stellte sich heraus, es fehlte eine Schicht: Abhängigkeits-Sicherheitsrichtlinie. Das Modell sagt dem Agenten wie er Code schreiben soll, aber sagt nichts darüber, was er installieren darf.

Agenten schufen keinen neuen Angriffsvektor. Supply-Chain-Attacken existierten lange vor Claude Code. Was Agenten taten, ist die Reibung zu entfernen, die dich vor ihnen schützte. Es ist ein Verstärker, keine Ursache. Aber die Konsequenz ist identisch.

Der Agent schuf nicht die Verwundbarkeit. Er entfernte die Reibung, die dich vor ihr schützte.

Trust chain diagram showing: Dev approves agent → agent calls pip install → pip resolves dependency tree (invisible to dev) → each node in tree is a potential attack point. Visual contrast between "before agents" flow (conscious friction, dev sees package name) and "with agents" flow (automatic approval, dependency tree hidden). Flat geometric style.
Vertrauenskette: Vor vs. Nach KI-Agenten

Was ich in 45 Minuten änderte (Und was dich tatsächlich schützt)

Erstes, was ich tat:

pip freeze > requirements-snapshot-20250325.txt

Das war's. Ein Snapshot von allem, was aktuell global installiert ist. Keine Reparatur. Ein Foto vom Tatort, bevor du anfängst aufzuräumen. Weil du wissen musst, wie das Chaos aussieht, bevor du es anfasst.

Dann ging ich durch den Snapshot und pinnte jede Version exakt. Kein >= mehr. Kein ~= mehr. Jede einzelne Version auf ihre exakte Nummer gesperrt. Wenn du requests>=2.28 in irgendeiner Datei auf deinem Rechner hast, sagst du pip "installiere welche Version du willst, solange sie über 2.28 ist." Das schließt eine hypothetische kompromittierte 2.32 ein, die TeamPCP nächsten Dienstag hochlädt.

Nächstes: uv. Gebaut von Astral (die Leute hinter Ruff). Ich initialisierte mein Transkriptions-Projekt diesmal ordentlich:

uv init transcription-tool
cd transcription-tool
uv add mlx-whisper typer

Zwei direkte Abhängigkeiten. uv löste sie in 63 Pakete auf. Installierte 42 (der Rest sind plattformspezifische Alternativen). Und der Teil, der zählt: das Lockfile enthält SHA256-Hashes für jedes einzelne Paket.

Der Hash ist der echte Schutz. Was mit litellm passierte: TeamPCP lud eine neue Version mit demselben Versionsnummernmuster auf PyPI hoch. Wenn du pip mit >= genutzt hättest, hättest du es automatisch gezogen. Aber wenn du ein Lockfile mit Hashes nutzt, stimmt der Inhalt des Pakets nicht mit dem aufgezeichneten Hash überein. uv sync weigert sich zu installieren. Fertig. Attacke an der Tür blockiert.

pip hat standardmäßig keinen äquivalenten Mechanismus. Du kannst --require-hashes nutzen, aber du musst sie selbst generieren und pflegen. uv macht es automatisch bei jedem uv lock. Vollständige Offenlegung: uv ist ein junges Tool (Astral veröffentlichte es 2024), und das Ökosystem darum bewegt sich noch schnell. Pip mit --require-hashes gibt dir ähnlichen Schutz, wenn du etwas Etablierteres bevorzugst. Das Wichtige ist das Lockfile mit Hashes, nicht das spezifische Tool.

Letzter Schritt: venv-Isolation pro Projekt. Jedes Projekt bekommt seine eigene Umgebung. Kein globales pip mehr auf einer Maschine, die SSH-Schlüssel, API-Token und Cloud-Credentials in Umgebungsvariablen liegen hat. Ich fügte auch .nosync für iCloud hinzu, weil dein Cloud-Service deine venv zu synchronisieren, während du Abhängigkeitskonflikte debuggst, ist die Art von Erfahrung, die du nicht zweimal haben willst.

Vorher: 30 Pakete in globalem pip. Keine Spur, was sie installierte oder wann.
Nachher: 2 Abhängigkeiten. 63 aufgelöst. Jede mit einem SHA256-Hash. In einer isolierten venv, die den Rest meines Systems nicht berühren kann.

Die vier Zeilen, die ich zu meiner CLAUDE.md hinzufügte

Tools reichen nicht, wenn der Agent, der Pakete für dich installiert, keine Regeln hat. Also fügte ich vier Zeilen oben in meine CLAUDE.md hinzu:

1. Führe niemals pip/npm install ohne explizite Nutzergenehmigung aus
2. Bevor du eine neue Abhängigkeit vorschlägst, liste ihre wichtigsten Sub-Abhängigkeiten auf
3. Bevorzuge das Generieren von Utility-Code gegenüber dem Hinzufügen einer Abhängigkeit, wenn die Funktionalität unter 200 Zeilen liegt
4. Pinne immer exakte Versionen, nutze niemals >= oder ~=

Ich hatte bereits "niemals installieren ohne zu fragen" in meinem Prompt-Contracts-Framework. Diese Regel war notwendig, aber nicht ausreichend. Ich genehmigte immer noch blind, weil ich den Abhängigkeitsbaum nicht sehen konnte.

Regel 2 ist die, die Reibung wiederherstellt. Wenn Claude Code mir sagt "mlx-whisper braucht torch, numpy, scipy, huggingface_hub, tokenizers und 25 transitive Abhängigkeiten," habe ich wieder einen bewussten Moment. Ich kann entscheiden, ob ein Transkriptions-Tool es wert ist, die Hälfte von PyTorch auf meinen Rechner zu ziehen. Vielleicht ist die Antwort immer noch ja. Aber wenigstens entscheide ich, anstatt abzustempeln.

Regel 3 ist das Karpathy-Prinzip mit einem Leitplanke. Wenn die Funktionalität einfach genug ist, generiere den Code, anstatt ein Paket zu installieren. Aber "einfach genug" braucht eine Grenze, sonst schreibst du am Ende Kryptographie-Bibliotheken von Grund auf neu (bitte tu das nicht). Zweihundert Zeilen ist meine Schwelle. Deine könnte anders sein.

Regel 4 ist die grundlegendste und die am meisten ignorierte. Jedes >= in deinem Projekt ist eine Tür, die du offen gelassen hast, damit jemand anderes hindurchgehen kann.

Diese vier Zeilen sind nicht magisch. Ein ausreichend fähiger Agent könnte sie umgehen. Aber für aktuelle Produktions-KI-Coding-Agenten ist es der Unterschied zwischen blind installieren und wissentlich installieren. Diese Regeln schützen nicht vor Paketen, die bereits in deinen bestehenden Abhängigkeiten kompromittiert sind. Sie verhindern zukünftige blinde Installationen. Das Auditieren dessen, was bereits da ist (pip-audit, uv lock), ist ein separater Schritt, den du immer noch machen musst.

Die Reibung, die dein Agent entfernte, stellst du mit vier Zeilen in deiner CLAUDE.md wieder her.


Das nächste litellm kommt. Keine Frage des "ob." Und diesmal wird der Angreifer keinen Bug machen, der dein RAM zum Absturz bringt. Die Malware wird still sein, die Exfiltration leise, und du wirst nicht wissen, dass deine SSH-Schlüssel weg sind, bis du Commits siehst, die du nie gemacht hast.

Bevor du vom nächsten getroffen wirst, öffne dein Terminal. Führe pip freeze aus. Zähle die Pakete, die du nicht erkennst. Installiere uv. Erstelle eine venv. Öffne deine CLAUDE.md und füge vier Zeilen vor deiner nächsten Session hinzu. 45 Minuten. Null Kosten.

Dein KI-Coding-Agent ist genau so sicher wie die Regeln, die du ihm gibst. Keine Regeln, keine Sicherheit. Nur Glück. Und Glück ist kein Plan.

Quellen: LiteLLM Supply Chain Compromise GitHub Thread: github.com/BerriAI/litellm/issues/9484 | uv Dokumentation: docs.astral.sh/uv

(*) Das Cover ist KI-generiert. Meine tatsächliche Terminal-Ausgabe während des Scans war viel weniger fotogen.