Arganoの池田です。 本記事では、弊社でも使う機会が増えているClaude Codeを安全に使うためのsandbox機能について、主要機能であるネットワークの制限とファイルアクセスの制限がどのように実現されているか、そして設定がどのように挙動に反映されるかについて調査を行い理解を深めていきます。
sandbox について
まず、Claude Codeにはサンドボックス機能が組み込まれています。所謂 /sandbox ですが、これを有効化するとClaude Code上で実行されるBashコマンドは /sandbox による制限がかかった状態で実行されます。利用目的としては、ローカルのファイル及びネットワークアクセスの制限ですが、組み込みのファイルツール、MCPサーバー、及びフックは引き続きホスト上で直接実行されるという特徴があります。
この /sandbox の内部で使われているサンドボックス化の仕組みは、Sandbox runtimeとしてOSSでGitHub上に公開されています。Sandbox runtimeを直接使ってClaude Codeのプロセスそのものを外側から包むと、 /sandbox ではサンドボックス化されていなかった組み込みのファイルツール、MCPサーバー、及びフックを含む全てのプロセスをサンドボックス化した状態にできます。以降の節ではこちらのリポジトリの利用方法と実装を眺めつつ詳細を見ていきます。
参考: Sandboxed Bash Tool Sandbox runtime
Sandbox runtime の概要
以降はリポジトリをクローンしてコードやドキュメントを読みつつ、npxで実際に動かしてみます。なお、本記事は執筆時点(2026/09/30)の main ブランチ(コミット ddbeb74)のコードを参照しており、以降の更新で実装や挙動が変わっている可能性があります。
主要なアクセス制限としては以下のような機能があるようです。
- ネットワークアクセスの制限: HTTP/HTTPS及び、その他のプロトコルでアクセス可能なホスト・ドメインを制限する
- ファイルアクセスの制限: アクセス可能なファイル・ディレクトリを制限する
- UNIXソケットの制限: アクセス可能なUNIXソケットを制限する
利用方法
以下のようにnpxコマンドからSandbox runtimeを利用して、ネットワークやファイルへのアクセスを制限した状態でClaude Codeを実行出来ます。
npx @anthropic-ai/sandbox-runtime claude
制限の内容は ~/.srt-settings.json に書きます(--settings <path> で別のファイルも指定できます)。例えば以下のような設定を入れてみます。
{
"network": {
"allowedDomains": ["github.com"],
"deniedDomains": []
},
"filesystem": {
"denyRead": ["~/.ssh"],
"allowWrite": ["."],
"denyWrite": []
}
}
この状態で、プロジェクトディレクトリからいくつかのコマンドをサンドボックス内で実行してみます。なお、 allowWrite の . はコマンドを実行したディレクトリを指すため、ホームディレクトリで実行すると最後の例は書き込めてしまう点に注意してください。
# 許可したドメインにはアクセスできる
npx @anthropic-ai/sandbox-runtime -- curl -sI https://github.com
# 許可していないドメインはProxyで拒否される
npx @anthropic-ai/sandbox-runtime -- curl -sI https://example.com
# denyRead に入れたパスは読めない
npx @anthropic-ai/sandbox-runtime cat ~/.ssh/id_ed25519
# allowWrite に含まれないパスには書き込めない
npx @anthropic-ai/sandbox-runtime touch ~/sandbox-test
拒否された際の出力は以下の通りです。
% npx @anthropic-ai/sandbox-runtime -- curl -sI https://example.com
HTTP/1.1 403 Forbidden
Content-Type: text/plain
X-Proxy-Error: blocked-by-allowlist
% npx @anthropic-ai/sandbox-runtime cat ~/.ssh/id_ed25519
cat: ~/.ssh/id_ed25519: Operation not permitted
% npx @anthropic-ai/sandbox-runtime touch ~/sandbox-test
touch: ~/sandbox-test: Operation not permitted
なお、設定ファイルが存在しない場合のデフォルトは allowedDomains: []、allowWrite: [] です。つまりネットワークは全て遮断され、書き込みも /tmp/claude などの既定のパスにしか出来ません。ただし、既定値が使われるのは ~/.srt-settings.json が存在しない場合のみで、 --settings で指定したファイルが存在しない場合や、設定ファイルが空・不正な場合は既定値に戻らずエラーで終了します。そのため claude をサンドボックス内で動かす場合は、Anthropic APIのドメインや、Claude Codeが書き込む設定ディレクトリを明示的に許可してください。
私の手元では、 "allowPty": true とclaudeが依存するホスト・設定ファイルへのアクセスを追加することによりサンドボックス内で claude が起動することを確認しました。
なお、この設定は検証において利用した設定であり過不足がないことを保証するものではないため、必要なホスト名やファイルは適宜追加してください。
{
"allowPty": true,
"network": {
"allowedDomains": [
"github.com",
"api.anthropic.com",
"claude.ai"
],
"deniedDomains": []
},
"filesystem": {
"denyRead": ["~/.ssh"],
"allowWrite": [".", "~/.claude", "~/.claude.json"],
"denyWrite": [
"~/.claude/settings.json",
"./.claude/settings.json",
"./.claude/settings.local.json"
]
}
}
allowWrite に ~/.claude や . を含めると、サンドボックス内のセッションがClaude Codeの設定ファイルも書き換えられる状態になります。設定ファイルに書き込まれたフックや権限ルールは、次回サンドボックスなしでClaude Codeを起動した際にサンドボックスの外で実行されます。そのため、上記の設定ではClaude Codeが設定を読み込むファイルを denyWrite で塞いでいます。
また、 allowedDomains のうち *. で始まらないエントリはホスト名の完全一致で判定され、サブドメインには一致しません。サブドメインも許可したい場合は *.example.com のように指定します。
macOSでの実装
私は現在macOSの端末を利用しています。macOSでは、Sandbox runtimeは内部でSeatbeltのサンドボックス機能を利用します。その実装をコードで確認してみます。
サンドボックスを起動するCLIコマンドのエントリポイントは以下のファイルです。
このファイルの main() を追っていくと、以下の処理を順に実行しています。
- commander によるコマンド定義の組み立て
- 設定ファイルの読み込み
- control fdを開く(
--control-fdを指定した場合のみ) - 読み込んだ設定ファイルでサンドボックス初期化 (
SandboxManager.initialize(runtimeConfig)) - control fdから届いた設定を反映するリスナーの登録(
--control-fdを指定した場合のみ。正しいJSONの行が1行届くごとにSandboxManager.updateConfig()が呼ばれ、不正な行は無視されて直前の設定が維持される) - 実行するコマンドの決定
- サンドボックス用のコマンド文字列の生成 (
SandboxManager.wrapWithSandbox(command)) - 生成したコマンドを
spawnで実行し、子プロセスの終了コードを引き継いで終了
サンドボックスの初期化
次に、サンドボックスの初期化処理の中身を見てみます。
sandbox-runtime/src/sandbox/sandbox-manager.ts の initialize() のうち、macOSで通る処理は以下の通りです。
- 設定の保存と前処理 : 受け取った設定を保持し、上流Proxy(
parentProxyや環境変数HTTP_PROXY)の解決や、名前解決後のIPアドレスをチェックするガードを作成する - TLS終端用CAの準備 :
network.tlsTerminateを設定した場合のみ - 依存チェック :
checkDependenciesAsync()で必要なものが揃っているか確認し、足りなければ例外で終了する - 終了時の後片付けを登録 :
registerCleanup() - ネットワーク基盤の起動 :
- 16バイトのランダムなProxy認証トークンを生成する。このトークンは
HTTP_PROXY=http://<user>:<token>@localhost:<port>の形でサンドボックス内のプロセスにだけ渡されるため、同じホスト上の他のプロセスはこのProxyを利用できない startMuxProxyServer()で、HTTPとSOCKS5を1つのポートで受けるProxyをホスト側に起動する
- 16バイトのランダムなProxy認証トークンを生成する。このトークンは
2のTLS終端は、ProxyでTLS通信を復号することでホスト名以外の情報(メソッドやURLなど)でフィルタリング処理を記述できるようにするためのものです。そこまで考えられているのは興味深いです。
ここで分かることは、この時点ではまだSeatbeltのプロファイルは作られていないという点です。initialize() が準備するのは、ドメインの許可・拒否を判定するためにホスト側で動くProxyです。サンドボックスのルールそのものは、次に見る wrapWithSandbox() でコマンドごとに組み立てられます。
サンドボックス内でのコマンド実行
最後にサンドボックス内部で実際に実行されるコマンドを確認します。
sandbox-manager.ts の wrapWithSandbox() では、以下のように設定を整理します。
- 書き込み: 既定の書き込み先と
allowWriteを合わせて許可リストとし、denyWriteをその中の例外とする- 既定の書き込み先は
/dev/stdoutや/dev/nullなどの標準入出力系、/tmp/claude、/private/tmp/claude、~/.npm/_logs、~/.claude/debugである。ホーム配下の2つは、denyReadでホームを塞ぐと除外される - また、
.bashrc、.zshrc、.gitconfig、.git/hooks/、.git/config、.mcp.json、.vscode/、.claude/commands/、.claude/agents/などは、allowWriteに含まれていても常に書き込みが拒否される
- 既定の書き込み先は
- 読み込み:
denyReadを拒否リストとし、allowReadをその中で再度許可するリストとする - ネットワーク:
network.allowedDomainsが定義されていれば(空配列であっても)ネットワーク制限ありとして扱う
その後、OSごとの実装に振り分けます。macOSの場合は src/sandbox/macos-sandbox-utils.ts の wrapCommandWithSandboxMacOS() です。
wrapCommandWithSandboxMacOS() 内部ではSeatbeltのプロファイル (SBPL) を生成しています。
このプロファイルは (version 1) と (deny default) から始まるデフォルト拒否のプロファイルで、プロセス実行や一部のMachサービス、sysctlなど、プログラムの動作に最低限必要な許可を含みます。ここではネットワークとファイルアクセスについて、設定がどのように反映されるかを見てみます。
ネットワークに関しては、既定の設定ではSBPLはProxyのポート以外へのTCP接続を拒否するため、外部への通信はホスト側のProxyを経由しない限り成立しないことが伺えます。Proxy用の環境変数を無視するツールは、Proxyを迂回するのではなく通信自体が失敗します。
(allow network-bind (local ip "localhost:${httpProxyPort}"))
(allow network-inbound (local ip "localhost:${httpProxyPort}"))
(allow network-outbound (remote ip "localhost:${httpProxyPort}"))
SOCKS用のポートについても同様のルールが生成されますが、HTTPとSOCKS5を1つのポートで受けるProxyを使う場合はポートが同じになるため、上記の3行のみとなります。
ただし、以下の設定を有効にするとProxyを経由しない経路が開くため、注意してください。
allowUnixSockets/allowAllUnixSockets: 指定したUNIXソケット(または全てのUNIXソケット)への接続を許可するallowLocalBinding: ローカルポートへのbindに加え、localhost:*への接続も許可するenableWeakerNetworkIsolation:com.apple.trustd.agentへのアクセスを許可する(READMEでも情報持ち出しの経路になりうると警告されている)allowAppleEvents:openなどで起動したアプリはサンドボックスの外で動作する
ファイルのアクセスについてはSeatbeltの設定が後勝ちで適用されていく性質上、以下のような順にSBPLが生成されるようです。
(allow file-read*): 始めに全て許可する(deny file-read* ...): denyReadに記述したパスを拒否する(allow file-read* ...): allowReadに記述したパスを許可する(deny file-read* ...): 3によって消えてしまったdenyを再設定する
この実装と周辺のコメントから、denyReadのファイルパス内部でallowReadに一致するパスがあればアクセスが許可されること、ただし、allowRead内部を指し示すdenyReadは再度拒否されるようになっていることが読み取れます。
つまり、~/proj で実行した場合、 "allowRead": ["~/proj"], "denyRead": ["~", "**/.env"] という設定では ~/proj へのアクセスは許可しつつ、 ~/proj/.env へのアクセスは拒否するSBPLが生成されるようです。なお、**/.env のような相対パスのglobは、実行したディレクトリを基準に解決されます。
最終的には、設定可能な環境変数を適用して以下のようなコマンドが生成されます。
env HTTP_PROXY=... HTTPS_PROXY=... TMPDIR=/tmp/claude ... \
/usr/bin/sandbox-exec -p '<生成したSBPL>' <bashのパス> -c '<実行したいコマンド>'
<bashのパス> は PATH 上の bash を解決した絶対パスです。cli.ts はこれを spawn(sandboxedCommand, { shell: true }) で実行し、子プロセスが終了するとその終了コードでsrt自身も終了します。ただし、子プロセスがシグナルで終了した場合は、SIGINT/SIGTERMなら 0、それ以外のシグナルなら 1 で終了します。
以上のように実装を眺めることで、Claude Codeで使われているサンドボックス機能の設定項目と、その設定がどのように反映されるかのイメージを掴むことができました。
