Claude Code „Don't ask again“ und settings.local.json: So findest du die Passwörter in deinen Allow-Regeln, und das solltest du ändern
Im aktuellen Claude-Code-Release macht der Prompt aus dem Großteil eines Befehls eine Wildcard, bevor er die Regel speichert, und ein Passwort, das vor dem Programm steht, überlebt trotzdem. Das Session-Transkript behält den ganzen Befehl, und der sauberste Fix ist eine Kommandozeile, die das Secret gar nicht erst enthält.
Inhalt
Am 5. November hat Apiiro eine Zahl veröffentlicht, die die meisten Claude-Code-Nutzer in fünf Minuten an ihrem eigenen Laptop nachprüfen können. Von 104.185 öffentlichen Kopien von .claude/ auf GitHub enthalten 1.122 etwas, das Apiiro als echtes Secret einstuft: Datenbankpasswörter, Cloud-Keys, API-Tokens, Root-Logins per SSH. Nimmt man die Git-History der Datei dazu, die geteilte settings.json und eingecheckte Agent-Transkripte, kommt man auf 2.539 verschiedene Secrets in 1.994 Repositories, verteilt auf 63 Organisationen. Apiiro hat keines davon ausprobiert. „Echt“ heißt also: ein bekanntes Key-Format oder ein Passwort neben einem öffentlichen Host, kein bestätigter funktionierender Login. Ein Unternehmen hat immerhin bestätigt, dass sein Key aktiv war.
Der Mechanismus ist banal. Claude Code fragt, bevor es einen Shell-Befehl ausführt, du wählst „Yes, and don’t ask again“, und aus der Freigabe wird eine Permission-Regel in genau dieser Datei. Stand im Befehl ein Passwort, kann es auch in der Regel stehen. Ich habe den Prompt auf Claude Code 2.1.319 mit Platzhalter-Credentials nachgestellt, um zu sehen, was das aktuelle Release speichert. Es ist nicht ganz das, was der Report beschreibt, und der Unterschied ändert, wonach du suchen solltest.
Was schreibt „Don’t ask again“ in Claude Codes settings.local.json?
Eine allow-Regel, geschrieben in .claude/ im Root des Git-Repositories und gültig für jede künftige Session dort. In meinen Tests auf 2.1.319 war die angebotene Regel ein Präfix mit Wildcard, nicht der vollständige Befehl. Übrig blieb, was vor dem Programm stand, eine Umgebungsvariable mit Passwort eingeschlossen.
Die Permissions-Doku sagt, Bash-Freigaben werden „permanently per repository and command“ gespeichert und der Dialog schreibt die Form mit Leerzeichen und *, wenn er ein Befehlspräfix anbietet. Das hier hat der Prompt für sechs Befehle angeboten, in einem frischen Repository, im Manual-Modus. Der erste ist absichtlich konstruiert, damit eine Zuweisung und ein Passwort als Flag im selben Befehl stehen:
command: PGPASSWORD=hunter2placeholder sshpass -p hunter2placeholder ssh app@db.example.invalid true
offered: PGPASSWORD=hunter2placeholder sshpass *
command: PGPASSWORD=hunter2placeholder psql -h db.example.invalid -U app -c 'select 1'
offered: PGPASSWORD=hunter2placeholder psql *
command: DATABASE_URL=postgresql://app:hunter2placeholder@db.example.invalid/app npm run migrate
offered: DATABASE_URL=postgresql://app:hunter2placeholder@db.example.invalid/app npm run *
command: mysql -h db.example.invalid -u app -phunter2placeholder -e 'select 1'
offered: mysql *
command: curl -H "Authorization: Bearer sk-placeholder-0000" https://api.example.invalid/v1/me
offered: curl *
command: psql postgresql://app:hunter2placeholder@db.example.invalid/app -c 'select 1'
offered: psql *Ich habe den ersten akzeptiert, und die Datei sah danach so aus:
{
"permissions": {
"allow": [
"Bash(PGPASSWORD=hunter2placeholder sshpass *)"
]
}
}Das Passwort hinter sshpass -p ist also in der Wildcard verschwunden, das hinter PGPASSWORD= ist geblieben. Das passt zur Matching-Regel in der Doku: Eine Allow-Regel „won’t match past an assignment“ für jede Variable außerhalb einer eingebauten Liste bekannt harmloser Variablen. Ein Präfix, das beim nächsten Lauf wieder matchen soll, muss die Zuweisung also enthalten, samt Wert.
Apiiro beschreibt die Regel als exakten Befehl, und die gezählten Dateien hat jeweils die Version geschrieben, die der Entwickler damals benutzt hat. Meine Tests zeigen nicht, seit wann der Prompt Präfixe vorschlägt. Lies deshalb jede Datei, die du findest, egal wie alt. Die Wildcard hat außerdem ihren eigenen Preis. curl * und mysql * enthalten kein Secret, genehmigen aber ab sofort jedes curl und jedes mysql in diesem Repository, eine viel größere Freigabe als der eine Befehl auf dem Bildschirm. Claude Codes Bash-Regeln matchen Befehlstext, nicht das Programm, und eine breite Allow-Regel ist deshalb auf Arten breit, die der Prompt nie zeigt.
Warum landet settings.local.json in git, obwohl Claude Code sie ignoriert?
Weil Claude Code sie in deiner globalen Git-Konfiguration ignoriert, nicht im Repository. Der Ignore-Eintrag existiert auf einer Maschine, und nichts im Projekt nimmt ihn irgendwohin mit.
Die Settings-Doku ist da genau: Wenn Claude Code die Datei zum ersten Mal in einem Git-Repository schreibt, das sie noch nicht ignoriert, trägt es **/. in deine globale Excludes-Datei ein. Das ist core.excludesFile, wenn deine globale Git-Konfiguration es auf einen absoluten oder ~-Pfad setzt, sonst $XDG_CONFIG_HOME/, oder ~/.config/git/ignore, wenn diese Variable nicht gesetzt ist. Hast du die Datei von Hand angelegt, sollst du sie laut Doku selbst in .gitignore eintragen.
Apiiro listet auf, wo das nicht reicht: ein Ordner, der erst später zum Repository wird, ein Klon auf einer anderen Maschine, eine Datei, die schon jemand committet hat. Dazu kommen ein git add -f, ein Docker-Image, das mit einem breiten Copy gebaut wird, ein Zip des Projektordners. Und oft committet der Agent die Datei selbst: In 539 der 1.105 Repositories mit einem Secret in dieser Datei, die Apiiro zurückverfolgt hat, hat ein von Claude mitverfasster Commit sie geändert.
Sobald git die Datei trackt, behandelt Claude Code sie als vom Repository geliefert und hält ihre Allow-Regeln zurück, bis du dem Ordner vertraust. Das schützt jeden, der das Repository klont, vor deinen Freigaben. Für das Passwort tut es nichts.
Wie prüfst du ein Repository auf eine committete settings.local.json?
Drei Git-Befehle pro Repository, danach eine Code-Suche über die ganze Organisation. Führ sie im Root des Repositories aus, in einem vollständigen Klon statt einem Shallow Clone, weil der dritte Befehl nur die History durchsuchen kann, die lokal vorliegt:
git check-ignore -v --no-index .claude/settings.local.json
git ls-files ':(glob)**/.claude/settings*.json'
git log --all --oneline -- ':(glob)**/.claude/settings*.json'check-ignore nennt Datei und Zeile, die sie ignorieren (--no-index sorgt dafür, dass es auch für eine getrackte Datei antwortet), ls-files listet die Kopien, die git aktuell trackt, und log listet jeden Commit, der je eine davon angefasst hat. Der Glob deckt verschachtelte Kopien ab. Das zählt, weil Claude Code die lokale Datei vor v2.1.211 in dem Verzeichnis abgelegt hat, in dem die Session gestartet wurde. Und er erfasst auch die geteilte settings.json.
In meinem Test-Repository habe ich die Datei committet und danach wieder untrackt. ls-files gab nichts aus, log zeigte weiterhin beide Commits. Genau das ist der Fall, um den du dich kümmern musst: Die Datei ist aus dem Tree verschwunden, das Passwort steckt noch in der History, und in jedem Klon und Fork, der diese Commits bekommen hat. Apiiro hat 734 Secrets gefunden, die nur noch in früheren Versionen der Datei existieren, und 211 Forks von 74 Repositories, die dieselben mitschleppen.
Für eine Organisation findet die Code-Suche von GitHub die aktuellen Kopien:
Q='.[] | select(.path | test("(^|/)\\.claude/")) | .repository.nameWithOwner + ":" + .path'
gh search code '{' --owner your-org --filename settings.local.json --limit 1000 --json repository,path -q "$Q"
gh search code '{' --owner your-org --filename settings.json --limit 1000 --json repository,path -q "$Q"Das { steht da, weil die Suche einen Suchbegriff braucht und jede JSON-Datei eins enthält. Der Filter behält nur Dateien in einem .claude-Ordner, weil --filename auch Namen wie managed-settings.json trifft. gh liefert 30 Ergebnisse, solange du das Limit nicht hochsetzt, und die Code-Suche liest nur den Default-Branch indexierter Repositories. Ein leeres Ergebnis beweist also wenig. Für die History brauchst du weiterhin die git log-Zeile in jedem Repository. Prüf auch die geteilte .claude/settings.json: Teams committen sie absichtlich, und Apiiro hat in 298 davon echte Secrets gefunden, die meisten in Allow-Regeln.
Wie findest du Passwörter in Claude-Code-Allow-Regeln auf deiner Maschine?
Such jede Kopie der Datei und durchsuch sie dann nach den Mustern, die Secret-Scanner übersehen. Die meisten dieser Dateien landen nie auf GitHub, und eine schlichte JSON-Datei in einem Projektordner ist genau das, was Malware zum Abgreifen von Credentials liest. Apiiro weist darauf hin, dass der npm-Wurm Shai-Hulud TruffleHog über die Dateien seiner Opfer laufen ließ.
PAT='PASSWORD=|sshpass -p|-p[^ "*)]{3,}|Bearer [^ "*]+|://[^/:@ ]+:[^@ ]+@|(api[_-]?key|token|secret)='
find ~ -path '*/.claude/settings*.json' -not -path '*/node_modules/*' -exec grep -lEi "$PAT" {} + 2>/dev/nullDer Befehl listet Dateinamen, keine Treffer. So laufen die Secrets nicht über den Bildschirm, und eine Session, aus der du ihn startest, kopiert sie nicht in ihr eigenes Transkript. Öffne die genannten Dateien in einem Editor außerhalb des Agents. Das Muster ist mit Absicht grob. Es meldet ein Passwort nach PGPASSWORD= oder mysql -p, einen Bearer-Header, ein Passwort in einem Connection String und die üblichen token=-Zuweisungen, aber auch --port, --prefix und jede Menge andere Wörter mit Bindestrich. Auf einem großen Home-Verzeichnis gibt es also Fehltreffer. Das ist der Preis dafür, zu erwischen, was spezialisierte Tools auslassen. Apiiro hat gitleaks und TruffleHog über alle 104.185 Dateien laufen lassen: Von den 1.122 mit echtem Secret hat gitleaks 564 gemeldet, TruffleHog 643, und 215 hat keines der beiden erkannt. Ein Passwort, das in einen Befehl getippt wurde, ist ein gewöhnliches Wort auf einer gewöhnlichen Kommandozeile, ohne Provider-Präfix, auf das ein Scanner anspringen könnte.
Danach durchsuchst du die Session-Transkripte, die mehr behalten als die Regel. Claude Code speichert jede Session als JSON Lines unter ~/.claude/projects/, und der Tool-Aufruf wird so aufgezeichnet, wie Claude ihn geschrieben hat. In meinem Test stand das sshpass -p-Passwort, das die Regel durch * ersetzt hatte, vollständig im Transkript, ebenso die Befehle, die ich abgelehnt hatte:
grep -rlEi "$PAT" ~/.claude/projects/ 2>/dev/nullDas gilt auch, wenn du nie auf „Don’t ask again“ klickst. Erscheint kein Prompt, etwa im Modus bypassPermissions, wird keine Regel geschrieben, aber das Transkript zeichnet den Befehl trotzdem auf.
Was tust du zuerst, wenn du ein Secret in settings.local.json findest?
Rotieren. Die Regel löschen, die Datei untracken oder die History umschreiben entfernt deine Kopie, aber keine der anderen. Das Credential muss also aufhören zu funktionieren, bevor das Aufräumen etwas wert ist. Apiiro empfiehlt dieselbe Reihenfolge: erst rotieren, dann aufräumen, und jedes Credential rotieren, das die Datei je enthalten hat.
Danach, in jedem Repository, in dem ls-files die Datei als getrackt gezeigt hat:
git rm --cached .claude/settings.local.json
echo '.claude/settings.local.json' >> .gitignore
git add .gitignore
git commit -m "Stop tracking Claude Code local settings" -- .gitignore .claude/settings.local.jsonDie Pathspec am Commit hält alles andere, was du gestaged hattest, aus dem Commit heraus. In jedem Repository, getrackt oder nicht, löschst du die Regeln mit dem Secret über /permissions oder direkt in der Datei, und die Zeile in .gitignore bleibt drin. So reist der Schutz mit dem Code, statt in der Git-Konfiguration einer einzelnen Maschine zu wohnen. Wenn du zusätzlich die History umschreibst, um das Secret zu entfernen, dann nach der Rotation und als Hygiene: Klone und Forks haben die alten Commits schon.
Wie hältst du Secrets aus Claude-Code-Permission-Regeln heraus?
Halt das Secret von der Kommandozeile fern. Der Lehrbuchweg dafür, eine $VAR-Referenz, hat in meinem Test bei jedem Lauf nachgefragt. Eine Credential-Datei oder ein Skript mit festem Zweck nicht.
Apiiro rät wie die meisten, das Secret als $VAR zu referenzieren, damit die Regel den Namen speichert und nicht den Wert. Für den einen Befehl, den ich ausprobiert habe, psql "$DATABASE_URL" -c 'select 1', bot der Prompt überhaupt keine „Don’t ask again“-Option an. Als ich Bash(psql *) von Hand in die Datei geschrieben habe (Claude Code lädt die Datei beim Speichern neu, und eine Regel, die ich ein paar Minuten später genauso ergänzt habe, griff ohne Neustart), fragte derselbe Befehl weiterhin nach, mit dieser Zeile:
A variable in this command can’t be checked before it runs
Sinngemäß: Eine Variable in diesem Befehl lässt sich vor der Ausführung nicht prüfen. Für ein Permission-System ist das eine vernünftige Regel, denn am Befehlstext kann Claude Code nicht ablesen, wozu die Variable expandiert. Es heißt aber auch, dass die $VAR-Form immer weiter fragt, und ein Prompt, der immer weiter fragt, ist ein naheliegender Weg zurück zum Wert direkt im Befehl.
Der bessere Fix: Lass das Tool sein Credential selbst finden. psql liest ~/.pgpass, eine Zeile hostname: pro Server, und ignoriert die Datei, solange ihre Rechte nicht streng genug sind, also chmod 0600 ~/.pgpass. Der Befehl enthält dann kein Passwort mehr, vorausgesetzt, nichts anderes darin enthält eins: Nimm das Passwort auch aus der Connection-URI heraus. Es bleibt eine Klartextdatei, nur eben eine außerhalb deiner Projekte. SSH-Keys ersetzen sshpass -p vollständig.
Wo es so eine Datei nicht gibt, kann ein Wrapper-Skript das Nachschlagen übernehmen. In meinem Test lief ein Skript, das DATABASE_URL aus seiner eigenen Umgebung las und mit dieser Regel freigegeben war, ohne Nachfrage:
{
"permissions": {
"allow": [
"Bash(./scripts/db-query.sh *)"
]
}
}Diese Regel nennt eine Datei, die du geschrieben hast, statt eines Credentials. Sie autorisiert aber alles, was das Skript mit seinen Argumenten macht, und das Skript kann sich ändern, nachdem du es freigegeben hast. Ein Wrapper, der freien Text an psql -c weiterreicht, übergibt einen vollständigen Client, Backslash-Befehle inklusive. Gib dem Skript genau eine feste Aufgabe (einen Status-Check, einen benannten Report), keine freie Eingabe, einen Datenbank-Login, der nur diese Aufgabe kann, und nimm lieber eine Regel mit exaktem Match ohne *. Außerdem darf es das Secret nie ausgeben, denn seine Ausgabe landet ebenfalls im Transkript.
Zwei kleinere Gewohnheiten helfen. Eine Deny-Regel Read(./.env) hält Claudes Datei-Tools davon ab, den Wert in den Kontext zu ziehen, wobei die Doku klar sagt, dass sie kein Skript abdeckt, das die Datei selbst öffnet. Und verschieb Secrets nicht in den env-Block von settings.local.json. Damit landen sie nur in genau der Datei, um die es in diesem ganzen Artikel geht. Die Sandbox ist eine andere Schicht: Sie begrenzt, wohin Befehle reichen, und sie schwärzt kein Secret, das schon in einem Tool-Aufruf oder einer Regel steht.
Hilft die IP-Allowlist der Claude Console bei einem geleakten Claude-API-Key?
Bei einem API-Key der Claude Platform, als Rückfallebene. Sie begrenzt, wo ein geleakter Key funktioniert, nicht wie lange, und für die Datenbankpasswörter und anderen Nicht-Claude-Credentials in Apiiros Datensatz tut sie nichts.
Die Doku zum IP-Allowlisting beschreibt eine Beta, die ein Admin oder Owner der Organisation in der Console unter Identity and access einschaltet. Sie gilt für die Endpoints Messages, Batches, Token Counting, Models, Files und Skills, für die Endpoints der Claude Managed Agents und für Admin- und Compliance-API-Keys, die nur diese Organisation abdecken. Eine Anfrage von außerhalb der Liste bekommt einen 403 permission_error. Bedrock, Claude Platform on AWS, Google Cloud und Microsoft Foundry deckt sie nicht ab. Einträge dürfen nicht breiter als /8 für IPv4 oder /32 für IPv6 sein, und Änderungen brauchen bis zu 15 Minuten. Das passt zu Keys, die von Servern und aus CI mit festen Egress-Adressen genutzt werden, nicht zu Laptops in wechselnden Netzen, und Anthropics eigene Seite empfiehlt zusätzlich eine kurze Key-Laufzeit oder Workload Identity Federation.
Claude Codes Allow-Liste war nie der richtige Ort für ein Secret
Die Regeldatei ist die Kopie, die du siehst, das Transkript die vollständige. Der Prompt in 2.1.319 hält die meisten Argumente aus der vorgeschlagenen Regel heraus, und in diesen Tests hat er ein als Präfix geschriebenes Passwort trotzdem behalten. Ein Fix, der nicht davon abhängt, was der Prompt speichert, ist eine Kommandozeile, die das Secret nie enthält: Dann haben die Regel, der Transkript-Eintrag für diesen Befehl und der Commit nichts zu behalten, egal wie die Vorschläge der nächsten Version aussehen. Ausgaben und gelesene Dateien können ein Secret trotzdem in eine Session tragen, das hier schließt also einen Weg, den, den Apiiro gezählt hat. Ein Prompt, den du nicht mehr liest, ist ein schlechter Ort für diese Entscheidung, und deshalb sind die Freigaben, die die Aufmerksamkeit eines Menschen verdienen, die seltenen.
01 / Diskussion
Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.
02 / Weitere Artikel
Claude Code und deine E-Mail im User-Agent: was der Privacy-Mod prüft und was ein Hook und eine Allowlist ergänzen
Niemand hat ein Feature gebaut, das deine Adresse an Websites schickt. Das Modell findet sie in seinem Kontext, trifft auf eine Website, die von Skript-Clients Kontaktdaten verlangt, und hilft. Jede Lösung, die bisher vorgeschlagen wurde, fängt die Adresse erst auf dem Weg nach draußen ab. Also ordne die Filter danach, welche das Modell nicht aushebeln kann, und behalte im Blick, welche Lücken jeder davon offen lässt.
Prompt-Injection-Beispiele gegen Coding-Agents: wo der Text hineinkam und was ihn aufgehalten hat, falls überhaupt etwas
Neun öffentliche Fälle, sortiert nach der Tür, durch die die Worte des Angreifers kamen. In den meisten hat während des Angriffs nichts gehalten. Die Fixes danach folgten alle einem Muster: Entweder wurde eine deterministische Prüfung neu ausgerichtet, oder eine Fähigkeit fiel weg. Ein Modellurteil, das versagt hat, lässt sich nicht neu ausrichten.
Backdoor in der Claude-Code-Extension im Cursor-Marketplace: was ausgeführt wurde, was nach der Deinstallation bleibt und warum die Versionsnummer nicht verrät, welchen Build du bekommen hast
Die manipulierten Builds trugen Anthropics Publisher-Namen und in einem Fall eine Version, die Anthropic wirklich veröffentlicht hat. Beides sagt nichts darüber, wer die Bytes gebaut hat, und was sie angelegt haben, bleibt auf dem Rechner, wenn die Extension längst weg ist.