25 Jahre lang vermied ich Malware. Claude Code speicherte trotzdem 600 meiner Geheimnisse.
25 Jahre am Keyboard, kein einziger Malware-Treffer. Null. Ich erkenne eine als PDF getarnte .scr-Datei schon von der anderen Raumseite. Ich rieche ein verdächtiges postinstall-Script auf zehn Meter Entfernung. Mit 14 habe ich sogar selbst zwei, drei Viren geschrieben, nur um die Mechanik zu verstehen (die Biologie davon faszinierte mich: Replikation, Mutation, Persistenz). Den Angreifer kenne ich von innen.
Heute Morgen habe ich mein Home-Verzeichnis der letzten 12 Monate auditiert. 600 Secrets im Klartext auf meiner Festplatte 😬. GitHub PATs, OAuth-Token, AWS-Keys, Google API, JWTs, das komplette Buffet. Nicht in einer vergessenen .env in einem öffentlichen Repo. Nicht in einem verpatzten Commit. In JSONL-Dateien, vergraben in ~/.claude, einem Verzeichnis, dessen Existenz ich vor zwei Wochen kaum registriert hatte.
Das ist kein Mea Culpa über schlechte Hygiene. Vor fünfzehn Jahren hatte ich 100 Passwörter im Keychain und das reichte. Heute schleppen wir Dutzende von API-Keys mit uns herum, Tools loggen sie ohne uns zu informieren, und meine 25-jährige Disziplin war nie auf sowas kalibriert.
Die alte Regel "sei vorsichtig, klick nicht auf zufälligen Kram" geht davon aus, dass ein Angreifer es auf mich abgesehen hat. Diese Bedrohung gibt es immer noch. Aber eine zweite Kategorie ist aufgetaucht: der Angreifer, der es überhaupt nicht auf mich abgesehen hat, sondern einfach mit meinen Privilegien läuft, eingepflanzt durch eine transitive npm-Dependency, die ich nie auditiert habe. Er landet in einem Home-Verzeichnis, das jetzt ein Museum von allem enthält, was ich jemals einem Assistenten gezeigt habe.
Deutlich gefährlicher. Zeit zu reagieren.

Das Audit: 5.904 Dateien, 171 betroffen, 600 Secrets im Klartext
Ich habe den Scan gegen ~/.claude/{projects,tasks,sessions,todos,shell-snapshots,paste-cache,file-history,debug} plus ~/.zsh_history und ~/.bash_history laufen lassen. 5.904 Dateien insgesamt. 1,1 GB kumulatives Gewicht. 171 dieser Dateien enthielten mindestens einen Credential.
Die Aufschlüsselung sieht ungefähr so aus:
- 95 GitHub OAuth-Token (
gho_) - 94 GitHub Fine-Grained PATs (
github_pat_) - 103 Google API-Keys (
AIza) - 197 JWT-ähnliche Strings (
eyJ) - 45 AWS Access Keys (
AKIA) - 18 OpenRouter
- 15 Resend
- 7 Anthropic OAuth
- 6 Telegram Bot-Token
- 3 Stripe Test-Keys
- 2 Vercel-Token
Etwa 600 Secrets über 171 Dateien verteilt. Fast alle rotierbar, viele bereits rotiert, während ich das hier schreibe. Ein Vorbehalt, den ich gleich hier platziere statt ihn am Ende zu vergraben: Der JWT_LIKE-Bucket ist verrauscht, er enthält Supabase Publishable Keys, die per Design öffentlich sind. Ich nehme die False Positives in Kauf. Ein False Positive kostet mich eine Schwärzung. Ein False Negative kostet mich einen Credential.
Das Lesen der JSONL fühlte sich an, als würde ich eine Autosave-Datei von einem Roguelike öffnen, von dem ich nie wusste, dass ich es spiele. Jeder Befehl, jedes Paste, jeder Read, für immer persistiert in der Reihenfolge, wie es passiert ist. NetHacks persistenter Dungeon, nur dass die Beute meine AWS-Keys sind.
Ein anderer Entwickler hat eine kleinere Version desselben Problems öffentlich in GitHub Issue #50014 am 17. April gemeldet: 5 verschiedene Secrets über 34 Session-Dateien nach etwa 30 Tagen Nutzung, 418 MB insgesamt.
30 Tage, 5 Secrets. 12 Monate, 600. Eine lineare Beziehung, die ich viel zu glaubwürdig finde.
Die Frage ist also nicht, ob das passiert. Sondern wie es passiert und was dagegen schützt.
Warum es passiert: fünf Wege zu einem Klartext-Transcript
Der Mechanismus ist mechanisch, nicht mysteriös. Jede Claude Code-Session schreibt eine JSONL-Datei in ~/.claude/projects/<project-hash>/<session-id>.jsonl. Jede Zeile ist ein Record: eine User-Message, eine Assistant-Reply, ein Tool-Call, ein Tool-Result. Die Datei ist append-only. Nichts bereinigt sie. Nichts schrubbt sie. Sie liegt da, solange du willst, und auf den meisten Macs bedeutet das für immer.
Fünf Wege führen ein Secret in diese Datei:
-
Bash-Output von einem legitimen Befehl.
infisical secrets get MY_TOKEN --plain,gh auth token,vercel token,cat .env,security find-generic-password -w,printenv | grep TOKEN,echo $SECRET. Alles, was einen Credential nach stdout druckt, zeichnet die JSONL auf. -
Manuelles Paste von dir im Chat. Du lässt einen Token in den Prompt fallen, um Claude zu bitten, ihn zu verwenden. Der Token ist jetzt für immer Teil des User-Message-Records.
-
File Read durch das
Read-Tool. Du bittest Claude,.envfür Kontext anzuschauen. Der Dateiinhalt landet im Tool-Result-Record. -
File Write mit einem hardcodierten Secret. Du bittest Claude, eine Config zu scaffolden und das Secret landet im neuen Dateiinhalt. Bonus: Derselbe Inhalt wird unter
~/.claude/file-history/dupliziert. -
Explizite Anzeige durch Claude in einer Antwort. Du fragst "was ist der Wert?", Claude druckt ihn zurück. Die Antwort steht im Assistant-Message-Record.
Fünf Wege, eine Datei, append-only, Klartext. Kein Purge, keine Rotation, kein Scan.
Jetzt der Schwenk. Die alte Verteidigung "paste deine Secrets nicht an zufällige Orte, führe keine sketchy Befehle aus" war für einen Angreifer gebaut, der dich angreift. Diese Verteidigung funktioniert immer noch gegen diesen Angreifer. Das Problem ist, dass eine andere Kategorie aufgetaucht ist, und sie umgeht die alte Verteidigung, ohne sie zu brechen.
Ich habe den LiteLLM-Hijack vor acht Monaten zu meinem eigenen pip-Cache zurückverfolgt, und die Lektion war bereits da: Ein vergiftetes Package landet mit den Privilegien des Users und beginnt zu lesen, was der User lesen kann. Im März 2026 vergiftete die TeamPCP-Kampagne 75 GitHub Action-Tags und schob bösartige Payloads an 141+ npm-Packages durch gestohlene CI/CD-Secrets. Im April 2026 fanden Check Point-Forscher 33 npm-Packages, die öffentlich .claude/settings.local.json-Dateien mit Inline-Credentials auslieferten. GitHub PATs, Telegram-Token, Production Bearer-Token, alles dabei.
Drei verschiedene Kampagnen. Dieselbe Mechanik. Der Angreifer klopft nicht an meine Tür. Er ist ein Script, das mit meinen Privilegien läuft, abgeworfen von einer Dependency, die ich nie direkt auditiert habe. Und sobald dieses Script in meinem Home-Verzeichnis lebt, sind meine 1,1 GB Claude Code-Transcripts eine Goldgrube.
GitGuardians 2026-Report bringt den breiteren Trend in Zahlen: AI-Service-Credential-Exposures, die auf öffentlichem GitHub entdeckt wurden, sprangen um 81% im Jahresvergleich. Claude Code-assistierte Commits leaken Secrets mit etwa 3,2%, gegen 1,5% für die Public-Commits-Baseline. AI-beschleunigte Commits leaken Secrets mit doppelter Rate. Der Shift ist branchenweit, nicht nur meine Festplatte.
Also hier ist der Schwenk, klar und deutlich.
Disziplin verteidigt gegen Angreifer, die dich angreifen. Mechanische Leitplanken verteidigen gegen Angreifer, die als du laufen.
Sogar dein Vault leckt in dem Moment, wo er seinen Job macht
Ich hatte Infisical. Ich hatte Deny-Rules. Ich hatte Runtime-Injection. Ich hatte trotzdem 600 Secrets im Klartext.
Die Geschichte ist ärgerlich, weil die Hygiene korrekt war. Das Secret schläft nicht in einer .env. Es lebt in einem verschlüsselten Vault. Und trotzdem.
Die Mechanik ist der Resolution-Step selbst. Das Secret verlässt den Vault für zwei Sekunden, um seinen Job zu machen, und diese zwei Sekunden existieren irgendwo: im Klartext in Bash-Output, im Klartext in einer Process-Env, im Klartext in einem manuellen Paste. Während dieser zwei Sekunden hört Claude Code zu. Die JSONL macht sich Notizen.
Schau dir den Unterschied zwischen zwei Befehlen an, die identisch aussehen:
infisical secrets get MY_TOKEN --plain
curl -H "Authorization: Bearer $(cat last_token.txt)" https://api...
MY_TOKEN=$(infisical secrets get MY_TOKEN --plain)
curl -H "Authorization: Bearer $MY_TOKEN" https://api...
Ein Zeichen Unterschied. Zwei entgegengesetzte Schicksale. Der erste schreibt den Token ins Transcript. Der zweite lässt ihn nie stdout berühren.
Ein Vault ist ein Schloss. Ein JSONL-Transcript ist ein Museum. Beide bewahren das Secret auf. Nur eines stellt es zur Schau.
Die 4-Schichten-Verteidigung, die ich von Grund auf bauen musste

Die Reihenfolge ist wichtig. Die früheste Schicht feuert, bevor das Secret die Festplatte berührt. Die späteste Schicht räumt auf, was durchgerutscht ist. Du willst beide. Stell es dir vor wie einen Raid zu planen: Jede Phase reduziert die Angriffsfläche für die nächste, und die letzte Phase räumt die Beute auf, die der Boss auf den Boden fallen gelassen hat.
Schicht 1: PreToolUse-Hook auf Bash. Er fängt die riskanten Patterns ab, bevor der Befehl läuft. infisical secrets get --plain nicht gepiped, gh auth token nicht gepiped, vercel token, security find-generic-password -w nicht gepiped, cat .env|.envrc|.netrc|.npmrc, printenv|env mit grep nach token|secret|key|password, echo $VAR_SECRET. Der Hook gibt ein JSON permissionDecision: ask mit einer Nachricht zurück, die das sichere Pattern erklärt. Kein strikter Block. Ein False Positive darf den Workflow nicht brechen, sonst deaktiviere ich den Hook innerhalb einer Woche und wir wissen beide das.
Schicht 2: UserPromptSubmit-Hook. Er scannt den Text, den ich einreiche, bevor er ins Transcript geht. Match bedeutet decision: block. Das eingefügte Secret schafft es nie in die JSONL. Dieselben Regex-Sets wie die anderen Schichten, plus ein extra Pattern für vollständige URLs mit eingebetteten Credentials.
Schicht 3: SessionEnd-Hook + Scrubber. Wenn eine Session sauber endet, glob-matched der Hook ~/.claude/projects/*/<session_id>.jsonl, schrubbt die Datei in place, validiert, dass jede Zeile immer noch gültiges JSON ist, schreibt atomisch (tmp-Datei plus os.replace). Das bringt das Leak-Fenster von 24 Stunden auf ein paar Sekunden runter.
Schicht 4: Täglicher Cron um 04:00. Netz für Sessions, die ungraceful gestorben sind. kill -9, Crash, Stromausfall, alles wo SessionEnd nie gefeuert hat. Der Cron läuft dieselben Pfade ab und macht denselben Scrub. Gürtel und Hosenträger.
Eine Verhaltensregel in Claudes Memory ("drucke keine Secrets nach stdout") ist Schicht null, die schwächste. Ich behalte sie als höflichen Hinweis. Das 600-Secret-Audit ist genug Beweis, dass die Disziplin des Modells irgendwann einknickt.
Ein paar Design-Entscheidungen, die für jeden wichtig sind, der das kopieren will:
Der Scrubber ist Python 3, nur stdlib. Null Dependencies. /usr/bin/python3 bricht nie während eines Homebrew-Upgrades. Pattern Matching ist auf bekannte Präfixe verankert: sk-ant-api, ghp_, github_pat_, AKIA, gho_. Keine generische "20 alphanumerische Zeichen"-Regex, das würde False Positives bei jeder UUID, jedem Hash und jedem base64-Chunk in der Datei erzeugen.
Replacement ist deterministisch mit einem sha256-Fingerprint: [REDACTED-GITHUB_PAT_CLASSIC-a1b2c3d4]. Dasselbe Secret an zwei Stellen schwärzt zum selben Fingerprint. Ich kann Duplikate tracken, ohne jemals den Wert selbst zu speichern. JSON-Validität bleibt erhalten, die Substitution passiert innerhalb des bestehenden String-Fields, die Struktur bleibt intakt.
Keine Pre-Scrub-Backups. Eine Backup-Datei ist nur eine weitere Kopie des Secrets, und ein Dieb liest sie genauso leicht wie das Original.
Ich habe das allererste Signal dieses ganzen Rabbit Holes vor einem Monat erwischt, als Claude Code sich weigerte, meine eigenen Secrets während eines Folder-Moves zu kopieren. settings.local.json tauchte mit Credentials im Klartext auf, an einem Ort, wo ich nie hinzuschauen gedacht hätte. Das Audit, das ich hier beschreibe, begann, weil ich mich weigerte zu glauben, dass das der einzige Ort war.
Der vollständige Code ist auf GitHub.
Der Scrubber muss nicht vorsichtig sein. Er läuft. Der Hook muss nicht smart sein. Er blockiert.
Was immer noch leckt und die neue Regel, nach der ich lebe
Die Verteidigung ist ein Perimeter, keine Mauer. Ehrliche Liste dessen, was durchrutscht:
Secrets ohne identifizierbares Präfix. URLs der Form https://user:pass@host. Beliebige Passwörter. UUID v4, die als Bearer-Token verwendet werden. Verankerte Patterns bedeuten gute Präzision, aber partielle Abdeckung. Ich akzeptiere diesen Tradeoff, weil die Alternative eine Regex ist, die jeden base64-String flaggt und innerhalb eines Tages deaktiviert wird.
Bash-Obfuskation. eval $(echo "infisical secrets get X --plain"), Custom-Aliases, die den gefährlichen Befehl wrappen, alles Indirekte. Schicht 1 fängt die direkten Patterns ab. Sie fängt keinen entschlossenen Obfuskator ab (was, um fair zu sein, hauptsächlich ich bin, der versucht, den Hook zu widerlegen).
Scope ist strikt Claude. ~/.claude, ~/.zsh_history, ~/.bash_history. Out of Scope: ~/.aws/credentials, ~/.ssh/id_*, Keychain via security, env vars in laufenden Prozessen, Browser-Cookies. Andere Oberflächen sind andere Projekte.
False Positives bei JWT_LIKE. Supabase Publishable Keys werden unnötig geschwärzt. Ich verliere lieber ein paar öffentliche Keys durch Schwärzung, als einen echten zu verpassen.
Also hier ist die neue Regel.
Die alte Regel war "sei vorsichtig, klick nicht auf zufälligen Kram." Sie ging von einem entfernten oder menschlichen Angreifer aus. Immer noch gültig für diese Kategorie, und ich werfe sie nicht weg.
Die neue Regel ist anders.
Nichts Geheimes überlebt im Klartext auf dieser Festplatte.
Nicht aus Paranoia. Aus Pragmatismus. Morgen wird ein Script mit meinen Privilegien laufen, abgeworfen von einer Dependency drei Schichten tief, die ich nie von Hand reviewt habe, und die einzige Verteidigung, die hält, ist mechanisch.
In sechs Monaten
Ein Vendor wird "massives Credential-Leak via AI-Assistant-Transcripts" ankündigen und alle werden überrascht aussehen. Es wird einen Corporate-Post mit vagem Blame auf eine Third-Party-Storage-Schicht geben. Es wird Threads geben, die erklären, dass du offensichtlich diese eine Einstellung hättest togglen sollen. Alle werden sagen, es war ein isolierter Fall.
Währenddessen auditieren einige von uns unsere Festplatten. Basteln Hooks zusammen. Lesen den Source, wenn er leckt. Bauen Netze. Nicht aus Paranoia. Aus Pragmatismus.
Die alte Regel ging von einem Menschen auf der anderen Seite aus. Das ist schon eine Weile nicht mehr der Default.
Die nächste Verteidigungsschicht ist, unseren AIs beizubringen, sich zu benehmen. Unsere neuen digitalen Kinder, nicht wahr?