3行まとめ
- PreToolUse Hooks(Python3本・200行超)を全削除。denyのワイルドカードだけで完結
- deny rulesの
*:mainパターンで、Hooksの動的解析と同等のブロック精度を実現- Hooksは廃止ではなく役割分担 — ブロックはdeny、監視はHooks
前回の安全設計記事で3本のPythonスクリプトを紹介しました。今回、この3つは全部削除。現在はPermissionsのワイルドカードだけで完結しています。
「え、あんなに力説してたのに?」...はい。でも「より良い手段が見つかったら乗り換える」のがエンジニアの仕事です。なぜやめたのか、判断基準を解説します。
この記事で学べること
- HooksからPermissionsに移行した具体的な理由
- deny rulesでHooksの動的検査を代替する方法
- Before/Afterの設定差分とコミット履歴
- どちらを選ぶべきかの判断フローチャート
前提知識
- Hooks安全設計170超のdenyルール実践パターン — Before の状態
- Hooksでテスト自動化 — 活用の別パターン
- settings.jsonの落とし穴と落とし所 — hooksの罠とeffortLevel
- Claude Codeの
settings.jsonの基本構造を理解している
Before:Hooksベース設計(サマリ)
移行前は3層構成。
| レイヤー | 仕組み | 役割 |
|---|---|---|
| L1: 静的deny | permissions.deny(170超) | 即座にブロック |
| L2: 動的フック | PreToolUse + Python 3本 | 動的解析して判定 |
| L3: 自動承認 | PreToolUseのallow応答 | 承認ダイアログ省略 |
3つのPythonスクリプト
protect-branches.py(約80行)
gh pr mergeの宛先をgh APIで確認、保護ブランチならpushブロック- refspecの宛先(
src:dstのdst)をパースして検出
auto-approve-git-safe.py
- チェーンコマンドを分割し、全パートが安全リストなら自動承認
auto-approve-gh-mcp.py
mcp__gh__プレフィックスで判定、破壊的キーワード(merge, delete等)が無ければ自動承認
この設計は動いていました。事故もゼロ。なぜ移行したのか。
転換点:なぜHooksをやめたのか
理由1:メンテナンスコストが高い
Pythonスクリプト3本、200行超。変更のたび3箇所を意識する必要。
settings.json → denyリストの追加・修正
protect-branches.py → refspec解析ロジックの修正
auto-approve-*.py → 安全リスト・除外リストの修正
新パターン追加のたび、denyかPythonかの判断が毎回必要でした。
理由2:deny rulesのワイルドカードが想像以上に強力だった
前回の記事では、ワイルドカードの表現力を過小評価していた。
"Bash(git push *:main)",
"Bash(git push *:master)",
"Bash(git push *:dev)",
"Bash(git push *:refs/heads/main)",
"Bash(git push *:refs/heads/dev)"
このパターンでprotect-branches.pyの仕事の大部分をカバーできると気づきました。git push feature:mainもgit push +HEAD:refs/heads/mainも*:mainで引っかかります。
Pythonで80行書くのと、denyリストに5行追加するの、どちらが保守しやすいかは明白でした。
理由3:auto-approveの仕組みが不要になった
auto-approve系フックは「Bashが承認確認を要求する環境」で価値がありました。確認ダイアログを避けるためallowを返していた。
ところが permissions.allow に "Bash" を含めるとBashはデフォルトで許可されます。危険なコマンドだけdenyでブロックすれば、auto-approve用スクリプトは不要。
"permissions": {
"allow": ["Bash", "Read", "Write", "Edit", "MultiEdit", "Task", "Skill", "WebSearch", "mcp__*"],
"deny": ["... 危険なパターンだけを列挙 ..."]
}
「denyが常にallowより優先」という仕様を活用しallowは広く、denyで穴を塞ぐ。auto-approveフックの出番はありません。
理由4:gh MCPサーバーの削除
auto-approve-gh-mcp.pyは、gh MCPのツール呼び出しに安全判定をしていました。gh MCPをdotfiles管理対象から外したため存在意義がなくなりました。
After:Permissionsベース設計
settings.jsonの構成
移行後のsettings.jsonは3セクション構成です。
{
"permissions": {
"allow": [
"Read",
"Write",
"Edit",
"MultiEdit",
"Bash",
"Task",
"Skill",
"WebSearch",
"mcp__serena",
"mcp__*"
],
"deny": ["... 170超のdenyルール ..."],
"defaultMode": "acceptEdits",
"disableBypassPermissionsMode": "disable"
},
"hooks": {
"SessionStart": ["..."],
"PreToolUse": [],
"PostToolUse": ["..."],
"Notification": ["..."]
}
}
注目は "PreToolUse": []。以前は3スクリプトが登録されていたセクションが空配列になっています。
deny rulesでprotect-branches.pyを代替
protect-branches.pyの3つの検査を、deny rulesでどうカバーするか見ていきます。
検査1:保護ブランチへの直接push
"Bash(git push origin HEAD:refs/heads/main)",
"Bash(git push origin HEAD:refs/heads/dev)",
"Bash(git push --set-upstream origin main)",
"Bash(git push --set-upstream origin dev)",
"Bash(git push origin main:main)",
"Bash(git push origin dev:dev)",
"Bash(git push *:main)",
"Bash(git push *:master)",
"Bash(git push *:dev)",
"Bash(git push *:refs/heads/main)",
"Bash(git push *:refs/heads/dev)"
*:mainが「任意のローカルブランチからmainにpush」を全てキャッチします。git push origin feature:mainもこのパターンで静的にブロックされます。
検査2:force push
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git push --force-with-lease origin HEAD:refs/heads/main)",
"Bash(git push --force-with-lease origin HEAD:refs/heads/dev)",
"Bash(git push --mirror:*)",
"Bash(git push --all:*)"
force pushの全バリエーションを網羅。--force-with-leaseもブロック。
検査3:ブランチ削除
"Bash(git push origin --delete dev)",
"Bash(git push origin --delete main)",
"Bash(git push origin --delete master)",
"Bash(git push origin :*)"
git push origin :main(空refspecでのブランチ削除)もorigin :*でブロック。
Hooksの現在の使い方
PreToolUseの3スクリプトは消えましたが、Hooks自体は全廃していません。得意な仕事には使い続けています。
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [{
"type": "command",
"command": "echo 'Context compacted. Reminder: Read CLAUDE.md for project context.'"
}]
},
{
"matcher": "startup",
"hooks": [{
"type": "command",
"command": "SCRIPT=\"$HOME/.claude/skills/claude-zombie-kill/scripts/zombie-kill.sh\"; [[ -x \"$SCRIPT\" ]] && bash \"$SCRIPT\" --force --min-hours 48"
}]
}
],
"PostToolUse": [
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "python3 \"$HOME/.claude/hooks/memory-monitor.py\""
}]
}
]
}
| Hook | 役割 | なぜdenyで代替しないか |
|---|---|---|
| SessionStart(compact) | 圧縮後の通知 | イベント通知はHooks得意 |
| SessionStart(startup) | ゾンビkill | 初期化はdeny守備範囲外 |
| PostToolUse | メモリ監視 | 監視はdeny不可能 |
ルール: 「ブロック/許可」はdeny rules、「通知/監視/初期化」はHooks。
詳しくは settings.json の落とし穴 に具体例があります。
Before/After 比較
設定ファイルの変化
| 項目 | Before | After |
|---|---|---|
| PreToolUse | 3スクリプト | 空配列 |
| Pythonスクリプト | 3本(200行超) | 0本(監視除く) |
| deny rules数 | 170超 | 170超(微増) |
| 設定ファイル数 | +3つの.py | settings.jsonのみ |
| 依存 | Python3 | なし |
移行は4段階に分けた
移行は2026年3月6日、次の順序で実行しました。
| 順序 | 内容 |
|---|---|
| 1 | 未使用MCP serverの設定とhook削除 |
| 2 | gh MCP自動承認hookスクリプト削除 |
| 3 | 冗長なgit auto-approve hook削除 |
| 4 | protect-branches hookをdeny代替 |
段階的に削除した理由は各ステップで「本当に安全か」を確認しながら進めるため。一括削除で事故が起きたら原因の特定が難しい。
メンテナンスの変化
| 観点 | Before | After |
|---|---|---|
| 新パターン追加 | deny/Python判断要 | deny1行追加のみ |
| デバッグ | ログ+deny2箇所確認 | 文字列マッチのみ |
| 新規理解 | settings+Python3本 | settingsのみ |
| 環境依存 | Python3必要 | なし |
判断基準
Permissions(deny rules)が適切なケース
- パターンマッチで判定可能な操作(git push、rm -rf等)
- ワイルドカードで表現できる禁止パターン(
*:main等) - 設定を1ファイルに集約したい場合
Hooks(PreToolUse)が適切なケース
- 外部APIが必要な判定(gh API等)
- 実行時のコンテキスト(ブランチ名等)に基づく判定
- イベント通知・監視系(メモリ監視等)
判断フローチャート
「この操作を制御したい」
├─ ブロック or 許可?
│ ├─ ブロック → パターンマッチで判定可能?
│ │ ├─ Yes → permissions.deny に追加
│ │ └─ No → 外部API/実行時コンテキスト必要?
│ │ ├─ Yes → PreToolUse Hook
│ │ └─ No → deny rulesのワイルドカードで再検討
│ └─ 通知/監視/初期化?
│ └─ → SessionStart / PostToolUse / Notification Hook
└─ 自動承認?
└─ permissions.allow で対応(Hookは不要)
移行チェックリスト
移行を検討するなら、以下を確認してください。
- パターンマッチのみのフック → deny rulesで代替可能
- auto-approveフック →
permissions.allowで代替可能 - 外部API呼び出しのフック → Hooksとして残す
- Python依存を減らしたい → deny rules移行が有効
まとめ
Hooksの機能が悪かったから移行したのではありません。deny rulesの表現力が高く、オーバーヘッドなしに同等の安全性を実現できたから移行しました。
移行の本質は「Pythonをやめた」ことではなく「設定を1ファイルに集約し、メンテナンスコストを下げた」こと。170超のdenyルールは多く見えますが、JSON1行追加は圧倒的にシンプルです。
ただし、HooksとPermissionsは排他的ではありません。メモリ監視やゾンビkillにはHooksを使い続けています。「ブロック」はdeny、「監視・通知」はHooksで使い分けます。
あわせて読みたい
- AIコーディングツール完全比較 — Claude Code・Codex・Antigravity
- Hooks安全設計170超のdenyルール実践パターン — 移行前の設計
- Hooksでテスト自動化 — 別の活用パターン
- settings.json と CLAUDE.md 完全ガイド — 使い分けの出発点
- settings.jsonの落とし穴と落とし所 — 実例
Claude Code エージェント・安全設計 完全ガイド — 本記事含む13本で体系解説。



