「このコマンド、結局どこにでもデータを送れるんじゃない?」——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 を一時的に無効化してすり抜けるのを禁じます。この2つが false だと「いつの間にか守りが外れていた」が起きるので、両方をハードゲートにしておくのが肝でした。
allowedDomains にはツールチェーンが実際に叩く先だけを並べます。npm・GitHub・パッケージレジストリといった「これがないと何も入らない」エンドポイントです。それ以外の送信先は初回に確認が走り、許可すれば記憶される。最初は許可の確認が頻発しますが、数日使えば自分の作業に必要なドメインが出揃って落ち着きます。
ここで一つ注意が必要でした。送信先を絞る防御は強力ですが、ツールによっては OS の隔離下で TLS 検証に失敗して動かなくなります。この「動かなくなったものをどう扱うか」が、次の2つの線引きにつながっていきます。
秘匿情報は「読ませない」のではなく「出力でマスクする」
sandbox には特定ディレクトリの読み取りを禁じる設定もあります。素直に考えれば「.env を読ませなければ漏れないのでは?」となります。実際、最初はそう設計しかけました。
ところがこれは罠でした。.env の読み取りを止めると、ビルドや各種スクリプトが正規に環境変数を読み込む処理まで一緒に壊れるのです。秘匿情報の保護のために、正当なランタイムの動作が動かなくなる。本末転倒です。
そこで読み取り禁止に並べるのは、本当に触る必要のない認証情報のディレクトリだけに絞りました。
{
"filesystem": {
"denyRead": [
"~/.aws/**",
"~/.ssh/**",
"~/.gnupg/**"
]
}
}
.env をここに入れない、というのが意図的な判断です。では .env の中身が会話の文脈に漏れる問題はどう塞ぐのか。読み込みは許すが、出力だけをマスクする。これが正しいレイヤーでした。
具体的には、ツールの実行結果が文脈に注入される直前にフックを挟み、出力テキスト中の秘匿パターンを [REDACTED:...] に置き換えます。読み込みそのものは妨げないので正規の処理は壊れず、しかし秘匿値が会話に流れ込むことはない。
この方針が正しいと確信できたのは、実データに対する検証で逆の事実が見えたからです。攻撃者視点でマスク漏れを洗い出した実測では、本物の高価値な秘匿情報のうち 43% が素通りしていたことが判明しました(67個の実 .env ファイルを対象にした計測結果)。穴の内訳は明確で、4つのパターンに分類できました。
- URL に埋め込まれた認証情報(
scheme://user:pass@hostの形)。これが最大の穴で、データベース接続文字列に潜む認証情報が大量に漏れていました - 区切り文字のない命名。アンダースコア区切りを前提にしていたため、連結された大文字の環境変数名が照合から外れていた
- 小文字・キャメルケースの変数名(
apikey=、access_token=など) - JSON のインライン表記(
"apiKey":"値"の形)
これらをマスク処理に追加した結果、67個の実 .env ファイルに対して、URL 埋め込み認証情報は全て塞がり、高価値項目のマスク数は大きく増えました。同時に、無害な設定値(普通の数値や通常の URL、よくある英単語)を誤ってマスクしないことも検証で確認しています。SALT のような短い単語をパターンに足すと誤検知が怖いのですが、似た綴りの一般語が実際にヒットしないことを一つずつ確かめました(このあたりの神経の使い方は、後述する hook の設計記事とも地続きです)。
要するに、禁止のレイヤーを間違えると正当な作業を壊し、しかも肝心の漏洩は塞げていない。読み取りは許してマスクで止める、という線引きがこの問題の正解でした。
skill だけは sandbox の外に出す、ただし interpreter 単位では出さない
3つ目の壁は、前述の TLS 検証の失敗です。
GitHub 操作を伴う一部の処理は、OS の隔離下では認証情報を保管している macOS の keychain にアクセスできず、TLS 検証に失敗して動かなくなります。skill の中から GitHub クライアントを呼び出す処理(PR 作成や情報取得を行うもの)が、軒並み沈黙してしまう。
ここでやってはいけないのが、「スクリプト実行を司る interpreter ごと sandbox から除外する」という雑な対処です。「python3 で始まるコマンドは全部除外」「bash で始まるコマンドは全部除外」とやれば確かに動きますが、それでは任意のスクリプトが隔離の外に出てしまい、最初の egress 隔離が無意味になります。守りを取り戻したつもりが、別の場所で穴を開ける。これでは元も子もありません。
採った方針は、実行経路の「パス」で除外することでした。skill が置かれているディレクトリ配下のコマンドだけを、起動の形(直接実行・bash 経由・python3 経由のいずれでも)に依存せず除外します。
{
"excludedCommands": [
"gh:*",
"~/.claude/skills/*",
"$SKILLS_DIR/*",
"bash ~/.claude/skills/*",
"python3 ~/.claude/skills/*"
]
}
「interpreter で除外」と「パスで除外」は、見た目が似ていて意味が正反対です。前者は「この言語で書かれたものは全部信用する」、後者は「この場所に置かれたものだけ信用する」。信用の単位を場所に紐づけることで、任意のスクリプトに穴が広がるのを防ぎます。
絶対パスで書く案も検討しましたが、不採用にしました。設定ファイルを複数マシンで同期している場合、絶対パスにはユーザー名が埋まり込んでしまい、別マシンに持っていくと壊れる。可搬性のために、ホームディレクトリ起点の相対表現と環境変数を使っています。
なお、GitHub クライアントを除外しても、その認証情報を保管するディレクトリは引き続き読み取り禁止のままにしてあります。「この処理だけは隔離の外で動かす」と「その秘密を隔離内の他のプロセスから守る」は両立する。除外したからといって、全部を無防備にする必要はないわけです。
動作確認
設定を本番の global 設定に反映する前に、一時的な local 設定で有効化して以下を確認するのが安全です。
- GitHub 操作を伴う skill が実際に動くか(TLS 検証で落ちないか)
- 通常の Bash コマンドが隔離下で走っているか
- 秘匿情報を含む出力が
[REDACTED]に置き換わるか - 隔離下で使う他のツール(コンテナ系・クラウド CLI など)が必要なら除外リストに入っているか
allowUnsandboxedCommands を false にしている以上、除外し忘れたツールは「確認が出る」ではなく「ハードに失敗する」挙動になります。普段使うツールは事前に洗い出しておくと、運用開始後に慌てずに済みます。
注意点・Tips
- 守りの強さと作業の通りやすさはトレードオフ: デフォルト全拒否は強力ですが、最初の数日は許可の確認が増えます。自分の作業ドメインが出揃うまでは育てる期間と割り切る
- 禁止のレイヤーを間違えない: 「読ませない」で解けない問題は「出力でマスクする」で解く。逆に、マスクで足りないものを読み取り禁止で塞ごうとすると正当な処理を壊す
- 除外は最小単位で: interpreter 単位ではなくパス単位。信用する範囲は狭く具体的に
- 設定の可搬性を意識する: 複数マシンで同期するなら絶対パスやユーザー名を埋め込まない
これらの設定は単独で効くのではなく、settings.json の権限・フックと組み合わさって初めて防御線になります。フック側の安全設計や、permissions・hooks の落とし所については以下の記事も合わせてどうぞ。
- 業務でClaude Code を半年使って気づいた settings.json の落とし穴と落とし所
- Claude Code Hooks 安全設計:denyルールとPreToolUseフックで「やらかし」を防ぐ実践パターン
- AI作業自動化ツールを3ヶ月運用したチームが直面した落とし穴と修正策
まとめ
Bash sandbox の実運用で効いたのは、機能を全部 ON にすることではなく、どのレイヤーで何を制限し、何を除外するかの線引きでした。
ネットワークはコマンド名ではなく OS レベルでデフォルト全拒否にして送信先を絞る。秘匿情報は読み取り禁止ではなく出力マスクで塞ぎ、正当な読み込みは壊さない。隔離下で動かないものは interpreter 単位ではなくパス単位で、信用する範囲を最小に切り出して除外する。
守りを固めるほど正当な作業が壊れる、という緊張関係そのものが設計の中身です。雑に全部を禁止すれば作業が止まり、雑に全部を除外すれば守りが消える。その間で、壊さずに守れる一点を探す作業こそが、sandbox を実運用に乗せるということでした。



