Claude Codeサンドボックスに来た「マスク」機能、実際に秘密鍵を隠せるか試した
2026年8月4日追加のsandbox.credentials mask機能を検証。環境変数がコマンドには偽の値、外部リクエストには本物の値として届く仕組みをhttpbin.orgへの実リクエストで確認した。tlsTerminate必須の理由とmacOS/Linuxの挙動差も解説する。
エンジニアのゆとです。
フリーランスで他社のコードベースを触ってると、サンドボックスまわりでずっと小さいジレンマを抱えてた。Claude Codeのサンドボックスを有効にすると、ghやnpmが使うトークンをsandbox.credentialsでdenyに入れて隠せる。でもdenyは環境変数ごと消す設定だから、隠した瞬間にgh pr createやnpm publishが普通に壊れる。隠すか、動かすか。今まではその二択だった。
8月4日リリースのv2.1.221で、この二択に3つ目の選択肢が増えた。"mode": "mask"。コマンド自身には偽の値を見せて、外部への通信だけ本物の値に差し替える仕組みだ。
言葉で聞くとよくできてる感じがするけど、そういう機能ほど「本当にそう動いてるのか」を自分の目で見るまで信用しないことにしてる。というわけで、この記事は実際にmacOS環境で設定を書いて動かしてみたレポートだ。
deny と mask は何が違うのか
まず前提を揃えておく。sandbox.credentialsは元々v2.1.187から存在する設定で、ファイルパスと環境変数を指定して保護できる。
{
"sandbox": {
"enabled": true,
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" }
],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" }
]
}
}
}
denyはサンドボックス化されたコマンドの実行前に環境変数を丸ごと外す。ファイルなら読み取り自体を拒否する。安全だけど、その変数を使うツールは軒並み動かなくなる。GITHUB_TOKENをdenyにすればghコマンドは認証エラーで止まる。当然だ、トークンごと消えてるんだから。
maskはここが違う。公式ドキュメントの説明を引くと、
サンドボックス化されたコマンドはセッションごとのセンチネル値を、本物の値の代わりに見る。各
maskエントリにはinjectHostsを指定できて、本物の値がアクセスを許可されるホストを列挙する。リクエストがそのホストの一つに向けて出ていくとき、サンドボックスプロキシがセンチネルを本物の値に置き換える。
コマンド本体とそのログには本物のシークレットが一切現れないのに、リクエストは正しく認証される。理屈としては筋が通ってる。問題は「センチネルを本物に置き換える」という部分がプロキシの中で本当に起きてるのかどうかだ。ここを実際に確認した。
実際にやってみた:コマンドが見る値と、外に出ていく値
検証用に--settingsフラグでこういう設定ファイルを渡した。プロジェクトの.claude/settings.jsonからはmask設定が無視される仕様なので(後述)、--settingsで外部から読み込ませる必要がある。
{
"sandbox": {
"enabled": true,
"network": {
"tlsTerminate": {},
"allowedDomains": ["httpbin.org"]
},
"credentials": {
"envVars": [
{ "name": "TEST_SECRET_TOKEN", "mode": "mask", "injectHosts": ["httpbin.org"] }
]
}
}
}
TEST_SECRET_TOKENにsk-realvalue-abcdef123456という偽の「本物っぽい値」をセットして、まずはコマンド自身に何が見えるか確認した。
TEST_SECRET_TOKEN="sk-realvalue-abcdef123456" \
claude --settings mask-test-settings.json -p \
"echo \"TOKEN_SEEN_BY_COMMAND=\$TEST_SECRET_TOKEN\""
返ってきた出力はこれだ。
TOKEN_SEEN_BY_COMMAND=fake_value_0424e70a-e8d6-41ab-b167-6f7929c9e510
セットしたはずのsk-realvalue-abcdef123456はどこにもない。サンドボックス化されたBashコマンドから見えているのは、セッションごとに生成されたランダムなセンチネル値だけだった。ここまではdenyでも似たような結果になりそうだけど(変数が消えてるか偽物かの違い)、本番はここから。
次に、実際に外部へリクエストを飛ばすコマンドを実行させた。httpbin.orgの/getエンドポイントは受け取ったヘッダーをそのままJSONで返してくれるので、リクエストの中身を覗くのにちょうどいい。
TEST_SECRET_TOKEN="sk-realvalue-abcdef123456" \
claude --settings mask-test-settings.json -p \
"curl -s 'https://httpbin.org/get' -H \"Authorization: Bearer \$TEST_SECRET_TOKEN\""
httpbin側が受け取ったAuthorizationヘッダーの値をそのまま報告させたところ、こう返ってきた。
Bearer sk-realvalue-abcdef123456
本物の値がそのまま届いていた。コマンドのプロセス内では見えなかった値が、サンドボックスの外に出ていく瞬間だけ本物にすり替わっている。プロキシがヘッダーの中身まで見て置換してるという説明を、実際のHTTPレスポンスで裏取りできた格好だ。
コマンド自身のログ・標準出力・エラーメッセージには本物のトークンが一度も現れない。でも認証は通る。.envをLLMのコンテキストに晒さずにghやnpmを動かしたい、というフリーランスの実務要求に対して、これはdenyより一段実用的な解決策になってる。
なぜ tlsTerminate が必須なのか
上の設定でnetwork.tlsTerminateを外すとどうなるか。公式ドキュメントにはこう書いてある。
プロキシはリクエストの中身を見て認証情報を置換する必要があるので、プロキシ自身がTLSを終端する必要がある。
network.tlsTerminateを設定しないと、マスキングは何も晒さずに失敗する。コマンドは依然としてセンチネルしか見ないが、そのセンチネルはそのままサーバーに届き、認証が失敗する。
つまりtlsTerminateなしだと、暗号化されたHTTPS通信の中身をプロキシが読めない。読めなければ「センチネルを本物に置き換える」という処理自体ができない。結果、サーバー側には偽物のセンチネル文字列がそのまま届いて、認証エラーで落ちる。実際に僕の検証でもnetwork.tlsTerminate: {}を入れ忘れた最初の設定ファイルでは同じ理屈で失敗する構成になっていて、公式ドキュメントの「安全に倒れる」という設計は納得できる内容だった。
セキュリティの観点で見ると、これは「サンドボックスのプロキシがTLSを一度復号している」ということでもある。中身を検査するには当然そうするしかないんだけど、暗号化された通信の中身をローカルのプロキシプロセスが見ている、という事実は覚えておいたほうがいい。公式ドキュメントもnetwork.tlsTerminateはexperimental設定だと明記している。
AWS のようなSigV4署名系は「セット」でマスクする
これは実際に踏み抜く前に知っておきたかった注意点。AWSのリクエストはAWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYの両方を使ってリクエスト全体に署名する。公式ドキュメントの説明だと、
シークレットだけをマスクすると、リクエストはプレースホルダーで署名されたままになり、プロキシはそれを検知できないので、AWS側で失敗する。Claude Codeはアクセスキーだけがマスクされているケースについては起動時に警告しない。
つまりAWS_SECRET_ACCESS_KEYだけmaskにしてAWS_ACCESS_KEY_IDは素通しにする、という中途半端な設定は動かない上に、起動時の警告にも引っかからないパターンがある。プロキシはアクセスキーのセンチネル値を手がかりにSigV4リクエストを検知して再署名する仕組みなので、2つを必ずセットでマスクする必要がある。地味だけど刺さる人には刺さる話だと思う。
ファイルマスクは Linux/WSL2 限定。macOSではdenyと同じ
環境変数だけでなく、ファイルそのものにもmaskを指定できる。~/.config/gh/hosts.ymlのような構造化ファイルの中の1トークンだけを隠したい場合、extractに正規表現を書く。
{
"sandbox": {
"credentials": {
"files": [
{
"path": "~/.config/gh/hosts.yml",
"mode": "mask",
"extract": "oauth_token:\\s*(\\S+)"
}
]
}
}
}
キャプチャグループ1にマッチした部分だけがセンチネルに置き換わるので、YAMLの他の構造は壊れず、ghコマンド自体はファイルをちゃんとパースできる。ここまでは環境変数マスクと似た発想だ。
ただし、これはプラットフォーム差がはっきりしている機能で、公式ドキュメントにこう明記されている。
Linux/WSL2:サンドボックス化されたコマンドは、シークレットがプレースホルダーに置き換わったセンチネルコピーを読む。macOS:サンドボックス化されたコマンドは、対象ファイルを一切読めない。センチネルコピーは作られず、外向き通信での置換も行われない。つまりそのファイルで認証するツールはサンドボックス内で動かなくなる。
denyと同じ効果になる。
僕の環境はmacOSなので、この機能をそのまま体験することはできない。ファイルマスクを設定しても、macOS上では実質「読めない」というdeny相当の挙動になる。ドキュメント通りなら、catしようとした瞬間に読み取り自体が拒否されるはずだ。ここは自分の手元では再現できない前提としてそのまま書いておく(今回、httpbin.orgへの実リクエストを重ねて試していたところで、無関係な機微情報の持ち出しに見えるとして環境側の自動判定に止められた。以降のファイルマスク検証はいったん打ち切り、公式ドキュメントの記述をそのまま引用するに留めている)。
Linux/WSL2でCIやDevContainer上のClaude Codeを動かしてる人にとっては、~/.aws/credentialsや.netrcのような「複数の値が1ファイルに同居してる」タイプの認証情報を、ツールを壊さずに部分的に隠せるということになる。macOS上のローカル開発では、ファイル単位のマスクではなく環境変数単位のマスクを軸に設計したほうが実用的、というのが今回の検証を通した実感だ。
誰の設定からなら mask が効くのか
もう1つ実務上重要な制約がある。maskエントリ・network.tlsTerminate・credentials.allowPlaintextInjectは、リポジトリの.claude/settings.jsonや.claude/settings.local.jsonからは無視される。有効なのはユーザー設定・管理者が配布するmanaged設定・--settingsCLIフラグだけだ。
理由は明快で、maskは「本物のシークレットを指定したホストに送っていい」という許可を与える設定だから。チェックアウトしてきた他人のリポジトリが勝手に.claude/settings.jsonへmask設定を仕込んで、自分のトークンを知らないホストに流す、という攻撃ベクトルを塞ぐための制限だと理解してる。denyが「どのスコープからでも追加できる(安全側にしか倒れないから)」のに対して、maskは「本人か管理者しか有効化できない」という非対称な設計になっている。
サンドボックス全般のfilesystem.disabledやnetwork.strictAllowlistまわりの話は前に別記事で検証したので、あわせて読むと設定の全体像がつかみやすいと思う。

どこで使うと刺さるか
今回検証した範囲で言うと、maskが効くのは大きく2パターンだと思う。
1つ目は、ghやnpmのようにトークンをそのまま使い続けたいCLIツールを、サンドボックス化されたコマンドから壊さずに使いたいケース。denyだとツールごと死ぬところを、maskなら動かしたまま隠せる。
2つ目は、AIエージェントのログや会話コンテキストにシークレットを絶対に載せたくないケース。Claude Codeがコマンドの標準出力を読む以上、そこにAPIキーが平文で出ていれば会話履歴やセッションログにも残る。maskを使えば、コマンドの出力そのものにセンチネル以外の値が現れない。
逆に、ローカルでの単発スクリプト実行のように、そもそもサンドボックスを使ってない・使う必要がない場面ではmaskのありがたみは薄い。あくまで「サンドボックス化されたBashコマンドに、外部サービス認証が必要な作業を任せたいとき」に効く機能、という位置づけで見ておくのがいいと思う。
シークレット管理を1Password側のVaultで一元化してる場合は、Claude Code側のサンドボックスマスクとどう組み合わせるかも整理しておく価値がある。

エージェントが増えるほどシークレット管理の設計自体が破綻しやすくなる、という話は以前に設計論としてまとめてある。今回のmask機能は、その設計の中の「AIエージェントに直接シークレットを渡さない」というレイヤーを、Claude Code側が公式にサポートし始めた、と位置づけて読むとしっくりくる。

deny / mask / 何もしない、の早見表
毎回ドキュメントを読み返さなくて済むように、判断基準を整理しておく。
| 状況 | 推奨設定 |
|---|---|
| その認証情報を使うツール自体をサンドボックス内で一切動かしたくない | deny(環境変数ごと消す、ファイルなら読み取り拒否) |
| ツールは動かしたいが、コマンドの出力やログに本物の値を絶対出したくない | mask(network.tlsTerminate必須、injectHostsで送信先を絞る) |
| AWSのようにアクセスキーとシークレットがセットで署名に使われる | 両方まとめてmask(片方だけだと壊れる上に警告が出ないことがある) |
| macOSでファイル単位の認証情報を扱っている | ファイルのmaskは事実上denyと同じになる。環境変数側のmaskに寄せて設計する |
| リポジトリを人から借りてきて動かす(信頼してないコードベース) | maskはチェックアウトした側の.claude/settings.jsonからは効かない。ユーザー設定・管理者設定・--settings側で明示的に用意する必要がある |
denyは「安全側にしか倒れない」設定だからどのスコープからでも足せる。maskは「本物の値を外に送っていい」という許可そのものなので、信頼できる場所からしか有効化できない——この非対称性さえ覚えておけば、設定に迷ったときの判断は早くなる。
まとめ
sandbox.credentialsのmaskモードは、「隠すか、動かすか」の二択にあった実務上の摩擦を、コマンド側には偽の値・通信側には本物の値という形で解消する機能だった。実際にhttpbin.orgへのリクエストで確かめた限り、説明通りにコマンドの出力にはセンチネルしか出ず、実際のHTTPリクエストには本物の値が届いていた。
ただし無条件に使える機能ではない。network.tlsTerminateが必須で、AWSのようなSigV4署名系は複数の値をセットでマスクしないと壊れ、ファイルマスクはLinux/WSL2限定でmacOSでは事実上denyと同じになる。そしてmask自体、リポジトリ側の設定からは効かないように意図的に制限されている。便利な機能ほど、効かない条件を先に把握しておいたほうが後で困らない。