Skip to content

refactor(claude): 権限設定の棚卸しとBash実行ガードのフック切り出し - #78

Merged
tunepolo merged 2 commits into
mainfrom
refactor/claude-permission-audit
Aug 20, 2026
Merged

refactor(claude): 権限設定の棚卸しとBash実行ガードのフック切り出し#78
tunepolo merged 2 commits into
mainfrom
refactor/claude-permission-audit

Conversation

@tunepolo

Copy link
Copy Markdown
Owner

概要

Claude Code の権限設定とサンドボックス設定を全プロジェクト分棚卸しし、実効性のないエントリを削って、実際に機能していなかった防御をフック側に移した。

~/.claude/projects/*/*.jsonl の全トランスクリプト(Bash 実行 7,969 件)を集計した結果が出発点。

期間 Bash 実行 サンドボックス外
2026-07 2,103 0 (0%)
2026-08 5,880 1,323 (22%)

サンドボックスを導入した 8/8 以降、実行の22%がサンドボックス外に出て毎回承認プロンプトになっていた。プロンプトのたびに「常に許可」で書かれたエントリが積み上がり、許可リストは355件に達していたが、実効性があったのは71件だけだった。

変更点

許可リストの整理

sandbox.autoAllowBashIfSandboxed: true なので、サンドボックス内で完結する Bash は許可エントリなしで通る。Bash(go test *) Bash(gofmt *) Bash(go vet *) のようなエントリは一度も効いていなかった。git/gh の粒度別エントリも Bash(git *) / Bash(gh *) に集約した(破壊的操作の判定はフックが持つため、パターンを細かく並べても意味がない)。

プロジェクト側の設定(このリポジトリの管理外)も同時に 355 → 71 件に整理した。内訳は、一回限りの具体コマンド 142件、サンドボックス内で自動許可されるもの 60件、フックが ask を強制するため効かない git -C 31件。

実際に開いていた穴を塞ぐ

  • .env 系グロブの逆転: **/.env.* はアンダースコア区切りの .env_prod / .env_stg にマッチせず、本物の秘密が読める状態だった。一方で無害な .env.example は塞いでいた。実在するファイル名に合わせて明示列挙に変更
  • force push の検知漏れ: Bash(git push --force *) は前方一致なので、フラグを末尾に置いた git push origin master --force を捕まえられなかった
  • ~/.ssh の読み取り: ~/.ssh/id_* のみ deny だったため config / known_hosts は読めた。~/.ssh/** に拡大し、~/.config/gh/hosts.yml も追加
  • aws の広すぎるワイルドカード(プロジェクト側): Bash(aws ec2 *) Bash(aws lambda *) Bash(aws ses *)terminate-instances / delete-function / send-email まで通していたため、読み取り専用の動詞に絞った

フックの切り出しと強化

settings.json に埋め込んだワンライナーを dot_claude/hooks/bash-guard.sh に独立させた。フラグの位置に依存しない検知は permissions のパターンでは書けないため、破壊的操作の防止はここに集約する。

  • deny: force push(位置を問わない)、reset --hardclean -f/--forcefilter-repo/filter-branchgh repo delete
  • ask: サンドボックス外実行、gh auth tokengh secret/variable の変更、gh api の書き込みメソッド、git -cGIT_SSH_COMMAND=/GIT_CONFIG_*=--upload-pack/--receive-pack/--exec-path
  • 通す: --force-with-leaseclean -ngh api の GET、git -C
  • jq 不在時や入力 JSON の解析失敗時は ask を返して fail-closed

git -C を ask から外したのは、作業ディレクトリを変えるだけで無害な一方、従来 deny に入っていたために同種の許可エントリが31件も溜まっていたため。任意コマンドを仕込める git -c は ask のまま。

サンドボックスの書き込み先を追加

golangci-lint がキャッシュに書けず58回失敗していた。~/Library/Caches/golangci-lint / Yarn / miseallowWrite に追加。

運用ルールを CLAUDE.md に明記

サンドボックス外に出た637件の Go コマンドの内訳は、/tmp 直下への書き込み128件、git/gh との複合96件、読み取りのみで昇格の必要がなかったもの26件。設定ではなく運用で減らせるものが多いため、以下をルール化した。

  • 一時ファイルは /tmp ではなく $TMPDIR に置く
  • git / gh はビルドやテストと同じコマンドに混ぜない(sandbox.excludedCommands はコマンド全文に対するマッチなので、gh をループやパイプの中で呼ぶと除外が効かず ~/.config/gh を読めずに失敗する。履歴上26件)
  • 昇格は先入観で付けず、実際に権限エラーで落ちてから行う

潰した誤検知

書いたフックが実際にこの PR のコミット自身を2回拒否した。どちらもテストケースに落としてある。

  1. ヒアドキュメントの本文まで検査していた: コミットメッセージ本文で git push origin master --force に言及しただけで deny された。本文は実行されるコマンドではないので検査対象から外した
  2. フラグの探索範囲がコマンド全体だった: git push origin main && grep -f patterns.txt loggrep -f を force フラグと誤認した。区切り(; | & 改行)をまたがないよう、git 呼び出しの引数部分に限定した

テスト

tests/bash-guard.test.sh に37ケース。既存の tests/*.test.sh の作法に合わせ、CI(ubuntu / macOS)にもジョブを追加した。shellcheck.sh の対象に dot_claude/hooks/*.sh を含めた。

pass=37 fail=0

shellcheck.sh は終了コード 0。

確認してほしいこと

  • .env 系の deny を明示列挙にしたので、将来 .env.<新しい環境名> を作ったときに列挙漏れが起きる。グロブの否定が書けないためこの形にしたが、運用として許容できるか
  • Bash(git *) / Bash(gh *) への集約は、破壊的操作の判定をフック1点に寄せる前提。フックが壊れたときの影響範囲が広がるトレードオフがある
  • サンドボックス設定はセッション開始時に読まれるため、allowWrite の追加は Claude Code の再起動後に効く

未対応

~/.claude/hooks/herdr-agent-state.sh は chezmoi 管理外のまま。hooks/ が管理下に入ったので、新しいマシンで chezmoi apply すると SessionStart フックだけ欠ける。別 PR で追加する。

🤖 Generated with Claude Code

tunepolo and others added 2 commits August 20, 2026 14:50
settings.json に埋め込んだワンライナーだった PreToolUse フックを
dot_claude/hooks/bash-guard.sh に独立させ、検知内容を強化した。

切り出した理由は、フラグの位置に依存しない検知を書けるようにするため。
permissions.deny のパターンは前方一致なので、Bash(git push --force *) では
フラグを末尾に置いた形(git push origin master --force)を捕まえられない。
破壊的操作の防止はフック側でしか実装できない。

- deny: force push(位置を問わない)、reset --hard、clean -f/--force、
  filter-repo/filter-branch、gh repo delete
- ask: サンドボックス外実行、gh auth token、gh secret/variable の変更、
  gh api の書き込みメソッド、git -c、GIT_SSH_COMMAND=/GIT_CONFIG_*=、
  --upload-pack/--receive-pack/--exec-path
- 通す: --force-with-lease、clean -n、gh api の GET、git -C
- jq 不在時や入力 JSON の解析失敗時は ask を返して fail-closed にする

git -C を ask 対象から外したのは、作業ディレクトリを変えるだけで無害な一方、
従来 deny に入っていたために同種の許可エントリがプロジェクト設定に31件も
溜まっていたため。任意コマンドを仕込める git -c は ask のまま残す。

誤検知を2つ潰した。どちらも実際にこのコミット自身を拒否したもの。

- ヒアドキュメントの本文を検査対象から外した。含めたままだと、この
  コミットメッセージのように本文で破壊的コマンドに言及しただけで拒否される
- フラグの探索範囲を git 呼び出しの引数部分に限定した。区切り(; | & 改行)を
  またがないので、後続コマンドの -f を force フラグと誤認しない

tests/bash-guard.test.sh で37ケースを検証する。CI にも同名のジョブを追加し、
shellcheck.sh の対象に dot_claude/hooks/*.sh を含めた。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
全プロジェクトの許可エントリを棚卸ししたところ、355件のうち実効性が
あったのは71件だけだった。sandbox.autoAllowBashIfSandboxed が true なので
サンドボックス内で完結する Bash は許可エントリなしで通り、Bash(go test *) や
Bash(gofmt *) のようなエントリは一切効いていない。8/8 にサンドボックスを
導入して以降、Bash 実行の22%がサンドボックス外に出て毎回プロンプトになり、
「常に許可」で書かれたエントリだけが積み上がっていた。

- allow: git/gh の粒度別エントリを Bash(git *) / Bash(gh *) に集約する。
  破壊的な操作の判定は bash-guard.sh 側に持たせたので、パターンを細かく
  並べても意味がない
- deny: .env 系のグロブが逆転していたのを修正する。**/.env.* は
  アンダースコア区切りの .env_prod / .env_stg にマッチせず本物の秘密が
  読める一方、無害な .env.example を塞いでいた。実在するファイル名に
  合わせて明示列挙に変える
- deny: ~/.ssh/id_* を ~/.ssh/** に広げ、~/.config/gh/hosts.yml を追加する
- deny: git filter-repo/filter-branch と gh repo delete を追加する
- deny: 位置依存で機能していなかった git の force push 系パターンを削除する
  (bash-guard.sh が位置を問わず deny する)
- sandbox.allowWrite: golangci-lint / Yarn / mise のキャッシュを追加する。
  golangci-lint はキャッシュに書けず58回失敗していた
- CLAUDE.md: 一時ファイルは /tmp ではなく $TMPDIR に置く、git/gh は
  ビルドやテストと同じコマンドに混ぜない、昇格は実際に失敗してから行う、
  という運用ルールを明記する

git/gh をビルドやテストと混ぜない理由は、sandbox.excludedCommands が
コマンド全文に対するマッチだから。gh をループやパイプの中で呼ぶと除外が
効かずサンドボックス内で走り、~/.config/gh を読めずに失敗する(履歴上26件)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tunepolo
tunepolo merged commit 8738711 into main Aug 20, 2026
7 checks passed
@tunepolo
tunepolo deleted the refactor/claude-permission-audit branch August 20, 2026 05:57
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