Sandbox Credential Masking: Tokens funktionsfähig halten und geheim halten
- Wann mask gegenüber deny zu bevorzugen ist — und wann deny noch sicherer ist
- Die vier Masking-Modi: whole-value, extract, decode: "jwt" mit maskClaims und awsPairs für SigV4
- Das exakte JSON, um Umgebungsvariablen, JWTs, AWS-Anmeldedaten und Dateien wie ~/.config/gh/hosts.yml zu maskieren
- Warum tlsTerminate zwingend erforderlich ist — und das Silent-Fail-Muster, wenn es fehlt
- Die Settings-Source-Regel: warum mask-Einträge aus .claude/settings.json ignoriert werden
- Der Linux/WSL- vs. macOS-Split: Datei-Masking wird auf macOS zu deny herabgestuft
Die meisten Geheimnis-Leaks passieren nicht, weil ein Token gestohlen wurde — sondern weil ein gut gemeintes Skript eins in ein Log, ein Diff oder das Transcript eines Subagenten ausgedruckt hat. Sandbox Credential Masking ist der eingebaute Fix von Claude Code: sandboxed Befehle sehen einen Session-spezifischen Sentinel-Wert statt des echten Geheimnisses, und der Sandbox-Proxy ersetzt den echten Wert bei ausgehenden Anfragen an Hosts, die du erlaubst. Der Befehl authentifiziert sich weiterhin. Der Befehl und alles, was er loggt, halten nie die echten Anmeldedaten.
Diese Seite ist der Praktikerleitfaden: die vier Masking-Modi, das exakte JSON, die Fallstricke und die OS-Matrix.
Mask vs. deny: welches willst du?
Zwei Wege, um Anmeldedaten aus den Händen eines sandboxed Befehls zu halten. Sie sehen ähnlich aus. Sie sind es nicht.
"mode": "deny" | "mode": "mask" | |
|---|---|---|
| Umgebungsvariable | Aus der Sandbox-Umgebung entfernt | Auf einen Session-spezifischen Sentinel gesetzt |
| Datei | Lesen schlägt fehl | Sandbox sieht eine Sentinel-Kopie (Linux/WSL2) oder Lesen schlägt fehl (macOS) |
| Tool, das das Geheimnis benötigt | Bricht (gh, npm, aws schlagen ohne Token fehl) | Funktioniert weiter — Proxy tauscht den echten Wert auf der Leitung ein |
| Echter Wert je in der Sandbox? | Nie | Nie (nur Sentinel; Proxy hält den echten Wert) |
Erfordert tlsTerminate | Nein | Ja — Proxy muss den Request-Inhalt sehen, um zu ersetzen |
| Aus Repo-Einstellungen berücksichtigt? | Ja | Nein — nur user, managed oder --settings |
Faustregel. Verwende deny, wenn das Tool die Anmeldedaten nicht braucht und du sie loswerden willst. Verwende mask, wenn das Tool sich authentifizieren muss — du willst, dass gh pr view funktioniert, ohne dass das Transcript, ein Subagent oder ein irrtümlicher env-Dump je das echte GH_TOKEN hält.
Voraussetzung: tlsTerminate
mask funktioniert, indem der Sentinel durch den echten Wert in den ausgehenden HTTP-Request-Headers und -Bodies ersetzt wird. Der Sandbox-Proxy muss diese Bytes sehen, also ist network.tlsTerminate zwingend erforderlich. Ohne es schlägt Masking auf die schlimmste Art fehl: der Befehl sieht weiterhin nur den Sentinel, der Sentinel erreicht den Server unverändert und die Authentifizierung schlägt fehl. Claude Code meldet diese Fehlkonfiguration beim Start — lies die Warnungen.
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com", "registry.npmjs.org"]
}
}
}
Jeder injectHosts-Eintrag, den du später verwendest, muss auch in network.allowedDomains erscheinen. Wenn ein Host nicht erlaubt ist, sieht der Proxy die Anfrage nie, um zu ersetzen.
Masking von Umgebungsvariablen
Der Grundfall. Ein envVars-Eintrag pro Anmeldedaten.
GH_TOKEN und NPM_TOKEN maskieren
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com", "registry.npmjs.org"]
},
"credentials": {
"envVars": [
{ "name": "GH_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] },
{ "name": "NPM_TOKEN", "mode": "mask" }
]
}
}
}injectHostsbeschränkt die Ersetzung auf bestimmte Hosts.GH_TOKENerreicht nie etwas anderes alsapi.github.com.- Lässt du
injectHostsweg, wird der echte Wert bei Anfragen an jeden Host innetwork.allowedDomainsersetzt. FürNPM_TOKENin Ordnung, wo das Token auf die Registry beschränkt ist. - Um zu bestätigen, dass die Maske aktiv ist: bitte Claude,
echo "$GH_TOKEN"in einem sandboxed Befehl auszuführen. Die Ausgabe sollte ein Session-spezifischer Sentinel sein, nicht das echte Token.
Extract: ein Feld innerhalb eines strukturierten Wertes maskieren
Viele "Anmeldedaten" sind kein blankes Geheimnis — sie sind ein Verbindungsstring mit einem darin vergrabenen Passwort. extract maskiert nur die Capture-Gruppe deines Regex und lässt den Rest lesbar, damit Parser weiter funktionieren.
Das Passwort innerhalb von DATABASE_URL maskieren, den Rest parsebar halten
{
"name": "DATABASE_URL",
"mode": "mask",
"extract": "://[^:]+:([^@]+)@"
}- Das Muster muss mindestens eine Capture-Gruppe enthalten; nur der Text von Gruppe 1 wird ersetzt.
onExtractNoMatchsteuert, was passiert, wenn das Muster nichts matcht:warn(Standard — unmaskiert durchgereicht mit Warnung),deny(fail closed) odererror(Sandbox fehlschlagen lassen). Verwendedeny, wenn das Geheimnis immer vorhanden sein sollte.
JWT-Masking mit decode und maskClaims
Für Access-Tokens in JWT-Form (header.payload.signature) bricht Whole-Value-Masking jeden Code innerhalb der Sandbox, der das Token dekodiert, um Claims anzusehen. decode: "jwt" behebt das: Claude Code verifiziert, dass der Wert ein gültiges JWT ist, und tauscht ein strukturell gültiges Fake-Token ein, sodass jwt.decode(...) innerhalb der Sandbox weiterhin einen wohlgeformten Payload zurückgibt.
Ein Session-JWT maskieren, aber die Form dekodierbar halten
{
"name": "SESSION_JWT",
"mode": "mask",
"decode": "jwt",
"maskClaims": ["sub", "email"]
}- Ohne
maskClaimsersetzt das komplette Fake-Token das echte — Code, der nurissoderaudbraucht, wird es egal sein, aber Code, dersubliest, bekommt einen Fake-Wert. - Mit
maskClaimsbleiben die anderen Claims lesbar; nur die aufgelisteten werden einzeln ersetzt. Nützlich, wenn die Appiat/exp/issfürs Routing braucht, abersub/emailnie sehen darf. decodekann nicht mitextractauf demselben Eintrag kombiniert werden. Wähle eins.- Wenn der Wert nicht als JWT verifiziert (oder kein gelisteter Claim matcht), reicht Claude Code ihn unmaskiert mit Warnung durch. Verwende
onExtractNoMatch: "deny", um fail closed zu erzwingen.
Erfordert Claude Code v2.1.224 oder neuer.
AWS SigV4: Schlüssel gemeinsam mit awsPairs maskieren
AWS ist der knifflige Fall. SigV4-Anfragen tragen eine HMAC-Signatur über den Request-Inhalt, berechnet aus dem Secret Key. Wenn du den Secret maskierst, aber nicht die Access Key ID, hat der Proxy keine Möglichkeit zu erkennen, welche Anfrage AWS ist — die Anfrage geht mit dem Sentinel signiert raus, AWS lehnt sie ab und du bekommst verwirrende Fehler. Maskiere die Access Key ID und den Secret immer zusammen.
Die gute Nachricht: für die konventionellen Variablennamen AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN verknüpft Claude Code sie automatisch, wenn alle drei Whole-Value-mask-Einträge sind. Der Proxy erkennt eine SigV4-Anfrage am Sentinel des Access Keys und signiert sie nach dem Ersetzen der echten Werte neu.
Wenn deine AWS-Anmeldedaten in nicht-konventionellen Variablennamen leben, gruppiere sie selbst mit awsPairs.
Nicht-Standard AWS-Variablen für SigV4-Re-Signing gruppieren
{
"sandbox": {
"credentials": {
"envVars": [
{ "name": "MY_KEY_ID", "mode": "mask" },
{ "name": "MY_SECRET_KEY", "mode": "mask" },
{ "name": "MY_SESSION_TOKEN", "mode": "mask" }
],
"awsPairs": [
{
"accessKeyIdVar": "MY_KEY_ID",
"secretAccessKeyVar": "MY_SECRET_KEY",
"sessionTokenVar": "MY_SESSION_TOKEN"
}
]
}
}
}- Jede benannte Variable muss ein
mask-Eintrag sein, der ihren gesamten Wert maskiert — keinextract, keindecode. sessionTokenVarist optional; wenn gesetzt, sendet der Proxy das echte Token alsx-amz-security-tokenbei re-signierten Anfragen.- Erfordert Claude Code v2.1.224 oder neuer.
Wenn der Proxy nicht re-signieren kann: credentials.sigv4
Drei AWS-Request-Formen tragen Signaturen, die der Proxy nicht neu berechnen kann — Chunked Payload Signing, Presigned URLs und SigV4A asymmetrische Signaturen. Standardmäßig lässt der Proxy diese fehlschlagen, statt eine kaputte Signatur weiterzuleiten. Wenn ein bestimmtes Tool auf einer davon basiert und du lieber die AWS-eigene Ablehnung als einen Proxy-Fehler sehen willst, lockere diese Form mit credentials.sigv4:
{
"sandbox": {
"credentials": {
"sigv4": {
"presignedUrl": "passthrough",
"chunkedPayload": "passthrough",
"sigv4a": "passthrough"
}
}
}
}
Wird eine Form auf passthrough gesetzt, wird die placeholder-signierte Anfrage unverändert weitergeleitet, damit das aufrufende Tool die AWS-Antwort erhält. Betrifft nur Anfragen, die mit dem Platzhalter eines maskierten Paars signiert sind — Anfragen mit unmaskierten Anmeldedaten werden nie angerührt. Ebenfalls v2.1.224+ und settings-source-beschränkt.
Datei-Masking: Anmeldedaten auf der Platte maskieren
Manche Tools speichern ihr Token in einer Config-Datei, nicht in einer Umgebungsvariablen (gh in ~/.config/gh/hosts.yml, docker in ~/.docker/config.json, ein SDK in ~/.netrc). Datei-Masking gibt dem sandboxed Prozess auf Linux und WSL2 eine Sentinel-Kopie der Datei. Auf macOS fällt Datei-Masking auf deny zurück — die Datei ist innerhalb der Sandbox nicht lesbar.
Die oauth_token-Zeile innerhalb von ~/.config/gh/hosts.yml maskieren
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com"]
},
"credentials": {
"files": [
{
"path": "~/.config/gh/hosts.yml",
"mode": "mask",
"extract": "oauth_token:\\s*(\\S+)",
"injectHosts": ["api.github.com"]
}
]
}
}
}- Das
extract-Muster hält den Rest vonhosts.ymllesbar. Ohne es ersetzt Claude Code den gesamten Dateiinhalt durch einen Sentinel — okay für eine Datei, die nur ein blankes Geheimnis enthält, aber bricht jeden Parser, der Struktur erwartet. - Für eine Datei mit einem JWT füge
decode: "jwt"hinzu (mit optionalemmaskClaims), um die Token-Form innerhalb der Sandbox dekodierbar zu halten. maskDuplicates: trueersetzt außerdem wortwörtliche Kopien des maskierten Werts, die außerhalb der gematchten Spans gefunden werden. Reserviere das für lange, hoch-entropische Geheimnisse — ein kurzer Wert würde überall ersetzt, wo er erscheint.- Liste jede Anmeldedaten-Datei einzeln auf.
maskfällt aufdenyzurück für einen Verzeichnispfad, ein Glob-Muster, eine Datei größer als 8 MiB oder eine Datei, die nicht UTF-8-Text ist.
OS-Matrix
| Feature | Linux | WSL2 | macOS |
|---|---|---|---|
Env-Var mask | Ja | Ja | Ja |
Datei-mask — Sentinel-Kopie | Ja | Ja | Nein (fällt auf deny zurück) |
extract / decode / maskClaims für Dateien | Ja | Ja | Nur wenn Dateisystem-Isolation aus ist |
Auf macOS werden mask-Datei-Einträge als deny angewendet, bevor das Muster läuft, sobald Dateisystem-Isolation aktiv ist. Um das Extract/Decode-Verhalten auf macOS zu bekommen, musst du Dateisystem-Isolation deaktivieren — was ein größerer Tradeoff ist, als die meisten Teams eingehen wollen.
Die Settings-Source-Regel (das erwischt alle)
mask-Einträge autorisieren den Sandbox-Proxy, deine echten Anmeldedaten an die von dir gelisteten Hosts zu senden. Das ist eine Delegation von Vertrauen. Claude Code erzwingt das, indem folgende Keys nur aus Settings-Scopes berücksichtigt werden, die du oder dein Administrator kontrollieren — User Settings, Managed Settings oder --settings CLI-Flag. Sie werden aus .claude/settings.json oder .claude/settings.local.json eines Repositories still ignoriert:
mode: "mask"-Einträge (Env-Vars und Dateien)network.tlsTerminatecredentials.allowPlaintextInject(erlaubt dem Proxy, in unverschlüsselte Anfragen zu injizieren)awsPairssigv4
Praktische Auswirkung. Du kannst keine .claude/settings.json in einem geteilten Repo ausliefern, die Masking für Teamkollegen aktiviert. Jeder Teamkollege muss die Mask-Einträge in seine eigenen User Settings stellen, oder ein Admin muss sie über Managed Settings pushen. Das ist by design — ein Repo, das du geklont hast, sollte nicht der Sandbox befehlen können, dein GH_TOKEN an evil.example.com zu mailen.
Häufige Fallstricke
- Kein tlsTerminate → mask schlägt still fehl. Sandbox sieht Sentinel; Sentinel geht an den Server; Auth schlägt fehl. Prüfe die Start-Warnungen.
- injectHosts muss in network.allowedDomains erscheinen, sonst sieht der Proxy die Anfrage nie, um zu ersetzen.
- AWS: nur den Secret zu maskieren (nicht die Access Key ID) bedeutet, dass der Proxy die Anfrage nicht erkennen kann. Maskiere beide zusammen oder verwende awsPairs.
- Repo-Level .claude/settings.json wird für mask/tlsTerminate/awsPairs/sigv4 IGNORIERT. Setze sie in User oder Managed Settings.
- Datei-mask auf macOS wird zu deny. Wenn deine App die Datei lesen muss, deaktiviere Dateisystem-Isolation oder laufe auf Linux/WSL2.
- extract ohne Capture-Gruppe ist ein Config-Fehler — das Muster muss Gruppe 1 enthalten.
- decode: "jwt" und extract können nicht auf demselben Eintrag kombiniert werden — wähle eins.
- Datei-mask fällt auf deny zurück bei: Verzeichnispfaden, Glob-Mustern, Dateien > 8 MiB oder Nicht-UTF-8-Dateien. Zerlege Verzeichnisse in Einträge pro Datei.
Empfohlene Konfiguration
Ein vernünftiger "Belt-and-Braces"-Ausgangspunkt für einen Entwickler-Laptop, der Claude Code Sessions gegen GitHub, npm und AWS laufen lässt:
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": [
"api.github.com",
"registry.npmjs.org",
"*.amazonaws.com"
]
},
"credentials": {
"envVars": [
{ "name": "GH_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] },
{ "name": "NPM_TOKEN", "mode": "mask", "injectHosts": ["registry.npmjs.org"] },
{ "name": "AWS_ACCESS_KEY_ID", "mode": "mask" },
{ "name": "AWS_SECRET_ACCESS_KEY", "mode": "mask" },
{ "name": "AWS_SESSION_TOKEN", "mode": "mask" },
{ "name": "ANTHROPIC_API_KEY", "mode": "deny" },
{ "name": "OPENAI_API_KEY", "mode": "deny" }
],
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
]
}
}
}
Anmerkungen zur Form:
GH_TOKENundNPM_TOKENsind maskiert und mitinjectHostsbeschränkt.- Das konventionelle AWS-Trio ist maskiert; Claude Code verknüpft sie automatisch für SigV4-Re-Signing, kein
awsPairsnötig. - LLM-API-Keys sind
deny-ed: kein sandboxed Prozess sollte sie je brauchen, und wenn du sie zugänglich lässt, könnte ein außer Kontrolle geratener Subagent dein Budget verbrennen. ~/.aws/credentialsund~/.sshsind als Verzeichnissedeny-gelistet (deshalb sind siedeny, nichtmask— Masking behandelt keine Verzeichnisse).- Gehört in deine User
settings.json, nicht ins Repo.