サンドボックスの認証情報マスキング: トークンを動かしつつ、秘密のままに保つ
- mask を deny より優先すべき場面 — そして deny のほうがより安全な場面
- 4 つのマスキングモード: whole-value、extract、decode: "jwt" と maskClaims、そして SigV4 用の awsPairs
- env 変数、JWT、AWS 認証情報、そして ~/.config/gh/hosts.yml のようなファイルをマスクするための正確な JSON
- なぜ tlsTerminate が必須なのか — そしてそれが欠けているときのサイレントに失敗するパターン
- 設定ソースのルール: なぜ .claude/settings.json からの mask エントリは無視されるのか
- Linux/WSL と macOS の分岐: ファイルマスキングは macOS では deny に降格する
シークレットの漏洩は、ほとんどの場合、トークンが盗まれたから起きるのではありません — 善意で書かれたスクリプトがログや diff、サブエージェントのトランスクリプトに 印字 してしまったから起きます。サンドボックスの認証情報マスキング は Claude Code に組み込まれた解決策です: サンドボックス化されたコマンドは本物のシークレットの代わりにセッションごとの セントリネル 値を見て、サンドボックスのプロキシがあなたが許可したホストへの送信リクエストで本物の値を差し替えます。コマンドはそのまま認証できます。コマンドとそれがログに書き出すものが、本物の認証情報を保持することは決してありません。
このページは実践者向けのガイドです: 4 つのマスキングモード、正確な JSON、落とし穴、そして OS マトリクスをまとめています。
mask と deny: どちらを選ぶべきか?
サンドボックス化されたコマンドの手から認証情報を遠ざける方法は 2 つあります。似ていますが、同じではありません。
"mode": "deny" | "mode": "mask" | |
|---|---|---|
| Env 変数 | サンドボックス環境から削除 | セッションごとのセントリネルにセット |
| ファイル | 読み込み失敗 | サンドボックスはセントリネルのコピーを見る (Linux/WSL2) か、読み込み失敗 (macOS) |
| シークレットを必要とするツール | 壊れる (トークンなしでは gh、npm、aws が失敗) | 動作する — プロキシが送信時に本物の値を差し替える |
| 本物の値がサンドボックスに存在する? | 決してない | 決してない (セントリネルのみ; プロキシが本物の値を保持) |
tlsTerminate が必要? | 不要 | 必要 — プロキシがリクエストの内容を見て差し替える必要がある |
| リポジトリ設定から尊重される? | はい | いいえ — ユーザー、マネージド、または --settings のみ |
目安。 ツールが認証情報を必要とせず、それを消し去りたい場合は deny を使います。ツールが認証する必要があり — gh pr view を動かしつつ、トランスクリプトやサブエージェント、うっかりの env ダンプが本物の GH_TOKEN を保持することを絶対に避けたい場合は mask を使います。
前提条件: tlsTerminate
mask は送信 HTTP リクエストのヘッダとボディの内部で、セントリネルを本物の値に差し替えることで機能します。サンドボックスのプロキシはそれらのバイトを見る必要があるので、network.tlsTerminate は必須 です。これがないとマスキングは最悪の形で失敗します: コマンドは依然としてセントリネルしか見えず、セントリネルは変更されないままサーバーに届き、認証は失敗します。Claude Code は起動時にこの設定ミスを報告します — 警告をよく読んでください。
{
"sandbox": {
"network": {
"tlsTerminate": {},
"allowedDomains": ["api.github.com", "registry.npmjs.org"]
}
}
}
後で使うすべての injectHosts エントリは、network.allowedDomains にも 必ず 表示される必要があります。ホストが許可されていない場合、プロキシは差し替えるためにリクエストを見ることが決してありません。
環境変数のマスキング
基本ケース。認証情報ごとに envVars エントリを 1 つ。
GH_TOKEN と 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は差し替えを特定のホストにスコープ制限します。GH_TOKENはapi.github.com以外の場所には届きません。injectHostsを省略すると、network.allowedDomainsに含まれる すべての ホストへのリクエストで本物の値が差し替えられます。トークンがレジストリにスコープされているNPM_TOKENには問題ありません。- マスクが有効かどうかを確認する: Claude にサンドボックス化されたコマンドで
echo "$GH_TOKEN"を実行してもらいます。出力は本物のトークンではなく、セッションごとのセントリネルであるべきです。
Extract: 構造化された値の中で 1 つのフィールドをマスクする
多くの「認証情報」は素のシークレットではなく — パスワードが埋め込まれた接続文字列です。extract は正規表現のキャプチャグループだけをマスクし、残りは読み取り可能なままにしてパーサーが動作し続けるようにします。
DATABASE_URL 内のパスワードだけをマスクし、残りはパース可能にする
{
"name": "DATABASE_URL",
"mode": "mask",
"extract": "://[^:]+:([^@]+)@"
}- パターンには 必ず 少なくとも 1 つのキャプチャグループが含まれる必要があります; グループ 1 のテキストだけが置換されます。
onExtractNoMatchは、パターンが何にもマッチしなかったときの挙動を制御します:warn(デフォルト — 警告を出しつつマスクせずに通す)、deny(フェイルクローズ)、error(サンドボックスを失敗させる)。シークレットが常に存在すべき場合はdenyを使いましょう。
decode と maskClaims を使った JWT のマスキング
JWT の形をしたアクセストークン (header.payload.signature) では、値全体のマスキングを行うと、サンドボックス内でクレームを見るためにトークンをデコードするコードが壊れてしまいます。decode: "jwt" がこれを修正します: Claude Code は値が有効な JWT であることを検証し、構造的に有効な偽のトークン に差し替えるので、サンドボックス内の jwt.decode(...) は依然として整った形のペイロードを返します。
セッション JWT をマスクしつつ、形をデコード可能に保つ
{
"name": "SESSION_JWT",
"mode": "mask",
"decode": "jwt",
"maskClaims": ["sub", "email"]
}maskClaimsを指定しないと、偽のトークン全体が本物と置き換わります —issやaudしか必要としないコードは気にしませんが、subを読むコードは偽の値を受け取ります。maskClaimsを指定すると、その 他の クレームは読み取り可能なまま残り、リストに指定したものだけが個別に置換されます。ルーティングのためにiat/exp/issは必要だがsub/emailは絶対に見せてはならない、というアプリに便利です。- 同一エントリで
decodeをextractと組み合わせることは できません。どちらか 1 つを選んでください。 - 値が JWT として検証されない (あるいは指定されたクレームが 1 つもマッチしない) 場合、Claude Code は警告を出しつつマスクせずに通します。フェイルクローズにするには
onExtractNoMatch: "deny"を使ってください。
Claude Code v2.1.224 以降が必要です。
AWS SigV4: awsPairs でキーをまとめてマスク
AWS は厄介なケースです。SigV4 リクエストはリクエストの内容に対する HMAC 署名を運び、その署名は シークレット キーから計算されます。シークレットだけをマスクしてアクセスキー ID をマスクしないと、プロキシはどのリクエストが AWS のものか検出する方法がありません — リクエストはセントリネルで署名されて送出され、AWS はそれを拒否し、混乱を招く失敗を得ることになります。アクセスキー ID とシークレットは常に一緒にマスクしてください。
朗報: 慣例的な変数名 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN については、それら 3 つがすべて whole-value の mask エントリの場合、Claude Code が自動的にリンクします。プロキシはアクセスキーのセントリネルによって SigV4 リクエストを検出し、本物の値に差し替えた後で再署名します。
AWS 認証情報が慣例外の変数名にある場合は、awsPairs で自分でグループ化してください。
非標準の AWS 変数を 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"
}
]
}
}
}- 名前の付いた変数はそれぞれ、値全体 をマスクする
maskエントリでなければなりません —extractやdecodeは不可。 sessionTokenVarはオプション; 設定されている場合、プロキシは再署名されたリクエストで本物のトークンをx-amz-security-tokenとして送ります。- Claude Code v2.1.224 以降が必要です。
プロキシが再署名できない場合: credentials.sigv4
3 種類の AWS リクエスト形式は、プロキシが再計算できない署名を運びます — チャンク付きペイロード署名、事前署名 URL、そして SigV4A の非対称署名です。デフォルトではプロキシは壊れた署名を転送するよりも失敗させます。特定のツールがこれらのどれかに依存していて、プロキシエラーよりも AWS 自身の拒否を見たい場合は、credentials.sigv4 でその形式を緩めることができます:
{
"sandbox": {
"credentials": {
"sigv4": {
"presignedUrl": "passthrough",
"chunkedPayload": "passthrough",
"sigv4a": "passthrough"
}
}
}
}
ある形式を passthrough に設定すると、プレースホルダで署名されたリクエストを変更せずに転送するので、呼び出し側のツールは AWS のレスポンスを受け取ります。マスクされたペアのプレースホルダで署名されたリクエストにのみ影響します — マスクされていない認証情報で署名されたリクエストには決して手を加えません。これも v2.1.224 以降、そして設定ソース制限があります。
ファイルマスキング: ディスク上の認証情報をマスクする
一部のツールはトークンを env 変数ではなく設定ファイルに保存します (gh は ~/.config/gh/hosts.yml、docker は ~/.docker/config.json、SDK は ~/.netrc など)。ファイルマスキングは Linux と WSL2 でサンドボックス化されたプロセスにファイルの セントリネルコピー を提供します。macOS ではファイルマスキングは deny にフォールバックします — サンドボックス内ではファイルが読めなくなります。
~/.config/gh/hosts.yml 内の oauth_token 行をマスク
{
"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"]
}
]
}
}
}extractパターンはhosts.ymlの残りを読み取り可能に保つためのものです。これがないと、Claude Code は ファイル全体 の内容を 1 つのセントリネルで置き換えます — 素のシークレットだけを持つファイルには問題ありませんが、構造を期待するパーサーは壊れます。- JWT を保持するファイルには
decode: "jwt"(オプションでmaskClaimsを含めて) を追加し、サンドボックス内でトークンの形をデコード可能に保ちます。 maskDuplicates: trueは、マッチしたスパンの外側にあるマスクされた値の逐語的なコピーも置き換えます。長くて高エントロピーのシークレットに限定してください — 短い値はどこに現れても置換されてしまいます。- 認証情報ファイルは 個別に リストしてください。
maskはディレクトリパス、glob パターン、8 MiB を超えるファイル、UTF-8 テキストではないファイルではdenyにフォールバックします。
OS マトリクス
| 機能 | Linux | WSL2 | macOS |
|---|---|---|---|
Env 変数 mask | はい | はい | はい |
ファイル mask — セントリネルコピー | はい | はい | いいえ (deny にフォールバック) |
ファイル用の extract / decode / maskClaims | はい | はい | ファイルシステム隔離がオフのときのみ |
macOS では、ファイルシステム隔離がオンの場合、mask のファイルエントリはパターンが実行される前に deny として適用されます。macOS で extract/decode の挙動を得るには、ファイルシステム隔離を無効化する必要があります — これはほとんどのチームが行いたいと思うよりも大きなトレードオフです。
設定ソースのルール (これは誰もが引っかかる)
mask エントリは、サンドボックスのプロキシに 本物の 認証情報をあなたが指定したホストに送ることを認可します。それは信頼の委任です。Claude Code は、あなたまたはあなたの管理者が制御する設定スコープからのみ、以下のキーを尊重することでこれを強制します — ユーザー設定、マネージド設定、または --settings CLI フラグ。リポジトリの .claude/settings.json や .claude/settings.local.json からは、これらは静かに 無視 されます:
mode: "mask"エントリ (env 変数とファイル)network.tlsTerminatecredentials.allowPlaintextInject(プロキシが暗号化されていないリクエストに注入することを許可)awsPairssigv4
実際的な影響。 チームメイト向けにマスキングを有効にする .claude/settings.json を共有リポジトリで配布することはできません。各チームメイトが自分のユーザー設定にマスクエントリを入れるか、管理者がマネージド設定経由で配布する必要があります。これは意図された設計です — クローンしたリポジトリが、サンドボックスに GH_TOKEN を evil.example.com にメールするよう命令できてはならないからです。
よくある落とし穴
- tlsTerminate なし → マスクはサイレントに失敗。サンドボックスはセントリネルを見て、セントリネルがサーバーに届き、認証は失敗。起動時の警告を確認すること。
- injectHosts は network.allowedDomains に含める必要がある。さもないとプロキシは差し替えるためにリクエストを見ることがない。
- AWS: シークレットだけをマスクし (アクセスキー ID をマスクしない) と、プロキシはリクエストを検出できない。両方一緒にマスクするか、awsPairs を使うこと。
- リポジトリレベルの .claude/settings.json は mask/tlsTerminate/awsPairs/sigv4 では無視される。ユーザーまたはマネージド設定に置くこと。
- macOS ではファイル mask は deny になる。アプリがファイルを読む必要があるなら、ファイルシステム隔離を無効化するか Linux/WSL2 で実行すること。
- キャプチャグループなしの extract は設定エラー — パターンにはグループ 1 が含まれている必要がある。
- decode: "jwt" と extract は同一エントリで組み合わせることができない — どちらか 1 つを選ぶこと。
- ファイル mask は次の場合に deny にフォールバック: ディレクトリパス、glob パターン、8 MiB を超えるファイル、UTF-8 でないファイル。ディレクトリはファイルごとのエントリに分解すること。
推奨される設定
GitHub、npm、AWS に対して Claude Code セッションを実行する開発者ラップトップ向けの、妥当な「念のため二重の備え」の出発点:
{
"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" }
]
}
}
}
形についての注釈:
GH_TOKENとNPM_TOKENはマスクされ、injectHostsでスコープ制限されています。- 慣例的な AWS の三兄弟はマスクされています; Claude Code が SigV4 の再署名のために自動的にリンクするので、
awsPairsは不要です。 - LLM の API キーは
denyにされています: サンドボックスプロセスはこれを必要とすべきではなく、アクセス可能なままにしておくと暴走したサブエージェントが予算を焼き尽くす可能性があります。 ~/.aws/credentialsと~/.sshはディレクトリとしてdenyリストに入っています (これがなぜmaskではなくdenyである理由です — マスキングはディレクトリを扱えません)。- リポジトリではなく、あなたの ユーザー の
settings.jsonに置くべきものです。