Skip to content

feat(claude): ループバックを sandbox 内で許可し承認プロンプトを削減する - #81

Merged
tunepolo merged 3 commits into
mainfrom
feat/claude-fewer-permission-prompts
Sep 3, 2026
Merged

feat(claude): ループバックを sandbox 内で許可し承認プロンプトを削減する#81
tunepolo merged 3 commits into
mainfrom
feat/claude-fewer-permission-prompts

Conversation

@tunepolo

@tunepolo tunepolo commented Sep 3, 2026

Copy link
Copy Markdown
Owner

背景

直近8日の全セッション(377 ファイル / Bash 呼び出し 10,635 件)を集計した。承認プロンプトは週約 2,400 回、1日あたり約 350 回発生していた。

発生源は3つで、いずれも許可リストの不足ではないsandbox.autoAllowBashIfSandboxed: true があるため、サンドボックス内で完結するコマンドは既に無条件で自動許可されている。Bash(npm run *)Bash(npx *) を allow に足しても何も変わらない。

経路 週間件数 仕組み
dangerouslyDisableSandbox: true 1,941 bash-guard.sh が無条件に ask を返す(設計どおり)
excludedCommandsdocker * 約 440 常にサンドボックス外で走るのに allow ルールが無い
③ MCP 読み取り系ツール 約 60 許可リスト未登録

① ローカル TCP の遮断が最大の原因(週 926 回、①の 47%)

サンドボックス外実行 1,941 件を分類した内訳。

理由 件数 割合
ループバック接続 / ローカル bind 926 47%
その場限りのスクリプト等(長い裾) 696 35%
git / gh(allow 済みなのに escape を付けていた) 276 14%
外部ネットワーク curl 25 1%

サンドボックス内で実測して原因を確定した。

connect 127.0.0.1:5432  -> EPERM (Operation not permitted)
bind/listen 127.0.0.1:0 -> EPERM (Operation not permitted)

npm run test:integration(Postgres 5432)、npm run test:e2e と Playwright の webServernpm run db:seedcurl http://localhost:3000(週 107 回)、Go の httptest がこれで落ち、そのたびに昇格が必要になっていた。

sandbox.network.allowLocalBinding: true を入れると、seatbelt プロファイルに次が出る。

(allow network-bind    (local  ip "*:*"))
(allow network-inbound (local  ip "*:*"))
(allow network-outbound (remote ip "localhost:*"))

外部ネットワークの制限は一切変わらない。緩むのはループバックだけである。

② docker は読み取り専用のみ allow(週約 40 回)

docker *excludedCommands にあるため常にサンドボックス外で走るが、allow ルールが無いので毎回プロンプトが出ていた。git *gh * には既にルールがある。

読み取り専用のサブコマンドだけを追加した。docker exec(週 243 回)と docker compose exec(週 128 回)はコンテナ内で任意コマンドを実行できるため、意図して承認を維持している

③ MCP 読み取り系(週約 58 回)

週3回以上使っているものだけを追加した。notion-search(1) などは見送っている。

説明不足のまま承認を求める問題

承認ダイアログに出るのは description だけだが、ヒアドキュメントで渡す長いスクリプトの説明が無く、ユーザが可否を判断できない状態だった。

bash-guard.sh に、承認を要する経路に限って description が 80 バイト未満なら deny を返す門を追加した。

ask ではなく deny にしたのは、deny の理由だけがモデルに返るためである。説明を書き直して再提示させられる。ask だと承認ダイアログをユーザに丸投げすることになる。

サンドボックス内で完結するコマンドはプロンプトが出ないので対象外にしてある。通常の作業に摩擦は増えない。

テスト

bash -n に加えて、フックへペイロードを直接渡して 11 パターンの判定を確認した(適用前の候補ファイルと、インストール後の実ファイルの両方で実施)。

入力 期待 結果
escape 付き・説明 15 バイト deny
escape 付き・説明が十分 ask(説明文を理由に同梱)
npm run test:integration(サンドボックス内) 素通り
docker exec … psql・説明 8 バイト deny
docker exec … psql・説明が十分 素通り→通常の承認へ
docker ps / docker compose ps 素通り
docker compose up -d・説明が短い deny
force push deny(従来どおり)
heredoc 本文の force push 誤検知しない(従来どおり)
git add に escape・説明が短い deny

適用後に実環境で再測定した。

connect 127.0.0.1:5432  -> OK
bind/listen 127.0.0.1:0 -> OK
docker ps               -> exit 0(プロンプト無し)

セッションの再起動は不要で、適用直後から反映された。

見込み

週約 2,400 回 → 約 1,100 回。内訳は allowLocalBinding で 926 回、docker 読み取りで約 40 回、MCP で約 58 回。

残るのは escape 付き実行の裾(説明が付くようになる)と docker exec 系である。

追加コミット

test(claude): 検査を新しい仕様に追随させる

escapeask とだけ期待していた既存の検査が、説明必須ゲートの追加で落ちていた。仕様が変わったのは意図どおりなので、検査側を追随させた。

assert_decisiondescription を渡せるようにし、新しい門の振る舞いを4つの観点で固定している。

  • 承認を要する経路(escape、docker の変更系)で説明が無い・短いと deny
  • 読み取り専用の docker とサンドボックス内のコマンドは説明を求めない。ここを縛ると通常の作業に摩擦だけが増えるため、素通りであることを明示的に固定した
  • 閾値 80 バイトの境界。79 で deny、80 で ask
  • 破壊的操作の deny は説明の有無より優先する。説明を書けば force push が通る、という抜け道が無いことを示す

あわせて docker-compose.yml を含む grep が docker の変更系として誤検知されないことも固定した。pass=36 → pass=55、fail=0。

feat(claude): auto-mode の設定を取り込み、テンプレート化する

当初このPRから外していた /auto-mode-setup 生成の autoMode ブロックを管理下に入れた。取り込まないままだと chezmoi apply が実ファイルから 180 行を消すドリフトが残るため。

同時に settings.jsonsettings.json.tmpl に変えている。autoMode の説明文にホームディレクトリの絶対パスが3箇所あり、そこにアカウント名が入る。このリポジトリは public で、これまで同じ文字列を一度も含んでいない。 {{ .chezmoi.homeDir }} に置き換えて公開物に残らないようにした。

chezmoi execute-template の出力が現行の ~/.claude/settings.json と完全に一致することを確認済み。3ファイルとも chezmoi diff は空である。

CI

7 チェックすべて pass(ShellCheck / bash-guard hook test macOS・Ubuntu / chezmoi apply on macOS / chezmoiscripts test macOS・Ubuntu / pre-commit hook test)。

🤖 Generated with Claude Code

tunepolo and others added 3 commits September 3, 2026 10:14
直近8日の全セッション(Bash 呼び出し 10,635 件)を集計したところ、承認
プロンプトは週約 2,400 回発生していた。発生源は3つで、いずれも許可リスト
の不足ではない。`autoAllowBashIfSandboxed` があるためサンドボックス内で
完結するコマンドは既に自動許可されており、`npm run test` などを allow に
足しても何も変わらない。

最大の発生源はサンドボックス外実行(週 1,941 回)で、その 47%(926 回)が
ローカル TCP の遮断によるものだった。サンドボックス内で実測すると
`connect 127.0.0.1:5432` と `bind/listen 127.0.0.1:0` が両方とも EPERM で
落ちる。統合テスト、E2E、dev サーバ、Playwright、Go の httptest、
`curl http://localhost:*` がこれで失敗し、そのたびに昇格が必要になり、
bash-guard.sh が承認を求めていた。

`sandbox.network.allowLocalBinding` を有効にすると seatbelt プロファイルに
network-bind / network-inbound / localhost への network-outbound が出る。
適用後に同じ測定を行い、両方とも成功することを確認した。外部ネットワーク
の制限は変わらない。

第二の発生源は docker で、`excludedCommands` により常にサンドボックス外で
走るのに allow ルールが無く、週約 440 回プロンプトを出していた。読み取り
専用のサブコマンドだけを allow に加える。`docker exec` と `compose exec`
はコンテナ内で任意コマンドを実行できるため承認を維持する。

第三に、週3回以上使っている MCP の読み取り系ツール3件を allow に加える。

あわせて、承認ダイアログに出るのが description だけであるのに、ヒア
ドキュメントで渡す長いスクリプトの説明が無く可否を判断できない問題を直す。
bash-guard.sh に、承認を要する経路(escape と docker の変更系)で
description が 80 バイト未満なら deny を返す門を追加した。ask ではなく
deny なのは、deny の理由だけがモデルに返るため、説明を書き直させられる
からである。ask にするとユーザに丸投げになる。サンドボックス内で完結する
コマンドは対象外なので、通常の作業に摩擦は増えない。

見込みは週約 2,400 回から約 1,100 回。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
escape を ask とだけ期待していた検査が、説明必須ゲートの追加で落ちていた。
仕様が変わったのは意図どおりなので、検査側を追随させる。

`assert_decision` に description を渡せるようにし、既存の「サンドボックス外」
の検査には十分な長さの説明を与えて ask のままにした。そのうえで、新しい門の
振る舞いを4つの観点で固定する。

- 承認を要する経路(escape、docker の変更系)で説明が無い・短いと deny になる
- 読み取り専用の docker とサンドボックス内のコマンドは説明を求めない。ここを
  縛ると通常の作業に摩擦だけが増えるため、素通りであることを明示的に固定する
- 閾値 80 バイトの境界。79 で deny、80 で ask
- 破壊的操作の deny は説明の有無より優先する。説明を書けば force push が
  通ってしまう、という抜け道が無いことを示す

あわせて `docker-compose.yml` を含む grep が docker の変更系として誤検知
されないことも固定した。

pass=36 から pass=55 になる。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`/auto-mode-setup` が生成した `autoMode` ブロックを管理下に入れる。取り込ま
ないままだと `chezmoi apply` が実ファイルから 180 行を消すドリフトが残る。

同時に `settings.json` を `settings.json.tmpl` に変える。`autoMode` の説明文
にホームディレクトリの絶対パスが3箇所あり、そこにアカウント名が入る。この
リポジトリは public で、これまで同じ文字列を一度も含んでいない。`{{ .chezmoi.homeDir }}`
に置き換えて公開物に残らないようにした。

`chezmoi execute-template` の出力が現行の `~/.claude/settings.json` と完全に
一致することを確認済み。3ファイルとも `chezmoi diff` は空である。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tunepolo
tunepolo merged commit b1ecbe6 into main Sep 3, 2026
7 checks passed
@tunepolo
tunepolo deleted the feat/claude-fewer-permission-prompts branch September 3, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant