Mascheramento credenziali nella sandbox: token che funzionano, ma restano segreti
- Quando preferire mask a deny — e quando deny resta più sicuro
- Le quattro modalità di masking: intero valore, extract, decode: "jwt" con maskClaims, e awsPairs per SigV4
- Il JSON esatto per mascherare env var, JWT, credenziali AWS e file come ~/.config/gh/hosts.yml
- Perché tlsTerminate è obbligatorio — e il pattern di fallimento silenzioso quando manca
- La regola sulla sorgente delle settings: perché le voci mask vengono ignorate da .claude/settings.json
- Divisione Linux/WSL vs macOS: il mascheramento file su macOS degrada in deny
La maggior parte dei leak di segreti non avviene perché un token è stato rubato — avviene perché uno script ben intenzionato lo stampa in un log, in un diff, o nella trascrizione di un subagente. Il mascheramento delle credenziali nella sandbox è la soluzione integrata di Claude Code: i comandi nella sandbox vedono un valore sentinella per-sessione al posto del segreto reale, e il proxy della sandbox sostituisce il valore reale sulle richieste in uscita verso gli host che autorizzi. Il comando si autentica lo stesso. Il comando e tutto ciò che logga non contengono mai la credenziale reale.
Questa pagina è la guida operativa: le quattro modalità di masking, il JSON esatto, le insidie e la matrice OS.
Mask vs deny: quale ti serve?
Due modi per tenere una credenziale fuori dalle mani di un comando in sandbox. Sembrano simili. Non lo sono.
"mode": "deny" | "mode": "mask" | |
|---|---|---|
| Env var | Rimossa dall'ambiente della sandbox | Impostata su una sentinella per-sessione |
| File | Lettura fallisce | La sandbox vede una copia sentinella (Linux/WSL2) o la lettura fallisce (macOS) |
| Tool che ha bisogno del segreto | Si rompe (gh, npm, aws falliscono senza token) | Funziona comunque — il proxy inserisce il valore reale sul filo |
| Valore reale mai nella sandbox? | Mai | Mai (solo sentinella; il proxy tiene il valore reale) |
Richiede tlsTerminate | No | Sì — il proxy deve vedere il contenuto della richiesta per sostituire |
| Onorato dalle settings del repo? | Sì | No — solo user, managed o --settings |
Regola pratica. Usa deny quando il tool non ha bisogno della credenziale e la vuoi eliminare. Usa mask quando il tool deve autenticarsi — vuoi che gh pr view funzioni senza mai lasciare che la trascrizione, un subagente o un errante env contengano il vero GH_TOKEN.
Prerequisito: tlsTerminate
mask funziona sostituendo la sentinella con il valore reale dentro header e body delle richieste HTTP in uscita. Il proxy della sandbox deve vedere quei byte, quindi network.tlsTerminate è obbligatorio. Senza, il masking fallisce nel modo peggiore: il comando vede solo la sentinella, la sentinella raggiunge il server invariata, e l'autenticazione fallisce. Claude Code segnala questa misconfigurazione all'avvio — leggi gli warning.
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com", "registry.npmjs.org"]
}
}
}
Ogni voce injectHosts che userai dopo deve comparire anche in network.allowedDomains. Se un host non è permesso, il proxy non vede mai la richiesta per sostituire.
Mascheramento di variabili d'ambiente
Il caso base. Una voce envVars per credenziale.
Maschera GH_TOKEN e NPM_TOKEN
{
"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" }
]
}
}
}injectHostsrestringe la sostituzione a host specifici.GH_TOKENnon raggiungerà mai nulla oltreapi.github.com.- Ometti
injectHostse il valore reale viene sostituito sulle richieste verso ogni host innetwork.allowedDomains. Va bene perNPM_TOKENdove il token è già limitato al registry. - Per confermare che il mask sia attivo: chiedi a Claude di eseguire
echo "$GH_TOKEN"in un comando sandbox. L'output deve essere una sentinella per-sessione, non il token reale.
Extract: maschera un solo campo dentro un valore strutturato
Molte "credenziali" non sono un segreto puro — sono una connection string con la password sepolta dentro. extract maschera solo il capture group della tua regex, lasciando leggibile il resto così i parser continuano a funzionare.
Maschera la password dentro DATABASE_URL, mantieni il resto parsabile
{
"name": "DATABASE_URL",
"mode": "mask",
"extract": "://[^:]+:([^@]+)@"
}- Il pattern deve contenere almeno un capture group; viene sostituito solo il testo del gruppo 1.
onExtractNoMatchcontrolla cosa succede quando il pattern non matcha nulla:warn(default — passa non mascherato con warning),deny(fail closed), oerror(fa fallire la sandbox). Usadenyquando il segreto deve essere sempre presente.
Mascheramento JWT con decode e maskClaims
Per access token a forma di JWT (header.payload.signature), il masking del valore intero rompe qualsiasi codice dentro la sandbox che decodifica il token per guardare i claim. decode: "jwt" risolve: Claude Code verifica che il valore sia un JWT valido e ci sostituisce un token fake strutturalmente valido, così jwt.decode(...) dentro la sandbox restituisce comunque un payload ben formato.
Maschera un JWT di sessione mantenendone la forma decodificabile
{
"name": "SESSION_JWT",
"mode": "mask",
"decode": "jwt",
"maskClaims": ["sub", "email"]
}- Senza
maskClaims, l'intero token fake sostituisce quello reale — al codice che legge soloissoaudnon interessa, ma quello che leggesubottiene un valore fake. - Con
maskClaims, gli altri claim restano leggibili; solo quelli che elenchi vengono sostituiti individualmente. Utile quando l'app ha bisogno diiat/exp/issper il routing ma non deve mai vederesub/email. decodenon può essere combinato conextractsulla stessa voce. Scegli.- Se il valore non si verifica come JWT (o nessun claim elencato matcha), Claude Code lo lascia passare non mascherato con un warning. Usa
onExtractNoMatch: "deny"per fail closed.
Richiede Claude Code v2.1.224 o successivo.
AWS SigV4: maschera le chiavi insieme con awsPairs
AWS è il caso complicato. Le richieste SigV4 portano una firma HMAC sul contenuto della richiesta, calcolata dalla chiave secret. Se maschera il secret ma non l'access key ID, il proxy non ha modo di riconoscere quale richiesta sia AWS — la richiesta esce firmata con la sentinella, AWS la rifiuta, e ottieni fallimenti confusi. Maschera sempre l'access key ID e il secret insieme.
Buona notizia: per i nomi convenzionali AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN, Claude Code li collega automaticamente quando tutti e tre sono voci mask a valore intero. Il proxy riconosce una richiesta SigV4 dalla sentinella dell'access key e la ri-firma dopo aver sostituito i valori reali.
Se le tue credenziali AWS vivono in nomi di variabile non convenzionali, raggruppale a mano con awsPairs.
Raggruppa variabili AWS non standard per la ri-firma SigV4
{
"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"
}
]
}
}
}- Ogni variabile nominata deve essere una voce
maskche maschera l'intero valore — noextract, nodecode. sessionTokenVarè opzionale; quando impostato, il proxy invia il token reale comex-amz-security-tokensulle richieste ri-firmate.- Richiede Claude Code v2.1.224 o successivo.
Quando il proxy non può ri-firmare: credentials.sigv4
Tre forme di richiesta AWS portano firme che il proxy non può ricalcolare — chunked payload signing, presigned URL e firme asimmetriche SigV4A. Di default il proxy fa fallire queste richieste piuttosto che inoltrare una firma rotta. Se uno specifico tool si basa su una di esse e preferisci vedere il rifiuto di AWS piuttosto che un errore del proxy, rilassa quella forma con credentials.sigv4:
{
"sandbox": {
"credentials": {
"sigv4": {
"presignedUrl": "passthrough",
"chunkedPayload": "passthrough",
"sigv4a": "passthrough"
}
}
}
}
Impostare una forma su passthrough inoltra invariata la richiesta firmata col placeholder così il tool chiamante riceve la risposta di AWS. Riguarda solo le richieste firmate col placeholder di una coppia mascherata — le richieste firmate con credenziali non mascherate non vengono mai toccate. Anche questo v2.1.224+ e ristretto per sorgente settings.
Mascheramento file: mascherare una credenziale su disco
Alcuni tool tengono il token in un file di config, non in una env var (gh in ~/.config/gh/hosts.yml, docker in ~/.docker/config.json, un SDK in ~/.netrc). Il mascheramento file dà al processo in sandbox una copia sentinella del file su Linux e WSL2. Su macOS il file masking degrada a deny — il file è illeggibile dentro la sandbox.
Maschera la riga oauth_token dentro ~/.config/gh/hosts.yml
{
"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"]
}
]
}
}
}- Il pattern
extractè ciò che mantiene leggibile il resto dihosts.yml. Senza, Claude Code sostituisce l'intero contenuto del file con una sentinella — va bene per un file che contiene solo un segreto e nient'altro, ma rompe qualunque parser che si aspetti struttura. - Per un file che contiene un JWT, aggiungi
decode: "jwt"(conmaskClaimsopzionale) per mantenere la forma del token decodificabile dentro la sandbox. maskDuplicates: truesostituisce anche copie letterali del valore mascherato trovate fuori dagli span matchati. Riservalo a segreti lunghi ad alta entropia — un valore corto verrebbe sostituito ovunque appaia.- Elenca ogni file di credenziali individualmente.
maskdegrada adenyper un path di directory, un glob pattern, un file più grande di 8 MiB, o un file non UTF-8.
Matrice OS
| Feature | Linux | WSL2 | macOS |
|---|---|---|---|
Env var mask | Sì | Sì | Sì |
File mask — copia sentinella | Sì | Sì | No (degrada a deny) |
extract / decode / maskClaims per file | Sì | Sì | Solo quando l'isolamento del filesystem è disattivato |
Su macOS, le voci mask sui file vengono applicate come deny prima che il pattern venga eseguito, ogni volta che l'isolamento del filesystem è attivo. Per ottenere il comportamento extract/decode su macOS devi disattivare l'isolamento del filesystem — un tradeoff più grande di quanto la maggior parte dei team voglia accettare.
La regola della sorgente delle settings (questa frega tutti)
Le voci mask autorizzano il proxy della sandbox a inviare la tua credenziale reale agli host che elenchi. È una delega di fiducia. Claude Code la impone onorando le seguenti chiavi solo dalle scope di settings che tu o il tuo amministratore controllate — user settings, managed settings, o flag CLI --settings. Vengono silenziosamente ignorate dal .claude/settings.json o .claude/settings.local.json di un repository:
- voci
mode: "mask"(env var e file) network.tlsTerminatecredentials.allowPlaintextInject(permette al proxy di iniettare in richieste non cifrate)awsPairssigv4
Impatto pratico. Non puoi spedire un .claude/settings.json in un repo condiviso che attivi il masking per i compagni di squadra. Ognuno deve mettere le voci mask nelle proprie user settings, oppure un admin deve spingerle via managed settings. È voluto — un repo che hai clonato non dovrebbe poter comandare la sandbox di spedire il tuo GH_TOKEN a evil.example.com.
Insidie comuni
- Nessun tlsTerminate → mask fallisce silenziosamente. La sandbox vede la sentinella; la sentinella va al server; l'auth fallisce. Controlla gli warning di startup.
- injectHosts deve comparire in network.allowedDomains, altrimenti il proxy non vede mai la richiesta per sostituire.
- AWS: mascherare solo il secret (non l'access key ID) fa sì che il proxy non riconosca la richiesta. Mascherali insieme, o usa awsPairs.
- Il .claude/settings.json a livello repo è IGNORATO per mask/tlsTerminate/awsPairs/sigv4. Mettili in user o managed settings.
- File mask su macOS diventa deny. Se la tua app deve leggere il file, o disattivi l'isolamento filesystem o lavori su Linux/WSL2.
- extract senza capture group è un errore di config — il pattern deve contenere il gruppo 1.
- decode: "jwt" ed extract non possono combinarsi sulla stessa voce — scegli.
- File mask degrada a deny per: path di directory, glob pattern, file > 8 MiB, o file non-UTF-8. Spezza le directory in voci per-file.
Configurazione raccomandata
Un ragionevole punto di partenza "cintura e bretelle" per un laptop di sviluppo che esegue sessioni Claude Code contro GitHub, npm e AWS:
{
"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" }
]
}
}
}
Note sulla forma:
GH_TOKENeNPM_TOKENsono mascherati e limitati coninjectHosts.- Il trio AWS convenzionale è mascherato; Claude Code li collega automaticamente per la ri-firma SigV4, nessun
awsPairsserve. - Le API key degli LLM sono
deny— nessun processo in sandbox dovrebbe averne bisogno, e se le lasciassi accessibili un subagente fuori controllo potrebbe bruciarti il budget. ~/.aws/credentialse~/.sshsonodeny-listati come directory (per questo sonodeny, nonmask— il masking non gestisce directory).- Va nel tuo
settings.jsonuser, non nel repo.