Aller au contenu principal

Masquage des credentials du sandbox : garder les tokens fonctionnels, les garder secrets

Avancé
What you'll learn
  • Quand préférer mask à deny — et quand deny est encore plus sûr
  • Les quatre modes de masquage : whole-value, extract, decode: "jwt" avec maskClaims, et awsPairs pour SigV4
  • Le JSON exact pour masquer variables d'env, JWTs, credentials AWS, et fichiers comme ~/.config/gh/hosts.yml
  • Pourquoi tlsTerminate est obligatoire — et le pattern de silent-fail quand il manque
  • La règle de source de settings : pourquoi les entrées mask sont ignorées depuis .claude/settings.json
  • Le clivage Linux/WSL vs macOS : le masquage de fichier se dégrade en deny sur macOS

La plupart des fuites de secrets n'arrivent pas parce qu'un token a été volé — elles arrivent parce qu'un script bien intentionné a imprimé un secret dans un log, un diff, ou la transcription d'un sous-agent. Le masquage des credentials du sandbox est le fix intégré de Claude Code : les commandes sandboxées voient une valeur sentinel par session au lieu du vrai secret, et le proxy du sandbox substitue la vraie valeur sur les requêtes sortantes vers les hôtes que vous autorisez. La commande s'authentifie quand même. La commande et tout ce qu'elle logue ne détiennent jamais le vrai credential.

Cette page est le guide du praticien : les quatre modes de masquage, le JSON exact, les pièges, et la matrice OS.

Mask vs deny : lequel voulez-vous ?

Deux façons de garder un credential hors de portée d'une commande sandboxée. Elles se ressemblent. Elles ne sont pas identiques.

"mode": "deny""mode": "mask"
Variable d'envRetirée de l'environnement du sandboxRéglée à un sentinel par session
FichierLecture échoueLe sandbox voit une copie sentinel (Linux/WSL2) ou la lecture échoue (macOS)
Outil qui a besoin du secretCasse (gh, npm, aws échouent sans token)Fonctionne encore — le proxy substitue la vraie valeur sur le fil
Vraie valeur jamais dans le sandbox ?JamaisJamais (sentinel uniquement ; le proxy détient la vraie valeur)
Nécessite tlsTerminateNonOui — le proxy doit voir le contenu des requêtes pour substituer
Honoré depuis les settings du repo ?OuiNon — user, managed, ou --settings uniquement

Règle du pouce. Utilisez deny quand l'outil n'a pas besoin du credential et que vous le voulez parti. Utilisez mask quand l'outil a besoin de s'authentifier — vous voulez que gh pr view fonctionne sans laisser la transcription, un sous-agent, ou un env dump errant détenir jamais le vrai GH_TOKEN.

Prérequis : tlsTerminate

mask fonctionne en substituant le sentinel avec la vraie valeur à l'intérieur des en-têtes et corps de requêtes HTTP sortantes. Le proxy du sandbox doit voir ces octets, donc network.tlsTerminate est obligatoire. Sans lui, le masquage échoue de la pire façon : la commande voit encore uniquement le sentinel, le sentinel atteint le serveur inchangé, et l'authentification échoue. Claude Code signale cette mauvaise configuration au démarrage — lisez les avertissements.

{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com", "registry.npmjs.org"]
}
}
}

Chaque entrée injectHosts que vous utilisez plus tard doit aussi apparaître dans network.allowedDomains. Si un hôte n'est pas autorisé, le proxy ne voit jamais la requête pour substituer.

Masquage de variables d'environnement

Le cas de base. Une entrée envVars par credential.

Masquer GH_TOKEN et 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" }
      ]
    }
  }
}
  • injectHosts scope la substitution à des hôtes spécifiques. GH_TOKEN n'atteindra jamais autre chose que api.github.com.
  • Omettez injectHosts et la vraie valeur est substituée sur les requêtes vers chaque hôte dans network.allowedDomains. Bien pour NPM_TOKEN où le token est scopé au registry.
  • Pour confirmer que le masque est live : demandez à Claude d'exécuter echo "$GH_TOKEN" dans une commande sandboxée. La sortie devrait être un sentinel par session, pas le vrai token.

Extract : masquer un champ dans une valeur structurée

Beaucoup de « credentials » ne sont pas un secret nu — c'est une chaîne de connexion avec un mot de passe enfoui dedans. extract masque uniquement le groupe de capture de votre regex, laissant le reste lisible pour que les parseurs continuent de fonctionner.

Masquer le mot de passe dans DATABASE_URL, garder le reste parseable

{
  "name": "DATABASE_URL",
  "mode": "mask",
  "extract": "://[^:]+:([^@]+)@"
}
  • Le pattern doit contenir au moins un groupe de capture ; seul le texte du groupe 1 est remplacé.
  • onExtractNoMatch contrôle ce qui se passe quand le pattern ne matche rien : warn (défaut — passe non masqué avec avertissement), deny (fail closed), ou error (fait échouer le sandbox). Utilisez deny quand le secret devrait toujours être présent.

Masquage JWT avec decode et maskClaims

Pour les access tokens en forme de JWT (header.payload.signature), le masquage whole-value casse tout code dans le sandbox qui décode le token pour regarder les claims. decode: "jwt" corrige : Claude Code vérifie que la valeur est un JWT valide et substitue un token faux mais structurellement valide, pour que jwt.decode(...) dans le sandbox retourne encore un payload bien formé.

Masquer un JWT de session mais garder la forme décodable

{
  "name": "SESSION_JWT",
  "mode": "mask",
  "decode": "jwt",
  "maskClaims": ["sub", "email"]
}
  • Sans maskClaims, le faux token entier remplace le vrai — le code qui n'a besoin que de iss ou aud s'en fiche, mais le code qui lit sub obtient une fausse valeur.
  • Avec maskClaims, les autres claims restent lisibles ; seuls ceux que vous listez sont remplacés individuellement. Utile quand l'app a besoin de iat/exp/iss pour le routage mais ne doit jamais voir sub/email.
  • decode ne peut pas être combiné avec extract sur la même entrée. Choisissez.
  • Si la valeur ne se vérifie pas comme JWT (ou aucun claim listé ne matche), Claude Code la passe non masquée avec avertissement. Utilisez onExtractNoMatch: "deny" pour fail closed.

Nécessite Claude Code v2.1.224 ou ultérieur.

AWS SigV4 : masquer les clés ensemble avec awsPairs

AWS est le cas délicat. Les requêtes SigV4 portent une signature HMAC sur le contenu de la requête, calculée depuis la clé secret. Si vous masquez le secret mais pas l'access key ID, le proxy n'a aucun moyen de détecter quelle requête est AWS — la requête sort signée avec le sentinel, AWS rejette, et vous obtenez des échecs confus. Toujours masquer l'access key ID et le secret ensemble.

La bonne nouvelle : pour les noms de variables conventionnels AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN, Claude Code les lie automatiquement quand tous trois sont des entrées mask whole-value. Le proxy détecte une requête SigV4 par le sentinel de l'access key et la re-signe après avoir substitué les vraies valeurs.

Si vos credentials AWS vivent dans des noms de variables non conventionnels, groupez-les vous-même avec awsPairs.

Grouper des variables AWS non standard pour la re-signature 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"
        }
      ]
    }
  }
}
  • Chaque variable nommée doit être une entrée mask qui masque sa valeur entière — pas de extract, pas de decode.
  • sessionTokenVar est optionnel ; quand défini, le proxy envoie le vrai token comme x-amz-security-token sur les requêtes re-signées.
  • Nécessite Claude Code v2.1.224 ou ultérieur.

Quand le proxy ne peut pas re-signer : credentials.sigv4

Trois formes de requête AWS portent des signatures que le proxy ne peut pas recalculer — chunked payload signing, URLs présignées, et signatures asymétriques SigV4A. Par défaut le proxy échoue plutôt que de forwarder une signature cassée. Si un outil spécifique dépend d'une d'elles et que vous préférez voir le rejet d'AWS plutôt qu'une erreur de proxy, relâchez cette forme avec credentials.sigv4 :

{
"sandbox": {
"credentials": {
"sigv4": {
"presignedUrl": "passthrough",
"chunkedPayload": "passthrough",
"sigv4a": "passthrough"
}
}
}
}

Régler une forme à passthrough forwarde la requête signée par placeholder inchangée pour que l'outil appelant reçoive la réponse d'AWS. N'affecte que les requêtes signées avec le placeholder d'une paire masquée — les requêtes signées avec des credentials non masqués ne sont jamais touchées. Aussi v2.1.224+ et restreint par source de settings.

Masquage de fichier : masquer un credential sur disque

Certains outils stockent leur token dans un fichier de config, pas une variable d'env (gh dans ~/.config/gh/hosts.yml, docker dans ~/.docker/config.json, un SDK dans ~/.netrc). Le masquage de fichier donne au processus sandboxé une copie sentinel du fichier sur Linux et WSL2. Sur macOS, le masquage de fichier retombe sur deny — le fichier est illisible dans le sandbox.

Masquer la ligne oauth_token dans ~/.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"]
        }
      ]
    }
  }
}
  • Le pattern extract est ce qui garde le reste de hosts.yml lisible. Sans, Claude Code remplace le contenu entier du fichier par un sentinel — bien pour un fichier qui ne détient qu'un secret nu, mais casse tout parseur attendant de la structure.
  • Pour un fichier détenant un JWT, ajoutez decode: "jwt" (avec maskClaims optionnel) pour garder la forme du token décodable dans le sandbox.
  • maskDuplicates: true remplace aussi les copies verbatim de la valeur masquée trouvées en dehors des spans matchés. Réservez aux secrets longs à haute entropie — une valeur courte serait remplacée partout où elle apparaît.
  • Listez chaque fichier de credential individuellement. mask retombe sur deny pour un chemin de répertoire, un pattern glob, un fichier de plus de 8 MiB, ou un fichier qui n'est pas UTF-8 texte.

Matrice OS

FonctionnalitéLinuxWSL2macOS
Var d'env maskOuiOuiOui
Fichier mask — copie sentinelOuiOuiNon (retombe sur deny)
extract / decode / maskClaims pour fichiersOuiOuiSeulement quand l'isolation filesystem est off

Sur macOS, les entrées fichier mask sont appliquées comme deny avant que le pattern ne tourne chaque fois que l'isolation filesystem est on. Pour obtenir le comportement extract/decode sur macOS, vous devez désactiver l'isolation filesystem — ce qui est un plus gros compromis que la plupart des équipes veulent faire.

La règle de source de settings (celle qui piège tout le monde)

Les entrées mask autorisent le proxy du sandbox à envoyer votre credential réel aux hôtes que vous listez. C'est une délégation de confiance. Claude Code applique cela en n'honorant les clés suivantes que depuis des scopes de settings que vous ou votre administrateur contrôlez — settings user, settings managed, ou flag CLI --settings. Elles sont silencieusement ignorées depuis .claude/settings.json ou .claude/settings.local.json d'un repo :

  • Entrées mode: "mask" (variables d'env et fichiers)
  • network.tlsTerminate
  • credentials.allowPlaintextInject (laisse le proxy injecter dans les requêtes non chiffrées)
  • awsPairs
  • sigv4

Impact pratique. Vous ne pouvez pas livrer un .claude/settings.json dans un repo partagé qui active le masquage pour les coéquipiers. Chaque coéquipier doit mettre les entrées mask dans ses propres settings user, ou un admin doit les pousser via les settings managed. C'est par conception — un repo que vous avez cloné ne devrait pas pouvoir commander au sandbox d'envoyer par email votre GH_TOKEN à evil.example.com.

Pièges courants

Watch out
  • Pas de tlsTerminate → mask échoue silencieusement. Le sandbox voit le sentinel ; le sentinel va au serveur ; l'auth échoue. Vérifiez les avertissements au démarrage.
  • injectHosts doit apparaître dans network.allowedDomains, sinon le proxy ne voit jamais la requête pour substituer.
  • AWS : masquer seulement le secret (pas l'access key ID) signifie que le proxy ne peut pas détecter la requête. Masquez les deux ensemble, ou utilisez awsPairs.
  • Le .claude/settings.json au niveau repo est IGNORÉ pour mask/tlsTerminate/awsPairs/sigv4. Mettez-les dans les settings user ou managed.
  • Le mask de fichier sur macOS devient deny. Si votre app a besoin de lire le fichier, soit désactivez l'isolation filesystem, soit tournez sur Linux/WSL2.
  • extract sans groupe de capture est une erreur de config — le pattern doit contenir le groupe 1.
  • decode: "jwt" et extract ne peuvent pas être combinés sur la même entrée — choisissez.
  • Le mask de fichier retombe sur deny pour : chemins de répertoires, patterns glob, fichiers > 8 MiB, ou fichiers non-UTF-8. Cassez les répertoires en entrées par fichier.

Configuration recommandée

Un point de départ raisonnable « ceinture et bretelles » pour un portable dev qui exécute des sessions Claude Code contre GitHub, npm et 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" }
]
}
}
}

Notes sur la forme :

  • GH_TOKEN et NPM_TOKEN sont masqués et scopés avec injectHosts.
  • Le trio AWS conventionnel est masqué ; Claude Code les lie auto pour la re-signature SigV4, pas de awsPairs nécessaire.
  • Les clés API LLM sont deny-ées : aucun processus sandboxé ne devrait jamais en avoir besoin, et si vous les laissiez accessibles un sous-agent qui déraille pourrait cramer votre budget.
  • ~/.aws/credentials et ~/.ssh sont deny-listés comme répertoires (c'est pourquoi ils sont deny, pas mask — le masquage ne gère pas les répertoires).
  • Va dans votre settings.json user, pas le repo.

Check yourself

0/3
  1. Votre équipe livre un .claude/settings.json dans le repo avec des entrées mask pour GH_TOKEN. Les coéquipiers clonent et lancent Claude Code. Que se passe-t-il ?
  2. Vous masquez AWS_SECRET_ACCESS_KEY mais pas AWS_ACCESS_KEY_ID. Qu'est-ce qui casse ?
  3. Vous ajoutez une entrée mask mais oubliez network.tlsTerminate. Que se passe-t-il vraiment au runtime ?

Suite