
Claude Code の Bash sandbox、有効化しただけでは認証情報が読めてしまいます
製造ビジネステクノロジー部のかずえです。
Bash sandbox とは
Claude Code の Bash sandbox は、Bash ツールで実行されるコマンドとその全子孫プロセスに境界を設定するという機能です。「境界」によってAI エージェントにビルド・テスト・Git 操作など各種作業を比較的自律的に実行させつつ、ホスト環境や秘密情報への影響を最小化することを狙った機能です。
この機能の主な目的は「承認疲れ」による承認ミスを減らすことです。(権限モードによっては、)Claude Code がコマンドを実行しようとするたびにその実行承認をユーザーに求めます。大量の実行承認にユーザーが疲弊し、内容を精査せず承認してしまい、結果として誤操作や機密情報の外部送信などを承認してしまうリスクがあります。この Bash sandbox によって、「逐次承認」ではなく、「事前に安全な境界を定義」することでコマンド実行のリスクを管理する発想です。
※ 厳密に言うと、公式ドキュメントには目的として「プロンプトを止めずに自律実行させる」ことまでしか書かれておらず、「承認疲れによる誤承認を防ぐ」ではありません。一歩踏み込んだ私の独自解釈と捉えてください。
ただ、デフォルトの設定だと弱い
Bash sandbox 機能はセッション中に /sandbox から簡単に有効化が可能です。ただし、単に有効化するだけだと、その「境界」の力は限定的です。
こうなっている理由はおそらく(=私の推測ですが)、基本的に「境界」の力が大きくなるほど利便性が損なわれるからだと思います。つまり Bash sandbox の強さと利便性≒開発者体験(DX)は基本的にトレードオフの関係にあります。デフォルトの設定では、そのバランスを取った設定になっているのだと思います。
情報漏洩に絞って、最大限セキュアな設定を考える
ということで、本エントリでは最大限セキュア寄りに振り切った Bash sandbox の設定を考えてみます。さらに、様々なリスクに対してまとめて検討すると焦点がぼやけるので、情報漏洩リスクに絞ります。強くすることで何が不便になるかも、あわせて書いていきます。
想定する経路も1つに絞ります。サプライチェーン攻撃です。依存パッケージに悪意のあるコードが仕込まれ、npm install の裏で自動実行される preinstall / postinstall などのライフサイクルスクリプトを足がかりに、開発者の手元にある認証情報を読み取って外部へ送信する、という筋道です。2025年9月に npm で観測された Shai-Hulud がこの典型で、盗んだ npm トークンで別のパッケージを汚染しながら自己増殖しました。同年11月に確認された Shai-Hulud 2.0 では実行が preinstall に移って影響範囲が広がり、認証情報の窃取に失敗した場合にホームディレクトリを破壊するフォールバックまで備えていました。
そして AI エージェントに作業を任せるほど、この経路は踏みやすくなると考えています。人間なら「このパッケージ怪しいな」と一度手が止まる場面でも、エージェントは指示どおりに npm install まで進むためです(定量的な裏付けがあるわけではなく、ここは私の実感です)。だからこそ、実行そのものを止めるのではなく、実行されても持ち出せるものがない状態を作る、という発想になります。
先に結論を書きます。sandbox.enabled: true だけでは、情報漏洩対策としてはほとんど効きません。理由は単純で、Bash sandbox のデフォルトは以下のようになっているからです。
- 読み取りはマシン全体が許可されている。
~/.ssh/も~/.aws/credentialsも読める - 環境変数は親プロセスから丸ごと継承される。
NPM_TOKENもAWS_SECRET_ACCESS_KEYもそのまま見える - あらかじめ用意された認証情報の deny リストは存在しない。自分で列挙したファイルと変数だけが制限される
デフォルトの状態を3つに分けると、強さの質がそれぞれ違います。
- 書き込み: 最初から堅い。作業ディレクトリの外は書けず、
.claude配下などの保護パスはallowWriteでもEditの allow ルールでも開けられません - 読み取り: 最初から穴。マシン全体が許可されていて、遮断は自分で設定して初めて機能します
- ネットワーク: 最初は堅いが、使うほど緩む。事前許可ドメインはゼロですが、プロンプトで「Yes, and don't ask again」を選ぶたびに許可リストが太っていきます
情報漏洩(= 読んで、外に出す)を止めたいなら、最初から穴が開いている読み取りと、放っておくと穴になるネットワークの両方に設定を足すところからが本番です。
この記事では公式ドキュメントを根拠に、漏洩の経路ごとに設定を積み上げていきます。
検証環境
検証環境について詳細を知りたい方のみ開いてご確認ください。
本文には、公式ドキュメントに書かれていない挙動や、書かれているとおりにはならなかった挙動がいくつか出てきます。いずれも実機で確認したもので、環境は次のとおりです。
- 主たる検証環境: macOS、Claude Code v2.1.273
- 一部の項目のみ: Linux(Claude Code on the Web の VM)、同じく Claude Code v2.1.273
後者はサンドボックスの入れ子(すでに隔離された VM の中で Bash sandbox を動かしている)にあたるので、環境依存の挙動については割り引いて読んでください。逆に言うと、Claude Code on the Web の中でも Bash sandbox は動きます。
実機で確認したのは以下の7点です。いずれも本文の該当セクションで詳しく書きます。
AWS_*のようなワイルドカードを書いてもエラーにならず、黙って効かない(Linux / 「穴②: 環境変数が丸ごと継承される」)- socat 未インストール時に、警告だけ出してサンドボックスなしでコマンドが通る(Linux / 「
failIfUnavailable: true(フェイルクローズ)」) - mask された変数はサンドボックス内では
fake_value_...に見え、!シェルモードでは本物が返る(macOS / 「認証情報を「渡さずに使わせる」: mask」) tlsTerminateの付け忘れは起動時の画面に出ず、claude doctorで初めて分かる(macOS / 同上)- プロジェクト設定に書いた
strictAllowlistが無視され、承認プロンプトが出てしまう(macOS / 「strictAllowlistでプロンプトごと消す」) denyReadをユーザー設定に誤配置すると、エージェントが Read ツールへ迂回して作業を続けてしまう(macOS / 「filesystem.denyReadでホーム配下をまるごと閉じる」)- 秘密鍵を守るつもりで書いた
Read(//**/*.pem)が、サンドボックス内の HTTPS を全滅させる(macOS / 「巻き込み事故: 秘密鍵を守るルールで HTTPS が壊れる」)
前提: Bash sandbox が守る範囲
まず境界の位置を確認します。Bash sandbox は OS のセキュリティ機構(macOS は Seatbelt、Linux / WSL2 は bubblewrap)を使って、Bash ツールで実行されるコマンドとその全子孫プロセスに境界を強制します。ここが重要で、サプライチェーン攻撃で頻出している npm install が走らせる preinstall / postinstall スクリプトも境界の内側に入ります。
なお Linux / WSL2 では、bubblewrap に加えて socat(通信をサンドボックスのプロキシへ中継する)のインストールも必要です。どちらか一方でも欠けるとサンドボックスは有効になりません。ただし、それでコマンドの実行が止まるわけではありません。デフォルトはフェイルオープンで、警告を出したうえでサンドボックスなしに実行されます(後述の「failIfUnavailable: true(フェイルクローズ)」を参照してください)。macOS は Seatbelt が OS 内蔵なので何も入れる必要がありません。
一方、以下は境界の外です。
- Read / Edit / Write ツール: サンドボックスではなくパーミッションシステムで制御される
- MCP サーバー・hooks: Bash ツール経由ではないため、サンドボックスの対象外
!シェルモードで自分が打つコマンド: Claude Code のセッション内で打っていても、Bash ツールを経由しないため境界の外- 人間が自分のターミナルで叩くコマンド、IDE の拡張機能、アプリ起動
3つめは特に見落としやすいところです。Claude Code のセッションの中で打っているので境界の内側だと錯覚しがちですが、公式ドキュメントは明言しています。
A developer can still type a command at the
!shell-mode prompt and run it outside the sandbox, with the same access they already have in any terminal outside Claude Code.
例外は2つだけです。バックグラウンドセッションと、Linux で CLAUDE_CODE_SUBPROCESS_ENV_SCRUB を設定している場合。このどちらでもなければ、allowUnsandboxedCommands: false(後述の Strict sandbox mode)にしていてもシェルモードは境界の外を走ります。
そしてこの挙動は v2.1.260 で変わっています。それ以前は Strict sandbox mode がシェルモードのコマンドも境界の内側に入れていました。古い記憶のまま検証すると結果が食い違うので、試す前に claude --version を確認してください。
実際、私はこの記事の設定を検証する際に ! で cat を叩いてしまい、「読めてはいけないファイルが読めた」と一度誤判定しました。設定が効いているかを確かめたいときは、Claude に依頼して Bash ツール経由で実行させる必要があります。
デフォルトのファイルシステム挙動を整理すると、こうなります。
| 操作 | デフォルトの範囲 |
|---|---|
| 書き込み | 作業ディレクトリとその配下 + セッション用一時ディレクトリのみ |
| 読み取り | マシン全体(一部の拒否ディレクトリを除く) |
| ネットワーク | 事前許可ドメインはゼロ。都度プロンプトで確認 |
ネットワークの「事前許可ドメインはゼロ」は一見もっとも堅く見えますが、この表はあくまで初期値です。3つのうち許可リストだけは、運用のなかで太っていきます(後述)。
書き込み側は最初から堅いです。~/.bashrc などのシェル設定、/bin/ のシステムバイナリは変更できません。さらに「保護パス」として、作業ディレクトリ内であっても .claude 配下の設定ファイル・.claude/hooks・.mcp.json・.git/hooks・.gitconfig などへの書き込みは拒否されます。サンドボックス内のコードが自分の権限を広げたり、境界の外で動く hook を仕込むのを防ぐためです。この保護は allowWrite でも Edit の allow ルールでも解除できません。
仕様の列挙だけだとありがたみが薄いので、具体例を挙げます。前述の Shai-Hulud 2.0 は認証情報の窃取に失敗するとホームディレクトリを破壊しますが、Bash ツール経由で実行される限り、この破壊は作業ディレクトリの外には届きません。この記事は読み取りの話に絞りますが、書き込み側はデフォルトのままですでに効いているわけです。
問題は読み取り側です。ここから塞いでいきます。
穴①: 認証情報ファイルが読める
デフォルトの読み取り範囲はマシン全体なので、~/.ssh/ や ~/.aws/credentials はサンドボックス内から素通しで読めます。ドキュメントも明示しています。
Default read behavior: read access to the entire computer, except certain denied directories. Note that this default still allows reading credential files such as
~/.aws/credentialsand~/.ssh/.
塞ぐ方法は2つあります。
sandbox.credentials.files で個別に拒否する
認証情報専用のブロックです。mode: "deny" を指定すると、そのパスの読み取りがサンドボックス内で拒否されます。
{
"sandbox": {
"enabled": true,
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
]
}
}
}
deny エントリはすべての設定スコープからマージされます。どのスコープからでも追加できるが、他のスコープが追加した deny を別のスコープが取り消すことはできないという一方通行の設計です。
なお sandbox.credentials ブロック自体は Claude Code v2.1.187 以降が必要です。
filesystem.denyRead でホーム配下をまるごと閉じる
より強い手として、ホームディレクトリ全体を閉じてからプロジェクトだけ開け直す形があります。読み取りルールが重なった場合はより具体的なパスが勝つため、こう書けます。
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["~/"],
"allowRead": ["."]
}
}
}
この設定はプロジェクトの .claude/settings.json に置く必要があります。ここが地味な罠で、. の解決先が設定ファイルの場所で変わります。
| プレフィックス | 解決先 |
|---|---|
/ |
ファイルシステムルートからの絶対パス |
~/ |
ホームディレクトリ基準 |
./ またはプレフィックスなし |
プロジェクト設定ならプロジェクトルート、ユーザー設定なら ~/.claude |
公式ドキュメントも、この設定をプロジェクト設定に置くよう名指しで指示しています。
Place it in your project's
.claude/settings.json, because the relative path.resolves to the project root only when the configuration lives in project settings
同じ JSON を ~/.claude/settings.json に置くと、. は ~/.claude に解決されるため、プロジェクトのファイルは denyRead: ["~/"] に引っかかったまま読めなくなるのです。エラーではなく「静かに意図と違う結果になる」パターンなので注意してください。
実際にこの誤配置を試してみたところ、「静かに」の意味がよく分かりました。ユーザー設定に置いた状態で Claude にプロジェクト内のファイルを cat させると、こう返ってきます。
Bashのサンドボックス制限でcatコマンドは実行できませんでした(プロジェクトディレクトリへの読み取りがサンドボックスでブロックされていました)。代わりにReadツールで内容を確認しました。
前述のとおり Read ツールは境界の外なので、エージェントが自力で迂回して目的を達成してしまいます。作業自体は進むので、設定ミスに気づく機会がありません。気づかないまま「Bash で動かすツールだけが理由の分からない失敗をする環境」ができあがります。
プレフィックスの詳しい扱い(末尾スラッシュやワイルドカード)は設定リファレンスに記載があります。
また denyRead: ["~/"] はかなり強い設定です。ホーム配下にツールチェーン(~/.nvm、~/.cargo、~/.rustup など)を置いている環境ではビルドが通らなくなるので、必要な分だけ allowRead で開け直す運用になります。逆方向の保護もあって、allowRead: ["~/"] のような広い許可の中に denyRead: ["~/**/.env"] を書くと deny が勝ちます。広い許可でシークレットを不意に再公開してしまうことはありません。
パーミッションの Read deny ルールも合流する
3つめの経路として、サンドボックスの設定ではないものも読み取り制限に効きます。permissions.deny に Read(//**/id_rsa*) のようなルールを書いていると、そのパスはサンドボックスの読み取り制限にもそのまま合流します。
Paths and domains from both sandbox settings and permission rules are merged into the final sandbox configuration.
パーミッションルールとサンドボックスは別レイヤー(前者はコマンド実行前の判定、後者は OS による強制)ですが、パスとドメインに関しては最終的なサンドボックス設定にマージされるということです。後述するネットワーク側で WebFetch(domain:...) の allow ルールが許可リストに流れ込むのと対称的な話で、こちらは読み取り版にあたります。
なので、すでに Read / Edit の deny ルールで秘密鍵を塞いでいる環境なら、sandbox ブロックを触る前から一部は守られていることになります。逆に言うと、sandbox の設定だけを見ても実際の境界は分かりません。/sandbox の Config タブで解決後の値を確認するのが確実です。実際に見ると、自分が書いた denyRead に加えてパーミッション由来のパスが並んでいるのが分かります。
巻き込み事故: 秘密鍵を守るルールで HTTPS が壊れる
この合流には副作用があります。私は ~/.claude/settings.json に、秘密鍵を Claude に読ませないつもりで以下を入れていました。
{
"permissions": {
"deny": [
"Read(//**/id_rsa*)",
"Read(//**/id_ed25519*)",
"Read(//**/*.pem)"
]
}
}
これがサンドボックス内からの HTTPS 通信を全滅させていました。
* Establish HTTP proxy tunnel to api.github.com:443
< HTTP/1.1 200 Connection Established
* (304) (OUT), TLS handshake, Client hello (1):
* error setting certificate verify locations: CAfile: /etc/ssl/cert.pem CApath: none
curl: (77) error setting certificate verify locations: CAfile: /etc/ssl/cert.pem CApath: none
macOS の CA バンドルは /etc/ssl/cert.pem で、これが *.pem に当たります。サンドボックス内から cat /etc/ssl/cert.pem すると Operation not permitted になり、TLS の証明書検証ができません。プロキシのトンネル自体は 200 で確立できているのに落ちるので、原因が非常に見えにくいです。
切り分けの手がかりは、HTTP は通るのに HTTPS だけ落ちることです。同じホストでも curl http://example.com は成功します。
さらに厄介なのは、原因となる2つの設定が別々のファイルにあることです。
Read(//**/*.pem)はユーザー設定にあり、しかも元々はRead ツールに秘密鍵を読ませないための設定sandbox.enabled: trueはプロジェクト設定
どちらのファイルを単体で眺めても原因にたどり着けません。Read ツール向けに書いたつもりのルールが、別のファイルでサンドボックスを有効にした瞬間に Bash にも波及する、という構図です。ここでも /sandbox の Config タブが唯一の答え合わせになります。
秘密鍵を守るための設定が TLS そのものを壊していたわけで、拡張子でまとめて塞ぐルールはこういう巻き込み事故を起こします。~/.ssh/** のようにディレクトリを絞るか、CA バンドルのパスを sandbox.filesystem.allowRead で開け直しておくのが無難です。
なお公式ドキュメントにも macOS の TLS 検証に関する記述はありますが、そちらは gh / gcloud / terraform といった Go 製 CLI の話で、この現象とは別物です。
穴②: 環境変数が丸ごと継承される
サンドボックス内のコマンドは、デフォルトで親プロセスの環境変数をそのまま継承します。NPM_TOKEN や AWS_SECRET_ACCESS_KEY を環境変数で持っているなら、postinstall スクリプトからも見えます。
sandbox.credentials.envVars で、サンドボックス内のコマンド実行前に変数を unset できます。
{
"sandbox": {
"enabled": true,
"credentials": {
"envVars": [
{ "name": "NPM_TOKEN", "mode": "deny" },
{ "name": "GITHUB_TOKEN", "mode": "deny" },
{ "name": "AWS_ACCESS_KEY_ID", "mode": "deny" },
{ "name": "AWS_SECRET_ACCESS_KEY", "mode": "deny" },
{ "name": "AWS_SESSION_TOKEN", "mode": "deny" }
]
}
}
}
AWS_* のようなワイルドカードで一括指定したくなりますが、できません。設定リファレンスに明記されています。
The
namemust start with a letter or underscore and contain only letters, digits, and underscores.
英数字とアンダースコアのみなので、変数名は1つずつ列挙する必要があります。
厄介なのは、AWS_* と書いてもエラーにならないことです。手元(v2.1.273)で { "name": "AWS_*", "mode": "deny" } を入れて試したところ、起動時の警告もエラーもないまま、AWS_ACCESS_KEY_ID も AWS_SECRET_ACCESS_KEY もサンドボックス内から見えたままでした。「書いたから塞がっているはず」と思い込みやすい挙動なので、列挙が漏れていないか目視で確認するしかありません。
まとめて剥がすなら CLAUDE_CODE_SUBPROCESS_ENV_SCRUB
一括で落としたい場合は環境変数側の仕組みを使います。CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 を設定すると、Anthropic およびクラウドプロバイダーの認証情報がサブプロセスの環境から剥がされます。
こちらのほうが射程が広いのがポイントで、Bash ツールだけでなく hooks と MCP stdio サーバーもカバーします。前述のとおり hooks と MCP はサンドボックスの外なので、sandbox.credentials では守れない範囲です。親の Claude プロセスは API 呼び出し用に認証情報を保持し続けますが、子プロセスからは読めなくなります。
Linux では追加で、Bash サブプロセスを独立した PID 名前空間で実行するため /proc 経由でホストプロセスの環境を読むこともできなくなります。副作用として ps / pgrep / kill がホストのプロセスを見えなくなる点は把握しておいてください。
なおこの変数を設定すると autoAllowBashIfSandboxed が off になり、filesystem.disabled が全スコープで無視される(= ファイルシステム分離が必ず on になる)という副作用もあります。
認証情報を「渡さずに使わせる」: mask
deny の弱点は、トークンを必要とするツールが動かなくなることです。gh や npm は認証情報がないと機能しません。「読まれたくないが、使えないと困る」という要求に応えるのが mode: "mask" です。
仕組みはこうです。
- サンドボックス内のコマンドには、本物ではなく セッションごとのダミー値(sentinel) が見える
- サンドボックスの外で動いているプロキシが、
injectHostsに指定したホスト宛の送信時に本物の値へ差し替える
結果として、コマンド自身もコマンドが出力するログも本物の認証情報を保持しないのに、リクエストの認証は通ります。
手元(v2.1.273)で GITHUB_TOKEN を mask して確かめると、サンドボックス内ではこう見えました。
$ printenv GITHUB_TOKEN
fake_value_f63c59ff-b003-47b6-9652-fb4186680cbc...
fake_value_ で始まる、本物とは似ても似つかない文字列です。一方、同じ変数を ! シェルモード(前述のとおり境界の外)で見ると設定した本物の値が返ります。この2つを見比べるのが、mask が効いているかを確かめる一番簡単な方法です。
以下が maskの設定例です。
{
"sandbox": {
"enabled": true,
"network": {
"tlsTerminate": {},
"allowedDomains": ["*.github.com", "registry.npmjs.org"]
},
"credentials": {
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] },
{ "name": "NPM_TOKEN", "mode": "mask" }
]
}
}
}
使う際の条件が3つあります。
network.tlsTerminateが必須。プロキシがリクエスト内容を書き換えるには TLS を終端して中身を見る必要があります。{}を指定するとセッション用の一時的な CA が生成されます。未設定だと、sentinel がそのままサーバーに届いて認証が失敗します(漏洩はしません)injectHostsの宛先はallowedDomainsでも到達可能でなければならない。プロキシは許可リストが通した接続にのみ注入します。injectHostsを省略するとallowedDomainsの全ホストが対象になります- リポジトリの設定ファイルからは効かない。後述します
1つめの付け忘れには検出機構があるのですが、起動時の画面には出ません。手元(v2.1.273)で tlsTerminate を外して起動しても表示上は正常で、claude doctor を実行して初めて分かりました。
% claude doctor
(略)
1 warning found
- sandbox.credentials mask entries (GITHUB_TOKEN) are configured but TLS termination is unavailable — sandboxed commands see only a sentinel value and the proxy cannot substitute the real credential on egress, so tools needing these will fail to authenticate.
Fix: Enable sandbox.network.tlsTerminate (or remove the mask entries)
ドキュメントは「起動時に報告される」(reports this misconfiguration at startup)と書いているので、起動時に気づけるつもりでいると見落とします。mask を設定したら claude doctor を一度通しておくのが確実です。
置換の対象はヘッダーとリクエストボディです。値に構造があるケース向けに、extract(正規表現のグループ1だけをマスクする。DATABASE_URL の中のパスワードだけ隠す用途)や decode: "jwt"(構造的に正しい偽 JWT に差し替える)といったオプションもあります(v2.1.224 以降)。ファイルに対する mask も可能ですが、Linux / WSL2 では sentinel コピーが読まれる一方、macOS では単にファイルが読めなくなる(実質 deny と同じ)というプラットフォーム差があります(v2.1.221 以降)。
なお mask は、安全にマスクできないエントリを黙って deny に落とします。ドキュメントは4つの条件を挙げています。
Claude Code falls back to
denyfor amaskentry it can't mask safely: a directory path, a glob pattern, a file larger than 8 MiB, or a file that isn't UTF-8 text.
ディレクトリ、glob パターン、8 MiB を超えるファイル、UTF-8 でないファイルの4つです。~/.aws/* のような書き方をしがちな glob が含まれている点は、意識しておいたほうがよいと思います。
AWS のように署名(SigV4)を使う場合は、AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY をセットで mask する必要があります。プロキシはアクセスキーの sentinel を手がかりにリクエストを検出し、本物に差し替えてから再署名します。シークレットだけを mask するとプレースホルダーで署名されたリクエストを検出できず、AWS 側で失敗します。
mask が防ぐもの・防がないもの
ここは正確に押さえておきたいところです。mask が防ぐのは「認証情報そのものの盗難」であって、「その認証情報の悪用」ではありません。
npm install のために registry.npmjs.org を許可すると、盗まれた NPM_TOKEN による npm publish(前述の Shai-Hulud の自己増殖経路)は正当な通信として通ってしまいます。許可済みドメイン自体が流出経路になる、という限界です。
NPM_TOKEN を mask してもこの穴は閉じません。プロキシは許可リストが通した接続に本物の値を注入するので、サンドボックス内のマルウェアが npm publish を実行すれば、プロキシがそれを認証してしまいます。
mask が確実に潰すのは、次の経路です。
- トークンの値を
echoしてログやファイルに書き出す - トークンの値を攻撃者のサーバーに POST する
- トークンの値を別の許可済みドメインに紛れ込ませて持ち出す
「値そのものは絶対に外へ出さない」という保証であって、「許可済みドメインに対する正当な形式の操作」は止まりません。ここは次節のネットワーク設定と、そもそも境界の内側に持ち込む認証情報を減らす設計で対処する話になります。
出口を絞る: ネットワーク設定
漏洩は「読む」だけでは成立せず「外に出す」が必要なので、出口の設定は読み取り制限と同じくらい効きます。
デフォルトでは事前許可ドメインはゼロで、新しいドメインが必要になるたびにプロンプトが出ます。ここに落とし穴があって、プロンプトで「Yes, and don't ask again」を選ぶと WebFetch(domain:...) の allow ルールがローカル設定に保存され、それがサンドボックスの許可リストにも流れ込みます。つまり許可リストは使っているうちに自然に太っていきます。
allowedDomains を明示し、最小限に保つ
必要なドメインは先に列挙してプロンプトを避けます。ワイルドカードは先頭の *. 形式が使えます。
{
"sandbox": {
"network": {
"allowedDomains": ["registry.npmjs.org", "*.github.com"]
}
}
}
github.com のような広いドメインの許可は、ドキュメント自身が漏洩経路になりうると警告しています。GitHub は書き込みができる先(Gist、リポジトリ、Issue)なので、許可の粒度は意識したほうがよいです。
deniedDomains で例外を穴埋めする
allowedDomains のワイルドカードが意図せず広く当たってしまう場合、deniedDomains が優先されます。
{
"sandbox": {
"network": {
"allowedDomains": ["*.example.com"],
"deniedDomains": ["sensitive.cloud.example.com"]
}
}
}
strictAllowlist でプロンプトごと消す
情報漏洩対策として一番効くのがこれです。true にすると、許可リスト外のホストへのアクセスはプロンプトを出さずに拒否されます(v2.1.219 以降)。
{
"sandbox": {
"network": {
"strictAllowlist": true,
"allowedDomains": ["registry.npmjs.org", "*.github.com"]
}
}
}
「うっかり Yes を押して許可リストが太る」経路を構造的に塞げます。
塞げるものがもう1つあります。auto モードには、Claude がそのコマンドに必要なホストを申告する仕組み(per-command allowed domains)があります。申告されたホストはそのコマンドの実行中だけ開き、セッションの許可リストにも設定にも残りません。便利ではあるのですが、許可リストの外に出る経路には違いありません。strictAllowlist はこれごと拒否します。
A per-command list widens only what the sandbox denies by default.
deniedDomainsentries still block. WhenstrictAllowlistorallowManagedDomainsOnlylocks the allowlist, Claude Code refuses per-command lists.
ただしこの設定は、ユーザー設定か managed settings からのみ有効です。リポジトリ側の設定では on / off できません。
ここを間違えると「効いているつもりで効いていない」状態になります。私はプロジェクトの .claude/settings.json に strictAllowlist: true と allowedDomains: ["example.com"] を書いて試しました。許可していない www.iana.org へ curl させると、確かに一度はブロックされます。ところがその直後に、このコマンドに限り www.iana.org を許可するという承認プロンプトが出てきました。
strictAllowlist が効いていれば、このプロンプトは出ません。ブロックされたのは単に許可リストになかったからで、設定自体は無視されていたわけです。プロンプトが出た時点で「効いていない」と判断できます。設定したら、許可外のホストでプロンプトが出ないことを必ず確認してください。
逃げ道を塞ぐ
allowUnsandboxedCommands: false
Claude Code にはエスケープハッチ(≒迂回経路) があります。サンドボックスの制限でコマンドが失敗したとき、Claude が dangerouslyDisableSandbox パラメータを付けてサンドボックス外で再試行することがあるのです。再試行は通常のパーミッションフローを通るので、手動モードなら確認プロンプトが出ますが、auto モードでは分類器の判断に委ねられます。
false にすると dangerouslyDisableSandbox は完全に無視されます。/sandbox の Overrides タブでは Strict sandbox mode と表示されます。
{
"sandbox": {
"allowUnsandboxedCommands": false
}
}
副作用として、git merge / git checkout が保護パスの書き換えで unable to unlink old エラーになったとき、Claude が外で再試行する提案ができなくなります。自分で別のターミナルで実行するか、そのコマンドを excludedCommands に入れる運用になります。
なお前述のとおり、この設定を入れても ! シェルモードで自分が打つコマンドは境界の外のままです。塞げるのは「Claude が実行するコマンド」だけだと理解しておいてください。
failIfUnavailable: true(フェイルクローズ)
デフォルトの挙動はフェイルオープンです。bubblewrap や socat が入っていない、プラットフォームが非対応、といった理由でサンドボックスが起動できないと、Claude Code は警告を出してサンドボックスなしでコマンドを実行します。
実際に Linux(Claude Code on the Web の VM)で socat を入れずに起動すると、こう表示されたうえでコマンドが通ります。なお socat が要るのは Linux / WSL2 だけなので、この警告は macOS では出ません。
⚠ Sandbox disabled: sandbox is enabled but dependencies are missing: socat not installed
Commands will run WITHOUT sandboxing. Network and filesystem restrictions will NOT be enforced.
sandbox.enabled: true が設定ファイルに書いてあっても、この状態では境界がありません。
セキュリティゲートとして扱うなら、ここは落としておくべきです。
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true
}
}
これで、サンドボックスが起動できない場合は Claude Code 自体が起動時にエラー終了します。
excludedCommands は狭く保つ
excludedCommands に列挙したコマンドは常にサンドボックス外で実行されます。docker はサンドボックスと非互換なので docker * を入れる、macOS で gh / gcloud / terraform が TLS 検証に失敗するので入れる、といった実用上の必要があります。
ただし複合コマンドの一部がマッチすると、コマンド全体がサンドボックス外で実行されます。excludedCommands は穴なので、リストは狭く保ってください。後述しますが、このキーには managed settings によるロックダウンがありません。
autoAllowBashIfSandboxed は漏洩対策ではない
誤解しやすいので明記します。このキーはセキュリティ強度を変えません。ドキュメントは「両モードでファイルシステムとネットワークの制限は同一」と明言しており、違いはサンドボックス済みコマンドを自動承認するかプロンプトを出すかだけです。
false にすればプロンプトは増えますが、漏洩に対する境界の強さは変わりません。人間のレビューを1枚挟む価値はありますが、これを「対策」に数えないほうが設計を見誤りません。
組織全体に強制する
チームやお客様案件で全員に効かせるなら managed settings を使います。ブール値のキー(enabled、failIfUnavailable など)は managed の値が優先され、開発者のローカル設定は無視されます。
一方配列のキー(excludedCommands、allowRead など)は全スコープからマージされるので、開発者が追記してポリシーを広げられます。これを防ぐキーが用意されています。
| キー | 効果 | スコープ |
|---|---|---|
sandbox.filesystem.allowManagedReadPathsOnly |
managed settings 由来の allowRead のみ有効にする。denyRead は全スコープからマージされ続ける |
Managed |
sandbox.network.allowManagedDomainsOnly |
許可ドメインを managed の値に固定し、非許可ドメインはプロンプトなしでブロック | Managed |
managed settings の例です。
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/"],
"allowRead": ["~/work"],
"allowManagedReadPathsOnly": true
},
"network": {
"allowedDomains": ["registry.npmjs.org", "*.github.com"],
"allowManagedDomainsOnly": true
},
"credentials": {
"files": [
{ "path": "~/.aws", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
]
}
}
}
もう1つ知っておきたい挙動があります。managed settings が sandbox.filesystem を設定している場合、または sandbox.credentials.files に "mode": "deny" のエントリを1つでも持つ場合、filesystem.disabled を managed settings からしか設定できなくなります。filesystem.disabled はファイルシステム分離を丸ごと off にするキーなので、管理者が敷いた読み取り制限を開発者が外せないよう固定される、という仕組みです。
なお excludedCommands には同等のロックダウンがありません。開発者は常にエントリを追記してサンドボックス外で動くコマンドを増やせます。managed のリストは狭く保つしかありません。
設定例まとめ(スコープ別)
ここが実用上の要点です。一部のキーはリポジトリの設定ファイルからは無視されます。1つの JSON をプロジェクトに置いて済ませようとすると、半分が効かない状態になります。
リポジトリの .claude/settings.json / .claude/settings.local.json から無視されるキーは以下です。
credentialsのmaskエントリ(denyは有効)network.tlsTerminatecredentials.allowPlaintextInject/awsPairs/sigv4network.strictAllowlistfilesystem.disabledallowAppleEvents
なぜリポジトリからは設定できないのか。この一覧は、性質の違う2組が並んでいると考えると腑に落ちます。
1組目は、本物の認証情報の送信を許可するキーです(mask / tlsTerminate / allowPlaintextInject / awsPairs / sigv4)。ドキュメントが理由を明示しています。
Unlike
deny, masking authorizes the proxy to send your real credential to the listed hosts, so Claude Code honors it only from settings you or your administrator control: user settings, managed settings, and the--settingsCLI flag.
deny が「読ませない」という狭める指定なのに対し、mask は「本物をこのホストへ送ってよい」という許可です。リポジトリ側から injectHosts を書けてしまうと、git clone してきたコードが自分のトークンの送信先を指定できることになります。だからこの組は、自分か管理者が管理するファイルからしか読まれません。
2組目は、境界そのものを弱められるキーです(filesystem.disabled / allowAppleEvents)。こちらも filesystem.disabled について明示があります。
Project settings in
.claude/settings.jsonand.claude/settings.local.jsoncan't, so a checked-out project can't switch filesystem isolation off.
「チェックアウトしてきたプロジェクトがファイルシステム分離を off にできないようにする」と、目的がそのまま書かれています。
残る network.strictAllowlist だけは少し毛色が違って、リポジトリからは off にできないだけでなく on にもできません。前述のとおり、書いたのに効いていないという失敗の原因になります。
通底しているのは、リポジトリの設定ファイルは「他人から受け取ったもの」として扱われているという点です。自分の ~/.claude/settings.json と管理者が配る managed settings は信頼できるが、git clone してきたリポジトリに同じ権限は与えない。だから境界を動かす系のキーと、本物の認証情報を扱う系のキーが、まとめて無視されます。
というわけで、2つに分けます。
~/.claude/settings.json(ユーザー設定)
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": {
"strictAllowlist": true,
"tlsTerminate": {},
"allowedDomains": ["registry.npmjs.org", "*.github.com"]
},
"credentials": {
"files": [
{ "path": "~/.aws", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
],
"envVars": [
{ "name": "AWS_ACCESS_KEY_ID", "mode": "deny" },
{ "name": "AWS_SECRET_ACCESS_KEY", "mode": "deny" },
{ "name": "AWS_SESSION_TOKEN", "mode": "deny" },
{ "name": "NPM_TOKEN", "mode": "deny" },
{ "name": "GITHUB_TOKEN", "mode": "mask", "injectHosts": ["api.github.com"] }
]
}
}
}
このブロックについて2点補足します。
~/.awsはディレクトリごと指定しています。本文では公式ドキュメントの例に合わせて~/.aws/credentialsを挙げましたが、~/.aws/configにもrole_arnやsso_start_url、プロファイル名といった情報が入るため、ここではディレクトリ単位で塞いでいます。なお前述のとおりmaskはディレクトリに対してはdenyにフォールバックするので、ディレクトリを指定するなら最初からdenyと書くのが素直ですstrictAllowlist: trueは「まず全部閉じる」設定です。許可リスト外のホストはプロンプトも出ずに拒否されるため、上記の2ドメインだけではpip/cargo/aptなどは通りません。また GitHub はリリース資産や raw ファイルをgithubusercontent.comという別ドメインで配信するので、*.github.comを許可してもobjects.githubusercontent.comやraw.githubusercontent.comには届きません。git/ghを使うなら別途足す必要があります。使うスタックに応じてドメインを足していく前提の出発点として見てください
プロジェクトの .claude/settings.json
{
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["~/"],
"allowRead": ["."]
}
}
}
. をプロジェクトルートに解決させるため、この filesystem ブロックはプロジェクト設定に置くのが必須です。
バージョン要件
設定キーによって必要な Claude Code のバージョンが違います。古いビルドではエラーではなく黙って無視されるので、効かないと思ったらここを確認してください。
| 機能 | 必要バージョン |
|---|---|
sandbox.credentials(deny) |
v2.1.187 以降 |
環境変数の mask、network.tlsTerminate |
v2.1.199 以降 |
filesystem.disabled |
v2.1.216 以降 |
network.strictAllowlist |
v2.1.219 以降 |
ファイルの mask |
v2.1.221 以降 |
extract / decode / awsPairs / sigv4 |
v2.1.224 以降 |
| IPv6 のブラケット表記 | v2.1.229 以降 |
反省: 公式サンプルのままで止まっていた
正直な話をすると、このブログを書いているリポジトリの .claude/settings.json はこうなっていました。
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": false,
"allowUnsandboxedCommands": false,
"network": {
"allowedDomains": [],
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false
},
"enableWeakerNestedSandbox": false,
"excludedCommands": []
}
}
これは anthropics/claude-code の公式サンプルを下敷きにしたものです(サンプルにある allowManagedPermissionRulesOnly と httpProxyPort / socksProxyPort は持ってきていません)。エスケープハッチは塞いでいるし、allowedDomains も空。悪くないように見えます。
しかし credentials ブロックも filesystem ブロックもありません。この記事の前半で見たとおり、それはつまり ~/.ssh も ~/.aws/credentials も読める状態です。「サンドボックスを有効にしてある」で安心していたけれど、情報漏洩の観点では一番大事な読み取り制限が空だった、というのが実態でした。
そして公式サンプル自体にも、credentials ブロックと filesystem ブロックはありません。サンプルが悪いわけではなく、この2つは環境ごとに中身が変わるので、サンプルに書きようがないという事情だと思います。公式サンプルは出発点であって完成形ではない、というのがこの記事を書いた動機でもあります。
それでも残る穴
設定を積んでも Bash sandbox は完全な隔離境界ではありません。ドキュメントの Limitations に沿って整理します。
ドメインフロンティング: 組み込みプロキシはクライアントが申告したホスト名で許可判定をし、デフォルトでは TLS を終端も検査もしません。つまりサンドボックス内のコードは、ドメインフロンティングのような手法で許可リスト外のホストに到達できる可能性があります。tlsTerminate は mask のために TLS を終端しますが、内容フィルタリングは追加しません。ここに強い保証が必要なら、TLS を終端して検査するカスタムプロキシ(httpProxyPort / socksProxyPort)を立て、その CA をサンドボックス内に入れる構成が必要です。
境界の外にあるもの: 前述のとおり Read / Edit / Write、MCP サーバー、hooks、! シェルモードはサンドボックスの対象外です。ここは CLAUDE_CODE_SUBPROCESS_ENV_SCRUB とパーミッションルールで別に守る必要があります。
Unix ソケット経由の特権昇格: allowUnixSockets の設定は強力なシステムサービスへのアクセスを与えてしまうことがあります。/var/run/docker.sock の許可は、実質的にホストシステムへのアクセス許可です(Dev Container の DooD と同じ構図です)。
弱くするスイッチ群: enableWeakerNestedSandbox(非特権コンテナ内で bubblewrap を動かすため /proc をバインドマウントする)、enableWeakerNetworkIsolation、macOS の allowAppleEvents(サンドボックス内のコマンドが他アプリをサンドボックス外で起動できるようになり、コード実行の隔離が失われる)は、いずれも境界を弱めます。外側で別の隔離が担保されている場合に限って使うべきものです。
作業ディレクトリ内への持続化: 保護パス以外の作業ディレクトリ内は書き換えられます。package.json の scripts を書き換えられれば、開発者が自分のターミナルで npm run dev した瞬間に境界の外で実行されます。
Bash sandbox の強みは、/sandbox と打つだけで今日から始められる手軽さです。一方で守れる範囲は AI のコマンド実行に限られます。それより強い境界が要るなら、Dev Container で開発環境ごと閉じ込める、Claude Code on the Web で端末そのものを手元から外す、といった別の手法を検討することになります。
この3つを「3つの壁」として並べ、守れる範囲と導入ハードルで使い分けを整理した記事があります。
まとめ
sandbox.enabled: trueだけでは情報漏洩対策にならない。デフォルトは読み取りがマシン全体許可、環境変数は親から継承、認証情報の deny リストはあらかじめ用意されていない。ネットワークの許可リストも、「Yes, and don't ask again」を選ぶたびに運用のなかで太っていく- 塞ぐ場所は4つ。読み取り(
credentials.filesのdeny/filesystem.denyRead)、環境変数(credentials.envVars/CLAUDE_CODE_SUBPROCESS_ENV_SCRUB)、出口(allowedDomainsを最小限にしてstrictAllowlist: true)、逃げ道(allowUnsandboxedCommands: falseとfailIfUnavailable: true)。ツールを動かしたまま値だけ隠したいならmode: "mask" - どこに書くかで効き方が変わる。
mask/tlsTerminate/strictAllowlist/filesystem.disabledはリポジトリの設定ファイルからは無視され、逆にdenyRead: ["~/"]+allowRead: ["."]はプロジェクト設定に置かないと.の解決先が変わる。ユーザー設定とプロジェクト設定に分けて書く - 「書いたのに効いていない」が一番危ない。
/sandboxの Config タブで解決後の値を確認し、claude doctorで起動時の画面に出ない警告を拾う。検証するときは!シェルモードを使わない(境界の外なので必ず誤判定する) - ここまで積んだ設定は、強度と利便性≒開発者体験(DX)のトレードオフになりえます。
denyRead: ["~/"]はホーム配下のツールチェーンを巻き込みますし、strictAllowlist: trueは許可し忘れたドメインでビルドを止めます。まず強い側に振っておいて、詰まったところだけallowRead/allowedDomainsで開け直すのが、緩い設定から始めるより安全だと思います
まずは自分の ~/.claude/settings.json に credentials ブロックがあるか確認するところからだと思います。私はありませんでした。
参考
- Configure the sandboxed Bash tool - Claude Docs
- Settings reference(sandbox settings)- Claude Docs
- Environment variables - Claude Docs
- Managed settings - Claude Docs
- Sandbox environments - Claude Docs
- anthropics/claude-code - examples/settings
- "Shai-Hulud" Worm Compromises npm Ecosystem in Supply Chain Attack - Unit 42








