「このコマンド、結局どこにでもデータを送れるんじゃない?」——AI エージェントに任意のコマンドを実行させていると、便利さに慣れたある日ふと、この一言が頭をよぎります。便利なのは間違いないのに、冷静になると背筋が少し寒くなる。
Claude Code には Bash の実行を OS レベルで隔離する sandbox があります。有効にすると、ネットワークの送信先を絞ったり、特定ディレクトリの読み取りを止めたりできる。ところが、これを業務で常用しようとすると面白い壁にぶつかります。守りを固めれば固めるほど、正当な作業まで壊れていくのです。守るために入れたはずの設定で、自分の仕事が止まる。これ、わりと笑えない話で。
この記事は、Bash sandbox を「飾り」ではなく実運用の防御線として機能させるために辿り着いた、3つの線引きの記録です。
この記事で学べること
- ネットワークの送信先を allowlist で絞る egress 隔離の考え方
- 秘匿情報の漏洩を「読み取り禁止」ではなく「出力マスク」で塞ぐ理由
- sandbox から特定の処理だけを安全に除外する設計判断
前提条件
- Claude Code を業務で日常的に使っている(Pro $20/月、Max $100〜$200/月のサブスクリプションで利用可能)
settings.jsonの編集に抵抗がない- macOS(OS の隔離機構として Seatbelt を利用)
なぜコマンド名の拒否リストだけでは足りないのか
多くの人が最初にやるのは「危険なコマンドを拒否リストに入れる」対策です。コマンド名を並べて止める。手軽だし、効いている気もする。でも、ネットワーク経由の情報持ち出しに対してこれで十分なのか、実際どうなの? 答えは、穴だらけでした。
パッケージのインストール時に走る postinstall スクリプト、自前のスクリプトから呼ばれる HTTP クライアント、コンパイル済みのバイナリ。これらはコマンド名での照合をすり抜けます。拒否リストに「この名前を止める」という形でしか書けない以上、名前を持たない経路は止めようがないわけです。
そこで、コマンド名ではなく OS レベルで「デフォルト全拒否」のネットワーク隔離をかけます。設定の骨格はこうです。
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": {
"allowedDomains": [
"github.com",
"registry.npmjs.org",
"crates.io",
"pypi.org"
]
}
}
}
ポイントは failIfUnavailable と allowUnsandboxedCommands の組み合わせです。前者は sandbox の初期化に失敗したとき、隔離なしで黙って続行する(=egress が再び開く)のを禁じます。後者はモデルが sandbox を一時的に無効化してすり抜けるのを禁じます。前者が false、後者が true だと「いつの間にか守りが外れていた」が起きます。上のとおり前者を true、後者を false にして、両方をハードゲートにしておくのが肝でした。
allowedDomains にはツールチェーンが実際に叩く先だけを並べます。npm・GitHub・パッケージレジストリといった「これがないと何も入らない」エンドポイントです。それ以外の送信先は初回に確認が走り、許可すれば記憶される。最初は許可の確認が頻発しますが、数日使えば自分の作業に必要なドメインが出揃って落ち着きます。
ここで一つ注意が必要でした。送信先を絞る防御は強力ですが、ツールによっては OS の隔離下で TLS 検証に失敗して動かなくなります。この「動かなくなったものをどう扱うか」が、次の2つの線引きにつながっていきます。
秘匿情報は「読ませない」のではなく「出力でマスクする」
sandbox には特定ディレクトリの読み取りを禁じる設定もあります。素直に考えれば「.env を読ませなければ漏れないのでは?」となります。実際、最初はそう設計しかけました。
ところがこれは罠でした。.env の読み取りを止めると、ビルドや各種スクリプトが正規に環境変数を読み込む処理まで一緒に壊れるのです。秘匿情報の保護のために、正当なランタイムの動作が動かなくなる。本末転倒です。
そこで読み取り禁止に並べるのは、本当に触る必要のない認証情報のディレクトリだけに絞りました。
{
"sandbox": {
"filesystem": {
"denyRead": [
"~/.aws/**",
"~/.ssh/**",
"~/.gnupg/**"
]
}
}
}
.env をここに入れない、というのが意図的な判断です。では .env の中身が会話の文脈に漏れる問題はどう塞ぐのか。読み込みは許すが、出力だけをマスクする。これが正しいレイヤーでした。
具体的には、ツールの実行結果が文脈に注入される直前にフックを挟み、出力テキスト中の秘匿パターンを [REDACTED:...] に置き換えます。読み込みそのものは妨げないので正規の処理は壊れず、しかし秘匿値が会話に流れ込むことはない。
この方針が正しいと確信できたのは、実データに対する検証で逆の事実が見えたからです。攻撃者視点でマスク漏れを洗い出した実測では、本物の高価値な秘匿情報のうち 43% が素通りしていたことが判明しました(手元の実 .env ファイル群を対象にした計測結果)。穴の内訳は明確で、4つのパターンに分類できました。
- URL に埋め込まれた認証情報(
scheme://user:pass@hostの形)。これが最大の穴で、データベース接続文字列に潜む認証情報が大量に漏れていました - 区切り文字のない命名。アンダースコア区切りを前提にしていたため、連結された大文字の環境変数名が照合から外れていた
- 小文字・キャメルケースの変数名(
apikey=、access_token=など) - JSON のインライン表記(
"apiKey":"値"の形)
これらをマスク処理に追加した結果、同じ実 .env ファイル群に対して、URL 埋め込み認証情報は全て塞がり、高価値項目のマスク数は大きく増えました。同時に、無害な設定値(普通の数値や通常の URL、よくある英単語)を誤ってマスクしないことも検証で確認しています。SALT のような短い単語は誤検知が怖いので、似た綴りの一般語がヒットしないことを一つずつ確かめました。
要するに、禁止のレイヤーを間違えると正当な作業を壊し、しかも肝心の漏洩は塞げていない。読み取りは許してマスクで止める、という線引きがこの問題の正解でした。
sandbox の外に出すのは、名前の決まった実体だけ
3つ目の壁は、前述の TLS 検証の失敗です。
GitHub 操作を伴う一部の処理は、OS の隔離下では認証情報を保管している macOS の keychain にアクセスできず、TLS 検証に失敗して動かなくなります。skill の中から GitHub クライアントを呼び出す処理(PR 作成や情報取得を行うもの)が、軒並み沈黙してしまう。
ここでやってはいけないのが、「スクリプト実行を司る interpreter ごと sandbox から除外する」という雑な対処です。python3 や bash で始まるコマンドを全部除外すれば動きますが、任意のスクリプトが外に出て egress 隔離が無意味になります。
採った方針は、除外するのを「名前の決まった実体」だけにすることでした。
{
"sandbox": {
"excludedCommands": ["gh:*"]
},
"permissions": {
"deny": ["Bash(gh alias *)", "Bash(gh extension *)", "Bash(gh extensions *)", "Bash(gh ext *)", "Bash(gh config set *)", "Bash(gh auth *)"]
}
}
GitHub クライアント自体は書き換えられない場所にあります。ただ実際は、alias・拡張・gh config set browser から任意のコマンドが走ります。gh auth token は鍵も出します。そこで除外と一緒に、その入口を permissions の deny で塞ぎます。
注意:
~/.claude/skills/*のように、ディレクトリ配下を*で除外する書き方は使いません。*は..を含む任意の文字列に一致し、~/.claude/skills/../../<書ける場所>/x.shも通ります。筆者の環境でも、一時ディレクトリのスクリプトがこの形で外で走りました。この形を設定に入れているなら外してください。
どの詰まりなら除外で開けてよく、どれを開けると抜け道になるのかは、会話記録から詰まりを数えて分ける基準にまとめています。
なお、GitHub クライアントを除外しても、その認証情報を保管するディレクトリは引き続き読み取り禁止のままにしてあります。「この処理だけは隔離の外で動かす」と「その秘密を隔離内の他のプロセスから守る」は両立する。除外したからといって、全部を無防備にする必要はないわけです。
動作確認
設定を本番の global 設定に反映する前に、一時的な local 設定で有効化して以下を確認するのが安全です。
- GitHub 操作を伴う処理が実際に動くか(TLS 検証で落ちないか)
- 通常の Bash コマンドが隔離下で走っているか
- 秘匿情報を含む出力が
[REDACTED]に置き換わるか - 隔離下で使う他のツール(コンテナ系・クラウド CLI など)が
allowWrite・allowedDomainsで通るか。除外に足すなら、任意のファイルやコマンドを実行しない実体か
allowUnsandboxedCommands を false にしている以上、通し忘れたツールは「確認が出る」ではなく「ハードに失敗する」挙動になります。普段使うツールは事前に洗い出しておくと、運用開始後に慌てずに済みます。
注意点・Tips
- 守りの強さと作業の通りやすさはトレードオフ: デフォルト全拒否は強力ですが、最初の数日は許可の確認が増えます。自分の作業ドメインが出揃うまでは育てる期間と割り切る
- 禁止のレイヤーを間違えない: 「読ませない」で解けない問題は「出力でマスクする」で解く。逆に、マスクで足りないものを読み取り禁止で塞ごうとすると正当な処理を壊す
- 除外は最小単位で: interpreter 単位でも、
*を含むパス単位でもなく、書き換えられない実体を名前で 1 つずつ。その実体が持つ実行の入口は deny で塞ぐ - 設定の可搬性を意識する: 名前で除外すれば、絶対パスもユーザー名も設定に入らない
これらの設定は単独で効くのではなく、settings.json の権限・フックと組み合わさって初めて防御線になります。フック側の安全設計や、permissions・hooks の落とし所については以下の記事も合わせてどうぞ。
- 業務でClaude Code を半年使って気づいた settings.json の落とし穴と落とし所
- Claude Code Hooks 安全設計:denyルールとPreToolUseフックで「やらかし」を防ぐ実践パターン
- AI作業自動化ツールを3ヶ月運用したチームが直面した落とし穴と修正策
まとめ
Bash sandbox の実運用で効いたのは、機能を全部 ON にすることではなく、どのレイヤーで何を制限し、何を除外するかの線引きでした。
ネットワークはコマンド名ではなく OS レベルでデフォルト全拒否にして送信先を絞る。秘匿情報は読み取り禁止ではなく出力マスクで塞ぎ、正当な読み込みは壊さない。隔離下で動かないものは、interpreter でもパスの glob でもなく、書き換えられない実体を名前で除外する。
守りを固めるほど正当な作業が壊れる、という緊張関係そのものが設計の中身です。雑に全部を禁止すれば作業が止まり、雑に全部を除外すれば守りが消える。その間で、壊さずに守れる一点を探す作業こそが、sandbox を実運用に乗せるということでした。
なお、この線引きを「エージェントに鍵を直接渡さない」という角度から詰め直すと、権限を最小限に絞る構成の話につながります。その最小構成は「AIエージェントにAPIキーを直接渡さずに動かす — 権限を絞る最小構成」で扱っています。



