この記事はシリーズ「自律運用の土台を 1 本まるごと読む: claude-code-repository-base 全解剖」の第 6 回(全 10 回)です。
Claude Code に毎回同じ指示をしなくて済むように、ルール・フック・スキル・ツールを一式にまとめて公開している自作リポジトリ kai-kou/claude-code-repository-base(MIT)を、作った本人が解説する連載です。設計の意図だけでなく、実際に動かして確かめた結果(自分でも気づいていなかった穴を含む)をそのまま載せます。掲載する実行結果と数値はすべて各回の執筆時点で採取し直し、検証したコミット SHA を各回の冒頭に記します。
自分のリポジトリで再現したい方へ: 同じリポジトリを「どう入れて、どう回して、どう追随するか」の手順書として書いた Zenn Book を公開しています(有料 500 円・試し読みあり)。
シリーズ全体の目次
- 第 1 回 Claude Code に毎回同じ指示をしなくて済むように、運用の土台をリポジトリ 1 本にまとめた
- 第 2 回 git push origin main | tee log で保護が素通りしていたので、コマンド分割で塞ぎ直した
- 第 3 回 ベースを別リポジトリへ配る 2 経路を dry-run で動かす(apply-to-repo.sh と bootstrap.sh)
- 第 4 回 Stop フックを 5 本並べたら最初の 1 本しか読まれなかったので、ルーター 1 本に集約した
- 第 5 回 コンテキスト圧縮で作業が消えるのを、圧縮前後の二段 WIP コミットで防いだ
- 第 6 回 sandbox.enabled を true にしてもクラウドでは bwrap が無く、許可リスト外へ素通りだった(この記事)
- 第 7 回 「確認してよいですか」を 6 種類に限定したら、それ以外は全部自律実行になった
- 第 8 回 ルールと教訓を増やし続けないために、常駐バイト予算と「昇格=物理削除」を機械強制した
- 第 9 回 複数エージェントが同時に書くホワイトボードを、個別ファイル+単一集約者で壊れなくした(公開予定)
- 第 10 回 10 回分を読み終えたら、自分のリポジトリに最初に持ち込む 3 つはどれか(公開予定)
検証時点: base kai-kou/claude-code-repository-base(MIT) HEAD 40551b9(2026-09-23 JST のコミット)
対象読者は、bypassPermissions や auto mode で Claude Code を自律運用しながら「本当に何が守っているのか」を疑っている方です。
はじめに
前回までの 2 回は、git push の物理ブロックや圧縮前後の WIP コミットのように、ローカルでもクラウドでも同じように効くフック を扱いました。今回は逆に、設定ファイルに書いてあっても実行環境によっては効かないもの を扱います。
base の .claude/settings.json には sandbox.enabled: true と、通信してよいドメインの許可リスト(network.allowedDomains)が入っています。これを読むと「Bash コマンドは許可リストの外へは出られない」と受け取りたくなります。ところが、この連載の執筆に使っている Claude Code のクラウド実行環境(Claude Cloud のスケジュール実行と同じ種類のリモートセッション)で試すと、許可リストに無いドメインへ普通に HTTP 200 が返ってきます。
この事実自体は、base のドキュメントに #383 として自分で記録してあったものです。今回はその記録を執筆時点のクラウド実行環境で追試して裏を取り、そのうえで「では sandbox の代わりに何が守っているのか」を、base に書いてある補償統制の 3 層に沿って整理します。
TL;DR
- base の
.claude/settings.jsonはsandbox.enabled: trueで、許可リストは 12 ドメイン。example.comは含まれていません。 - 執筆セッションのクラウド実行環境には Linux 用のサンドボックス実装
bwrap(bubblewrap)が無く、which bwrap/command -v bwrapはどちらもexit=1でした。 - 同じ環境から
curl https://example.comを実行するとHTTP_STATUS:200が返りました。許可リストは効いていません。 - 公式ドキュメントは、依存が欠けてサンドボックスを起動できないときは 警告を出してサンドボックスなしで実行する のが既定だと説明しています。挙動としてはこの既定どおりです。
- クラウドで実際に効いている防御は、①セッションコンテナ自体の隔離 ②
permissions.deny③PreToolUse フック、の 3 層です。sandbox の許可リストはこの 3 層に数えません。
検証した環境と手順
検証はすべて、この記事を書いているセッション自身のクラウド実行環境で行いました。base を scratchpad に clone し、検証時点の HEAD が 40551b9 であることを確認したうえで、次の 3 点を順に見ています。
-
bwrapが存在するか - base の
settings.jsonに何が書いてあるか - 許可リスト外のドメインへ到達できるか
先に断っておくと、3 の到達性は このセッションの実行環境 についての結果です。clone した base の中で完結する self-test ではありません。「base を入れればどこでも同じ結果になる」という主張ではなく、「Claude Code のクラウド実行環境では、base の sandbox 設定がこう振る舞う」という観測です。
1. bwrap が見つからない
$ which bwrap; echo "exit=$?"
exit=1
$ command -v bwrap; echo "exit=$?"
exit=1
どちらの方法でもパスが出力されず、終了コードは 1 でした。Claude Code の公式ドキュメント(Configure the sandboxed Bash tool)によると、Linux と WSL2 では bubblewrap を使って分離を行い、macOS では OS 組み込みの Seatbelt を使います。Linux で bubblewrap が無いということは、サンドボックスを張る道具そのものが無いということです。
2. base の settings.json には許可リストが書いてある
検証時点の .claude/settings.json の 118 行目からが sandbox ブロックです。
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true,
"network": {
"allowedDomains": [
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"slack.com",
"api.slack.com",
"api.anthropic.com",
"mcp.context7.com",
"context7.com",
"api.cloudflare.com",
"registry.npmjs.org",
"pypi.org",
"files.pythonhosted.org"
]
},
許可しているのは GitHub・Slack・Anthropic API・Context7・Cloudflare API・npm / PyPI の 12 ドメインで、example.com は入っていません。enabled: true は、これが無いと以下のサブ設定がすべて無効になる起動スイッチです。base 自身も 2026-08-02 まではこの 1 行が欠けていて、許可リストが丸ごと飾りになっていた時期がありました(#383)。
3. 許可リスト外の example.com に 200 が返る
$ curl -sS -o /dev/null -w "HTTP_STATUS:%{http_code}\n" https://example.com
HTTP_STATUS:200
enabled: true で許可リストも書いてあるのに、リストに無いドメインへ素通りしました。公式ドキュメントには次の記述があります。
By default, if the sandbox cannot start because dependencies are missing or the platform is unsupported, Claude Code shows a warning and runs commands without sandboxing.
依存(bubblewrap)が欠けていればサンドボックスは起動せず、コマンドはサンドボックスなしで実行される、というのが既定の挙動です。今回の結果はこの説明と整合します。設定ファイルの enabled: true は「サンドボックスを使いたい」という意思表示であって、「サンドボックスが動いている」ことの保証ではありません。
著者視点の発見ポイント
「書いてある」と「効いている」は別の話だった
base の docs/rules/sandbox-rules.md には、この状況をそのまま書いてあります。
Claude Code on the web のコンテナ(
CLAUDE_CODE_REMOTE_ENVIRONMENT_TYPE=cloud_default)には Linux 側のサンドボックス実装であるbwrap(bubblewrap)が存在しない(command -v bwrapで確認済み)。そのためenabled: trueを設定してもクラウドでは許可リストは適用されず、example.comへの到達は引き続き成功する(実機確認済み)。
今回の追試でも同じ結果になりました。自分で書いた記録ですが、改めて手を動かすと「設定ファイルを読めば防御の範囲が分かる」という前提がいかに危ういかを実感します。同じファイルの冒頭には、設定を変えたら「必ず許可リスト外ドメインへの到達可否を実機で確認する(設定ファイルの記述だけを根拠にしない)」とも書いてあり、今回の追試はこの一文を自分に適用したものです。
クラウドで通ったことを根拠に、許可リストを緩めない
ここで取れる判断は 2 つあります。「クラウドでは効かないのだから許可リストは要らない、整理しよう」と考えるか、「クラウドでは効かないが、残す」と考えるかです。base は後者を選び、sandbox-rules.md に次のように書きました。
enabled: trueを設定する意義はローカル実行と配布先にある。本リポジトリはapply-baseで下流へ配布されるベースであり、bwrap/ Seatbelt が使えるローカル環境では設定が実際に効く。したがって「クラウドで到達できた」ことを根拠に許可リストを緩めない。検証はローカル環境で行う。
base は第 3 回で見たとおり別のリポジトリへ配る土台なので、配布先がローカル PC で動く可能性があります。そこでは bubblewrap や Seatbelt が使えるので、許可リストは本物の防御になります。クラウドで素通りするという観測は「クラウドでは別の層に頼っている」という意味であって、「許可リストが不要だ」という意味にはならない、という整理です。
同じ理由で、公式ドキュメントにある sandbox.failIfUnavailable: true(サンドボックスが使えないときにセッションの起動自体を失敗させる設定)も採っていません。無人で回るクラウドセッションが全部起動失敗で沈黙し、誰も気づけないまま止まるほうが、運用としては危険だと判断したためです。
この連載を回しているリポジトリには、sandbox キー自体が無い
もう 1 つ、今回の検証で確かめたことがあります。この連載を実際に執筆・公開している筆者のブログ運用リポジトリは、base を apply-base で取り込んだ下流プロジェクトです。その .claude/settings.json を検索しても、sandbox を含む行はありませんでした。
$ grep -n "sandbox" .claude/settings.json
$ grep -n "bypassPermissions" .claude/settings.json
どちらもヒットなしです。このリポジトリの sandbox-rules.md には「.claude/settings.json に sandbox キーは存在しない」と現状が明記されていて、「導入する場合の設計指針」の節があるだけです。主運用がクラウドの無人セッションである以上、入れても効かないため、意図的に採用していません。
つまり base(sandbox あり)と下流(sandbox なし)で設定は違いますが、クラウドで実際に効いている防御は同じ 3 層 です。設定の有無が防御の実態を決めていない、というのが今回いちばん腹落ちした点でした。
クラウドで実際に守っている 3 層
base の docs/rules/security-posture-controls.md は、確認プロンプトを出さない運用を前提に、それを安全にしている補償統制を 1 か所に集めたドキュメントです。冒頭に次の注意を置いています。
クラウド実行環境(Claude Code on the web)には
bwrapが存在せず、サンドボックスは動作しない(実機確認済み)。したがって本リポジトリの主運用であるクラウド無人セッションで実効的な補償統制は §1.1 deny リスト・§1.3 フック・セッションコンテナ自体の隔離 の 3 層であり、sandbox network allowlist をここに数えてはならない。
3 層がそれぞれ何を受け持っているかを図にすると、次のようになります。
実線がその層で止める経路、点線が「その層では止められず、次の層に引き受けてもらう」経路です。最後に全部を受け止めているのが ① のコンテナ隔離で、sandbox の許可リストはクラウドではこの図の中で何も止めていません。
② permissions.deny は cwd 配下しか守らない
検証時点の settings.json の deny は次の 20 件です。
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(**/*.pem)",
"Read(**/*.key)",
"Read(**/*.p12)",
"Read(**/*.pfx)",
"Read(**/*.jks)",
"Read(**/*.keystore)",
"Read(**/*.ppk)",
"Read(**/credentials*)",
"Read(**/id_rsa)",
"Read(**/id_dsa)",
"Read(**/id_ecdsa)",
"Read(**/id_ed25519)",
"Read(**/.aws/**)",
"Read(**/*service-account*.json)",
"Read(**/.git-credentials)",
"Read(**/.netrc)",
"Write(.claude/settings.local.json)",
"Edit(.claude/settings.local.json)"
]
どれも **/ 始まりか相対パスで、プロジェクトディレクトリ(cwd)を起点に解釈されます。~/.ssh/id_rsa や /tmp/foo.pem のような cwd の外にあるファイルは、この deny の射程に入りません。security-posture-controls.md には、この射程を経路ごとに分けた内訳を残してあります。
| 経路 | cwd 内 | cwd 外(例 ~/.ssh/id_rsa / /tmp/foo.pem) |
|---|---|---|
| Read ツール | deny で止まる | 射程外 |
Bash の cat / head 等 |
deny で止まる | 射程外 |
python3 -c "open(...)" 等の任意サブプロセス |
塞げない | 塞げない |
なお、以前書いた「settings.local.jsonは守っていたのに、本体は無防備だった」は、deny 設定そのものが欠けていた穴の話でした。今回は deny が正しく入っている前提で、それでも届かない範囲を何が補っているかを見ています。
③ フックが cwd 外を第 2 層として塞ぐ
deny が届かない cwd 外を受け持つのが、.claude/hooks/pre-tool-use-router.sh の _sensitive_file_access 関数(262 行目から)です。中核の判定はこの部分です。
# 秘密ディレクトリ配下はファイル名を問わず対象(`~/.ssh/**` ・ `~/.aws/**` ・ `~/.gnupg/**`)。
# **ホーム基準・絶対パス・先頭要素のときだけ** 一致させる
if printf '%s' "$_sfa_lower" \
| grep -qE '^([~.]?/)?\.(ssh|aws|gnupg)(/|$)|^[~/][^[:space:]]*/\.(ssh|aws|gnupg)(/|$)'; then
_sfa_hit=0; break
fi
case "$_sfa_base" in
# 解説ドキュメントは対象外
*.md|*.markdown|*.rst|*.adoc|*.html|*.htm) continue ;;
# 鍵・証明書は拡張子で判定
*.pem|*.key|*.p12|*.pfx|*.jks|*.keystore|*.ppk) _sfa_hit=0; break ;;
esac
deny が cwd を起点にパターンを当てるのに対し、フックは Bash コマンドの引数を見て ホーム基準・絶対パス の ~/.ssh / ~/.aws / ~/.gnupg を捕まえます。ブロック時のメッセージにも「permissions.deny は cwd アンカーのため cwd 外を守れず、本フックが第2層を担う」と理由を書いてあります。
このフックで実際に ~/.ssh を読みにいく実演は、今回はしていません。実在する鍵に触る操作を記事のために走らせる理由が無いためで、ここではコードの判定ロジックを示すに留めます。回帰の確認用には tools/test_sensitive_file_guard.sh を base に置いてあります。
① 最後はセッションコンテナの隔離が引き受ける
deny とフックを重ねても、塞ぎきれない経路が残ります。security-posture-controls.md は、python3 -c のような任意のサブプロセス経由の読み取りを「どちらの層でも塞げない恒久的な設計限界」と書いています。本来の答えは sandbox ですが、クラウドでは動きません。そこで残余リスクは、クラウド実行環境のセッションコンテナ自体が破棄前提で隔離されていることに引き受けてもらう、という整理にしています。
フックの側にも限界があります。フックが見るのは Bash コマンドだけで、コード内のコメントにも「ネイティブ Read ツールは permissions.deny のみが守る」と書いてあります。判定は名前ベースなので、.pub の公開鍵を除外している都合上、機密ファイルを .pub にリネームすれば通ります。誤検知で通常運用を止めない側に倒しているぶん、素通りする命名が残ることを前提に読んでください。
未解決の点
1 つ、今回の検証中に気づいて解決できていない食い違いがあります。security-posture-controls.md の「設定の現状」表の 1 行目は permissions.bypassPermissions を true としていますが、検証時点の settings.json を検索した限りでは、bypassPermissions や defaultMode を含む行は見つかりませんでした。bypass を起動フラグ側で有効にしている想定なのか、ドキュメントが古いまま残っているのかは、この回では確かめきれていません。同じドキュメントが「実ファイルに無いパターンを書いておくと『設定済みだから安全』という誤った前提が生まれる」と警告しているのと同じ種類のずれなので、base 側で別途直します。
設定の有無ではなく、どの層が効いているかで考える
今回の検証から言えることは次の 3 点です。
-
sandbox.enabled: trueは意思表示であって、動作の保証ではありません。Linux で bubblewrap が無ければ、既定では警告のうえサンドボックスなしで実行されます。 - クラウドで許可リスト外へ届いたことは、許可リストを緩める根拠になりません。配布先のローカル環境では同じ設定が本物の防御になるからです。
- クラウドでの実効的な防御は、コンテナ隔離・
permissions.deny・PreToolUse フックの 3 層です。deny の cwd 外をフックが補い、両方で塞げない経路をコンテナ隔離が受け止める、という重ね方になっています。
確認プロンプトを減らして自律運用するなら、設定ファイルを読むより先に、自分の実行環境でどの層が本当に効いているかを 1 回確かめておくと判断を誤りにくくなります。which bwrap と許可リスト外への curl の 2 行で済みます。
次回は、フックで物理的に止められない部分をどう運用ルールで扱うか、つまり「確認してよいですか」を 6 種類に限定した確認境界の話をします。今回の ③ フックが、そのルールとどう連動しているかも次回で触れます。
関連記事
- git push origin main | tee log で保護が素通りしていたので、コマンド分割で塞ぎ直した(連載第2回)
- Stop フックを 5 本並べたら最初の 1 本しか読まれなかったので、ルーター 1 本に集約した(連載第4回)
- コンテキスト圧縮で作業が消えるのを、圧縮前後の二段 WIP コミットで防いだ(連載第5回)
参考リンク
-
kai-kou/claude-code-repository-base(MIT・検証時点 HEAD
40551b9) -
.claude/settings.json(sandboxブロックとpermissions.denyの引用元) -
docs/rules/sandbox-rules.md(#383 の実機確認記録と運用判断の引用元) -
docs/rules/security-posture-controls.md(補償統制 3 層の引用元) -
.claude/hooks/pre-tool-use-router.sh(_sensitive_file_accessの引用元) - Claude Code 公式ドキュメント: Configure the sandboxed Bash tool
- bubblewrap(containers/bubblewrap)
シリーズの前後の記事
- ⬅️ 前の記事: 第 5 回 コンテキスト圧縮で作業が消えるのを、圧縮前後の二段 WIP コミットで防いだ
- ➡️ 次の記事: 第 7 回 「確認してよいですか」を 6 種類に限定したら、それ以外は全部自律実行になった
自分のリポジトリで再現したい方は、同じリポジトリを「どう入れて、どう回して、どう追随するか」の手順書として書いた Zenn Book(有料 500 円・試し読みあり)へどうぞ。