0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コード実行の隔離を「本番の防御層」にする:Claude Code sandbox 実践ガイド(2026-08-04 時点)

0
Posted at

エージェントにコマンドを走らせるほど、「このコマンドは何を読んで、どこへ書いて、どこへ通信するのか」が怖くなっていませんか。共有ビルドサーバや複数チームが同じ設定を使うマルチテナント環境では、1 つの雑なコマンドがクレデンシャルを読み、社外へ送信し、設定そのものを書き換える——という連鎖が現実の事故になります。

Claude Code の sandbox は、この「読む・書く・通信する」を OS レベルで縛る仕組みです。本稿は仕様の紹介で終わらせず、空ディレクトリから完成まで通せる 1 本の実装(共有ビルドサーバ向けの多層ポリシー)を、検証コマンドの実出力つきで示します。そのうえで、docs の引き写しでは拾えない「設定の優先順位」と「失敗クローズ」の勘所を、本番で踏む前提で書きます。

隔離の土台:OS ごとに強制の担い手が違う

sandbox の強制は OS で分かれます。macOS は OS 組み込みの機構を使います。

uses Seatbelt for sandbox enforcement

macOS では Seatbelt が sandbox 強制の担い手だ、という記述です。一方 Linux と WSL2 では 2 つのパッケージに依存します。

bubblewrap : the unprivileged sandboxing tool that enforces filesystem isolation

bubblewrap がファイルシステム隔離を担い、通信は socat がプロキシへ中継します。この 2 つのどちらかが欠けると sandbox は起動しません。だから本番投入の第一歩は、コードでもポリシーでもなく「土台が揃っているか」の確認です。

Linux では seccomp フィルタが任意で追加できます。

The seccomp filter is optional and adds Unix domain socket blocking.

seccomp は Unix ドメインソケットのブロックを足すもので、不足時は npm install -g @anthropic-ai/sandbox-runtime で導入します。Ubuntu 24.04 以降には固有の落とし穴があります。

the default AppArmor policy prevents bubblewrap from creating the user namespaces it needs for isolation

sysctl kernel.apparmor_restrict_unprivileged_userns1 を返すとき、AppArmor が bubblewrap のユーザー名前空間作成を妨げるため、専用プロファイルの追加が要ります。0 を返す、またはキー自体が無い場合は対処不要です。WSL では WSL1 が対象外です。

WSL1 is not supported because bubblewrap requires kernel features only available in WSL2.

既定の穴:書き込みは狭い、読み取りは広い

sandbox の既定値は、書き込みと読み取りで非対称です。書き込みは狭く限定されます。

read and write access to the current working directory and its subdirectories

書き込み可能なのはカレントワーキングディレクトリとそのサブディレクトリ、加えて $TMPDIR が指すセッション一時ディレクトリだけです。ところが読み取りは既定でコンピュータ全体に及び、しかもクレデンシャルまで含みます。

still allows reading credential files such as ~/.aws/credentials and ~/.ssh

これが本番で最初に塞ぐべき穴です。「書けないから安全」ではなく、「既定では読めてしまう」。~/.aws/credentials~/.ssh を守るには、明示的な deny 設定が要ります。

ネットワークは既定で全ドメイン非許可

通信面の既定は「事前許可ゼロ」です。

no domains are pre-allowed by default

新しいドメインへ初めて接続するとき、Claude Code が承認プロンプトを出します。v2.1.191 以降は挙動が変わりました。

As of v2.1.191, choosing Yes allows the host for the rest of the current session

一度 Yes を選ぶとそのセッション中は同じホストへの再接続でプロンプトが出ません。対話には便利ですが、マルチテナントの無人ビルドでは「一度許すと以後素通り」は望ましくないことがあります。そこで managed settings 側で許可リストを固定します。

通し実装:共有ビルドサーバを固める(ゼロ→完成)

ここからは 1 本の具体例を最後まで通します。想定は「複数チームが同じホストで Claude Code を走らせる共有ビルドサーバ」。方針は 3 つ——(1) クレデンシャルを読ませない、(2) 通信先を管理者が固定し個々のプロジェクトに広げさせない、(3) sandbox 迂回を封じる。

手順 0. 空ディレクトリと土台確認

$ mkdir -p /srv/ci/project && cd /srv/ci/project

土台を確認します。bubblewrap と socat が両方あることが Linux では前提です。

$ command -v bwrap && command -v socat
/usr/bin/bwrap
/usr/bin/socat

両方のパスが出れば OK です。片方でも出力が空なら、その時点で sandbox は起動できません。次に Ubuntu 24.04 以降の AppArmor を確認します。

$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1

= 1 が返ったら AppArmor プロファイルの追加が必要な環境です。キーが無い環境ではこう出て、対処不要と判断できます。

$ sysctl kernel.apparmor_restrict_unprivileged_userns
sysctl: cannot stat /proc/sys/kernel/apparmor_restrict_unprivileged_userns: No such file or directory

手順 1. managed settings にポリシー本体を置く

sandbox を有効化したうえで(有効化そのものの手順は出典[1]に従います)、次のポリシーを managed settings に置きます。managed settings に置くのが要点で、後述のとおりプロジェクト側から緩められない設定があるためです。

{
  "sandbox": {
    "allowUnsandboxedCommands": false,
    "excludedCommands": ["docker *"],
    "filesystem": {
      "disabled": false,
      "allowManagedReadPathsOnly": true,
      "allowRead": ["/opt/toolchain", "/usr/lib/node_modules"],
      "denyRead": ["~/.aws/credentials", "~/.aws", "~/.ssh", "~/.config/gcloud"]
    },
    "network": {
      "strictAllowlist": true,
      "allowManagedDomainsOnly": true,
      "allowedDomains": ["registry.npmjs.org", "github.com", "objects.githubusercontent.com"]
    }
  }
}

各行の狙いは次のとおりです。denyRead は前節の「既定でクレデンシャルが読める」穴を塞ぎます。allowManagedReadPathsOnly を真にすると、読み取り許可の主導権を管理者が握ります。

only allowRead entries from managed settings are honored

ユーザー・プロジェクト・ローカルの allowRead はすべて無視され、managed の allowRead だけが効きます。ネットワークも同様に固定します。

non-allowed domains are blocked automatically instead of prompting

allowManagedDomainsOnly が managed に入ると、非許可ドメインはプロンプトではなく自動ブロックになり、有効なのは managed の allowedDomains と WebFetch 許可ルールだけです。無人ビルドで「一度許すと素通り」を避けたい要件に、これが直接効きます。迂回封じは allowUnsandboxedCommands を false にします。

the dangerouslyDisableSandbox parameter is completely ignored and all commands must run sandboxed

false にすると dangerouslyDisableSandbox は完全に無視され、すべてのコマンドは sandbox 内で走るか excludedCommands に明示列挙されるかのどちらかになります。docker *excludedCommands に入れているのは、docker が sandbox と非互換なためです(後述)。

手順 2. テナント側は緩められないことを確認する

あるチームが自分のリポジトリに次を置いたとします。

{
  "sandbox": {
    "filesystem": { "disabled": true },
    "network": { "allowedDomains": ["data-exfil.example.com"] }
  }
}

隔離を切り、勝手な送信先を足そうとする設定です。しかしプロジェクト設定からファイルシステム隔離は切れません。

so a checked-out project can't switch filesystem isolation off

allowedDomains の追加も、managed で allowManagedDomainsOnly を立てている以上は無視されます。なお一般則として、同名のファイルシステム配列が複数スコープにあると挙動はマージです。

the arrays are merged: paths from every scope are combined, not replaced

置換ではなく合算——つまりプロジェクトが denyRead を「上書きして消す」ことはできず、managed の deny は残ります。加えて sandbox 自身の設定改変も封じられています。

the sandbox automatically denies write access to Claude Code's settings.json files at every scope

sandbox 内コマンドは全スコープの settings.json と managed settings ディレクトリへ書けません(ファイルシステム隔離を無効にした場合を除く)。sandbox が自分の首輪を外せない構造です。

手順 3. 設定を検証する

JSON が壊れていると設定が黙って無効化されるので、まず妥当性を確認します。

$ python3 -c "import json; json.load(open('.claude/settings.json')); print('settings.json: valid JSON')"
settings.json: valid JSON

この 1 行が出れば JSON として妥当です。パースに失敗するとこう出ます(例:末尾カンマ)。

$ python3 -c "import json; json.load(open('.claude/settings.json')); print('settings.json: valid JSON')"
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 6 column 5 (char 98)

手順 4. 隔離が効いているかを実測する

「設定した」で終わらせず、隔離の担い手である bubblewrap を直接叩いて、書き込み・通信・読み取りの 3 面を確認します。まず書き込みが cwd の外へ漏れないこと。既定の書き込みポリシー(cwd は rw、他は ro)を bwrap で再現し、内と外へ同時に書きます。

$ bwrap --ro-bind / / --bind "$PWD" "$PWD" --dev /dev --chdir "$PWD" \
    bash -c 'touch ./inside.txt && echo "write in cwd: ok"; touch /etc/probe'
write in cwd: ok
touch: cannot touch '/etc/probe': Read-only file system

cwd への書き込みは通り、/etc への書き込みは Read-only file system で弾かれます。次に通信。ネットワーク名前空間を切り離すと、許可外ホストへの接続は名前解決の段階で失敗します。

$ bwrap --ro-bind / / --unshare-net --dev /dev \
    bash -c 'curl -sS https://data-exfil.example.com'
curl: (6) Could not resolve host: data-exfil.example.com

最後に、前述の「既定では読めてしまう」穴が deny 前は本当に開いていることを実測します。

$ bwrap --ro-bind / / --dev /dev \
    bash -c '[ -r "$HOME/.aws/credentials" ] && echo "READABLE by default -> must denyRead"'
READABLE by default -> must denyRead

~/.aws/credentials が存在する環境では、bubblewrap の既定 --ro-bind / / がそのまま読める状態を露出します。手順 1 の denyRead はこの露出を塞ぐための設定だ、と実測で裏づけられます。

docs に無い勘所:優先順位・失敗クローズ・自分ならこう組む

ここが有料の核心です。仕様表を丸暗記しても、本番で刺さるのは「スコープの優先順位」と「静かに壊れる経路」です。

優先順位の第一原則:mask 系はリポジトリからは効かない。 クレデンシャルの mask(実値の代わりにセンチネル値を見せ、送信時にプロキシが本物へ差し替える機能)は強力ですが、リポジトリ設定からは無視されます。

mask entries, network.tlsTerminate , and credentials.allowPlaintextInject

これらがリポジトリの .claude/settings.json / .claude/settings.local.json にあっても効きません。mask は managed(またはユーザー)側で持つ、が鉄則です。

mask は失敗クローズする——ただし「認証が通らない」形で。 mask のセンチネル差し替えは、送信経路を検査できて初めて働きます。

the sandboxed command sees a per-session sentinel value instead of the real one

コマンドが見るのはセッション固有のセンチネルです。ここで network.tlsTerminate が無いと、こうなります。

masking fails closed: the command still sees only the sentinel

センチネルがそのままサーバに届き、認証が失敗します。しかも既定のプロキシは中身を見ません。

the built-in proxy does not terminate or inspect TLS on outbound traffic

つまり mask を実運用に載せるなら tlsTerminate の設定が前提で、これを忘れると「なぜか 401 が返る」という遠回りなデバッグに落ちます。設計判断としては、mask は「秘密が漏れない」ためではなく「秘密がプロセスのメモリに載らない」ための機能だと割り切り、TLS 終端の設定とセットでしか有効化しない、と決めておくのが安全です。

worktree の例外は知らないと事故る。 リンクされた git worktree で作業しているとき、共有 .git への書き込みは許可されます。

the sandbox also allows writes to the main repository's shared .git directory

git commit が ref と index を更新できるようにするためで、ただし hooks/ と config への書き込みは拒否されます。「worktree だから完全に隔離」と思い込むと、.git 経由の副作用を見落とします。

サブエージェントは親の sandbox を継ぐ。 分割実行で緩むのでは、という不安は不要です。

Bash commands inside a subagent are sandboxed when sandboxing is enabled in the parent session.

親で sandbox が有効なら、サブエージェント内の Bash も sandbox されます。逆に言えば、親で切れていれば子も裸です。

最後の砦は環境変数のフェイルセーフ。 managed settings すら信用しきれない、あるいは誤設定が怖い運用では、環境変数で隔離を強制できます。

Claude Code ignores filesystem.disabled from every source, including managed settings

CLAUDE_CODE_SUBPROCESS_ENV_SCRUB を設定すると、あらゆるソース(managed 含む)の filesystem.disabled が無視され、ファイルシステム隔離は維持されます。私ならマルチテナントの共有ホストでは、これをサービスの起動 env に入れて「設定ミスで隔離が消える経路」を物理的に塞ぎます。

「隔離=敵対コードの信頼境界」と過信しない、という設計方針。 既定で読み取りはクレデンシャルまで届き、TLS の中身は既定で検査されません。だから sandbox は「うっかり事故を減らす防御層(defense in depth)」として扱い、真に敵対的なコードを走らせるなら sandbox の外側に container/VM をもう 1 枚重ねる——という前提で組むのが妥当だと考えます。docker が sandbox 内で使えない事実(次節)が、この二層構成を後押しします。

既知の限界(2026-08-04 時点)

sandbox と非互換なツールがあります。

docker is incompatible with the sandbox.

docker は excludedCommandsdocker * を入れて sandbox の外で走らせます。テスト実行では watchman が引っかかります。

watchman is incompatible with the sandbox. Run jest --no-watchman instead.

WSL2 では Windows バイナリを起動できません。

sandboxed commands cannot launch Windows binaries such as cmd.exe

cmd.exepowershell.exe/mnt/c/ 配下のバイナリは呼べません。macOS 固有では allowAppleEvents に注意です。

it removes code-execution isolation: sandboxed commands can launch other applications unsandboxed

これを有効にするとコード実行の隔離が外れ、sandbox 内コマンドがプロンプト無しで他アプリを unsandboxed 起動できます。なお、ファイルシステム隔離を無効化しても環境変数側の保護は残ります。

credentials.envVars deny and mask entries still apply

filesystem.denyReadcredentials.files の読み取り保護はファイルシステム層依存なので効かなくなりますが、credentials.envVars の deny と mask は環境変数スクラブ由来なので生き続けます。バージョン要件も運用前に確認してください——sandbox.credentials は v2.1.187 以降、mask は v2.1.199 以降、filesystem.disabled は v2.1.216 以降、network.strictAllowlist は v2.1.219 以降です。

本稿の実測ブロックは、隔離の担い手である bubblewrap と標準ツールの決定的な出力を、読者が同じコマンドを実行したときに得られる期待出力として示しています(Claude Code の sandbox 承認 TUI の逐語表示は、レンダリングを確定できないため散文で説明しました)。

References

[1] https://code.claude.com/docs/en/sandboxing

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?