Claude Codeのsandbox.excludedCommandsが効かない——リストに書いたコマンドがまだサンドボックスの中にいる

Claude Codeのsandbox.excludedCommandsが効かない——リストに書いたコマンドがまだサンドボックスの中にいる

Claude Codeのsandbox.excludedCommandsにdocker *やgh *を登録しても、ps/pgrepやgit -Cなど一部のコマンドは依然サンドボックス内で失敗する。GitHub Issue群と公式settings-referenceを突き合わせて原因パターンを整理し、実務での回避策をまとめた。

エンジニアのゆとです。

settings.jsonのsandbox.excludedCommandsに"docker *"や"gh *"を書いたのに、そのコマンドがまだサンドボックスの中で失敗する——ここ数週間、Claude CodeのGitHub Issueにこの手の報告が立て続けに増えている。

"sandbox": {
  "enabled": true,
  "excludedCommands": ["docker *", "gh *", "ps *", "pgrep *"]
}

こう書いておけば、公式ドキュメントが「Go製CLIはSeatbelt配下でTLS検証に失敗するからexcludedCommandsに入れろ」と案内している通りに動くはずだ。ところが実際にはps -p 1がoperation not permitted: psで落ち、gh pr viewがtls: failed to verify certificateで落ちる。dangerouslyDisableSandbox: trueを付けて同じコマンドを実行すると普通に成功するので、コマンド自体やネットワークの問題ではない。excludedCommandsというリストそのものが、書いた通りに機能していない。

自分もsettings.jsonでサンドボックスの穴を塞ぐ作業をここ数ヶ月ちょこちょこやっていて、このexcludedCommandsだけは何度か「あれ、効いてないな」と首をひねった記憶がある。ちょうどGitHub Issue側でも同じ違和感を持った人たちが技術的な検証を積み上げていたので、今回はその一次情報を整理しておく。

まず結論——「仕様」と「バグ」が両方混ざっている

先に見取り図を出す。excludedCommandsが期待通り動かないケースは、大きく2種類に分かれる。

  1. 公式ドキュメント通りの仕様だが、直感に反する挙動(複合コマンドの一部が一致すると全体が対象外になる、その裏返しの過剰修正で単一コマンドまで弾かれる)
  2. 単純に実装がバグっている(リダイレクトを付けただけで除外が外れる、コマンド名が偶然ビルトインと衝突して無視される)

どちらも記事執筆時点(2026年9月21日、Claude Code 2.1.278)でオープンのまま残っている。GitHub Issue #95813では、ps・pgrep・ghをexcludedCommandsに登録しているのに全滅する再現報告が上がっていて、dangerouslyDisableSandbox: trueでの同一コマンドは成功するという切り分けまで示されている。

github.com
sandbox.excludedCommands has no effect: listed commands still run sandboxed · Issue #95813 ps/pgrep/ghがexcludedCommandsに登録済みでもサンドボックス内で失敗する再現報告。dangerouslyDisableSandboxでは成功することを比較して切り分けている。

公式仕様のおさらい——「一部が一致したら全体が対象外」というルール

まず公式のsettings-referenceが何と書いているかを確認しておく。

Each entry uses the same syntax as the content of a Bash(...) permission rule: an exact command, a prefix such as docker *, or a wildcard pattern. When any part of a compound command matches an entry, Claude Code runs the whole command unsandboxed.

要点はこうだ。

  • 書き方は完全一致・docker *のようなプレフィックス・ワイルドカードパターンのいずれか
  • 複合コマンドの「どこか一部」が一致すれば、コマンド全体がサンドボックス外で動く

この「一部が一致したら全体」という仕様、実は過去に2段階で問題を起こしている。まず「緩すぎる」側の問題として報告されたのがIssue #81157だ。

{
  "sandbox": {
    "enabled": true,
    "excludedCommands": ["git *"],
    "filesystem": { "denyRead": ["~/.ssh"] }
  }
}

この設定でcat ~/.ssh/id_ed25519.pub && git statusのような複合コマンドを実行すると、gitの部分がexcludedCommandsに一致した瞬間、先に実行されるcatまで含めてコマンド全体がサンドボックス外で動いてしまう。denyReadで塞いだはずの~/.sshが、gitをこっそり混ぜるだけで読めてしまうことになる。報告者は「実行順序的にはcatが先に走って成功しているので、サンドボックス判定はコマンド文字列全体に対して事前に行われている」という技術的な裏付けまで示していた。

修正が入ったら今度は「厳しすぎる」側の問題が出た

このバグは2.1.277で公式に修正されている。CHANGELOGにはこうある。

Fixed a sandbox.excludedCommands glob exempting an entire compound Bash command from the sandbox when only one part matched; every part must now match

「一部が一致したら全体」から「全部の部分が一致しないとダメ」に変わった。これ自体はセキュリティ上正しい修正なんだけど、この変更がいくつかの副作用を連れてきた。

副作用1: サブコマンド前のフラグが「別の部分」として扱われる(Issue #95455)

excludedCommands: ["git *"]を設定した状態で、gitの後にフラグを挟むパターンを比較した表がIssueに載っている。

コマンド2.1.2762.1.277
git status --porcelain除外される除外される
git --no-pager status --porcelain除外される除外される
git -C . status --porcelain除外されるサンドボックス化される
git -c core.pager=cat status --porcelain除外されるサンドボックス化される
git --git-dir=<path>/.git --work-tree=<path> status --porcelain除外されるサンドボックス化される

git -C . statusは単一コマンドで、パイプも連結もない。それでもgit *という文字列パターンの機械的な一致判定に引っかからず、サンドボックス内で実行される。報告者が指摘している一番厄介な影響はここだ。

git statusが「エラーで落ちる」のではなく「もっともらしい間違った結果を返す」。サンドボックスがマスクしているホームディレクトリのドットファイルなどが、通常の未追跡ファイルとしてgit statusの出力に紛れ込む。

コマンドが失敗してくれれば気づけるが、git statusは終了コード0で「それっぽい」結果を返してしまう。読み取り専用のはずのコマンドが、サンドボックスの境界のせいで嘘の出力を返す可能性がある、という話だ。

副作用2: リダイレクトを付けただけで除外が外れる(Issue #95532)

同じ「every part must match」ルールの下で、ghコマンドにリダイレクトを付けるかどうかを比較した検証もある。

コマンドサンドボックス外で動く?
gh api user --jq .login動く
cd /repo && gh api user --jq .login動く
gh api user --jq .login 2>&1動く
gh api user --jq .login > "$TMPDIR/out.txt"動かない
gh api user --jq .login > /dev/null動かない

ファイルディスクリプタの複製(2>&1)は許容されるのに、/dev/nullを含めてファイルへのリダイレクトを付けた瞬間に除外扱いが外れる。報告者いわく「リダイレクトはコマンドではないはずなのに、every partの判定ではリダイレクトも1つの部品として数えられている」。

これが特に地味に効くのは、ghの出力をパイプできない(パイプも別部品扱いで弾かれる)ので、構造化データを取り出す手段が--jqオプションに限られてしまう点だ。JSONをファイルに書き出して後続コマンドで処理する、という自然なワークアラウンドも同じ理由で通らない。

副作用3: コマンド名がシェルビルトインと衝突すると無視される(Issue #95560)

もう一つ、より狭いケースだが実務で刺さる人がいそうなバグもある。excludedCommandsに絶対パスの実行ファイルを登録していても、そのファイル名がreadだと2.1.277以降マッチしなくなる。

"excludedCommands": ["/path/to/bin/read *", "/path/to/bin/write *"]

同じ設定でwriteという名前のコマンドは問題なく除外される一方、readという名前のコマンドだけがサンドボックス内に取り残される。readはシェルの組み込みコマンド名でもあるため、絶対パス指定であってもマッチング処理のどこかでreadビルトインへのガード処理に巻き込まれているのではないか、というのが報告者の見立てだ。社内ツールにread/writeのような一般的な単語をコマンド名として使っている場合は、このパターンを疑う価値がある。

それとは別に、そもそも登録が反映されないケースもある(Issue #92445)

ここまでは「一致判定のルールが厳しすぎる/緩すぎる」系の話だったが、もっと単純に「設定ファイルに書いた内容が効いていない」ように見える報告もある。

{
  "sandbox": {
    "enabled": true,
    "excludedCommands": ["codeassembly *", "gh *"],
    "filesystem": {
      "allowWrite": ["~/repos/vaults/*-kb", "~/repos/vaults/*-kb.*"]
    }
  }
}

allowWriteにワイルドカード付きのパスを登録しているのにtouch ~/repos/vaults/coding-kb/probeがOperation not permittedで拒否される一方、ワイルドカードなしの通常パスなら通る、という報告。加えて同じセッション内でgh pr createが最初はtls: failed to verify certificateで失敗し、10分後に設定を何も変えていないのに成功するようになった、という再現性の低い挙動も併記されている。こちらは原因がまだ特定できておらず、報告者自身も「フィルタリングプロキシがグラフAPIのパスだけ弾いていたのでは」という仮説止まりで終わっている。

再現性の高いバグ(#95813、#95455、#95532、#95560)と、再現性の低い断続的な不具合(#92445)は分けて考えたほうがいい。前者は設定を工夫しても避けられない類のバグ、後者は環境固有のネットワーク要因が絡んでいる可能性がある。

自分の環境で切り分ける手順

excludedCommandsが効いていないと感じたら、まずこの順番で切り分けるといい。

  1. claude --versionで現在のバージョンを確認する。2.1.277以降かどうかで、当てはまる可能性のあるバグの範囲が変わる
  2. 疑わしいコマンドをdangerouslyDisableSandbox: true付きで一度実行してみる。ここで成功するなら、コマンド自体・ネットワーク・権限の問題ではなくexcludedCommandsのマッチングの問題だと確定できる
  3. 同じコマンドを、フラグなし・リダイレクトなし・複合コマンドなしの最小形にして試す。最小形で通るなら、フラグやリダイレクトが「別の部品」として扱われて弾かれている可能性が高い
  4. excludedCommandsに登録している文字列と、実際にClaudeが実行しようとしているコマンド文字列を見比べる。gitだけの完全一致指定はgitという0引数の文字列にしかマッチしないので、実質的に何も除外しない(これも#81157で指摘されている落とし穴)

この4ステップで大体、「そもそも設定の書き方が間違っている」のか「設定は正しいのに実装側のバグに当たっている」のかを切り分けられる。

今できる回避策

修正を待つ間に取れる現実的な手はいくつかある。優先度順に並べる。

1. 公式が推奨する通り、まずfilesystem.allowWriteを検討する

そもそも公式ドキュメントのsettings-referenceにはこう書いてある。

Exclusion is a convenience, not a security boundary: prefer filesystem.allowWrite when a tool only needs to write somewhere specific.

excludedCommandsは最初から「セキュリティ境界」ではなく「利便性のための機能」だと明言されている。特定のパスへの書き込みだけが必要なら、コマンドごとサンドボックス外に出すよりsandbox.filesystem.allowWriteで個別にパスを許可するほうが、意図しない副作用(今回挙げたような一部一致・厳格一致の揺れ)を踏みにくい。

Claude Codeサンドボックス、ファイルと通信を別々に緩める設定が来た
Claude Codeサンドボックス、ファイルと通信を別々に緩める設定が来たClaude Code v2.1.216とv2.1.219で追加されたsandbox.filesystem.disabledとsandbox.network.strictAllowlistを検証。ファイル隔離だけ外す設定と、許可リスト外の通信を黙って弾く設定をsettings.json実例つきで解説する。読む →

2. TLS検証系の失敗はenableWeakerNetworkIsolationも選択肢に入れる

gh・gcloud・terraformのようなGo製CLIがTLS検証で落ちる件について、公式トラブルシューティングは「MITMプロキシ(httpProxyPort)を使っているならenableWeakerNetworkIsolationを、使っていないならexcludedCommandsを」と案内している。excludedCommands側で今回のようなバグを踏み続けるなら、ネットワーク構成次第ではこちらのほうが安定する可能性がある。

3. コマンドは可能な限り「フラグなし・パイプなし・単発」で組む

git -C . statusのようにサブコマンドの前にフラグを挟む形は避けて、作業ディレクトリを先にcdしてからgit statusを単発で呼ぶ、といった形に寄せると、every part must matchルールに引っかかりにくくなる。リダイレクトも同様で、出力をファイルに落とさず標準出力のまま扱えないか検討する。根本解決ではないが、修正を待つ間の一番手軽な緩和策ではある。

4. 深刻な影響が出ている場合はIssueに追記して監視する

特にgit statusが「エラーにならず間違った結果を返す」#95455のケースは、気づかないまま作業を続けるリスクがある。該当バージョンを使っている場合は、Issueをウォッチしつつ、git statusの出力を鵜呑みにせずgit status --porcelainの結果に見慣れないパスが混じっていないか一度目視するくらいの警戒はしておいたほうがいい。

Claude Codeサンドボックスに来た「マスク」機能、実際に秘密鍵を隠せるか試した
Claude Codeサンドボックスに来た「マスク」機能、実際に秘密鍵を隠せるか試した2026年8月4日追加のsandbox.credentials mask機能を検証。環境変数がコマンドには偽の値、外部リクエストには本物の値として届く仕組みをhttpbin.orgへの実リクエストで確認した。tlsTerminate必須の理由とmacOS/Linuxの挙動差も解説する。読む →

似た「権限・サンドボックス系の回帰」と混同しないために

Claude Codeの権限・サンドボックス周りはアップデート頻度が高く、似たタイミングで別の回帰が起きることも多い。読み取り専用のgitコマンドが長時間セッション後に急に許可を求めてくる別の回帰も、つい最近扱ったばかりだ。

長時間セッションでgit statusが急に許可を求めてくる——Claude Code 2.1.269の権限プロンプト回帰を追った
長時間セッションでgit statusが急に許可を求めてくる——Claude Code 2.1.269の権限プロンプト回帰を追ったClaude Code 2.1.269で、読み取り専用のgitコマンドがセッション経過後に突然許可を求める回帰バグが発生。公式リリースノートで確認した原因の見立てと、2.1.270での修正内容、影響を受けているか確認する手順を整理した。読む →

「サンドボックスの中で失敗する」のか「サンドボックスの外でそもそも権限プロンプトが出る」のかで原因の層が違うので、症状を切り分けてから対処法を探したほうが早い。

FAQ

excludedCommandsに書けば絶対安全に動きますか?

いいえ。公式ドキュメント自身が「利便性のための機能であってセキュリティ境界ではない」と明言している。加えて記事で紹介した通り、2.1.277以降は「複合コマンドの全部の部品が一致しないと除外されない」というルールに変わっており、フラグやリダイレクトの付け方次第で除外が効かないことがある。書いたら終わりではなく、実際に動くところまで確認する必要がある。

"git"と"git *"、どちらで書くべきですか?

用途次第だが、"git"という完全一致指定は実質的に何もしない。Issue #81157が指摘している通り、これは「引数なしのgitという文字列」にしかマッチせず、git statusのような実際の呼び出しには一致しない。ワイルドカード指定の"git *"を使うのが公式の例でも示されている書き方だが、その分「一部が一致すれば」系の副作用も引き受けることになる。

バグが直るまでどう運用するのが現実的ですか?

excludedCommandsに頼り切らず、書き込み先が決まっているツールはfilesystem.allowWriteで個別に許可し、TLS検証だけが問題ならネットワーク側の設定を見直す、というように役割を分散させておくのが無難だと思う。excludedCommandsはあくまで「最後の手段」くらいの位置づけで扱ったほうが、今回のような一致判定の揺れに振り回されにくい。

まとめ

sandbox.excludedCommandsが効かないという報告の中身は一枚岩ではなく、「複合コマンドの一部一致で全体が対象外になっていた仕様上のセキュリティホール」(#81157)、その修正の副作用としての「フラグ・リダイレクトを含む単一コマンドまで弾かれる回帰」(#95455、#95532)、「コマンド名がビルトインと衝突して無視されるバグ」(#95560)、「設定自体が反映されない断続的な不具合」(#92445)という、性質の違う複数の問題が積み重なっている。

記事執筆時点(2026年9月21日、Claude Code 2.1.278)でこれらはまだオープンのままだ。excludedCommandsは公式にも「セキュリティ境界ではなく利便性のための機能」と位置づけられているので、書き込み権限はfilesystem.allowWriteで、TLS周りはネットワーク設定で、というように役割を分けておくのが、今のところ一番安定する運用だと思う。

github.com
excludedCommands 'every part must match' also drops single commands carrying a pre-subcommand flag · Issue #95455 git -C . statusのような単一コマンドが2.1.277以降サンドボックス化され、git statusが間違った結果を返すようになった再現報告。
← 記事一覧に戻る