「Hooks完璧に動いてるのに、なんで移行するの?」
前回の安全設計記事でprotect-branches.py、auto-approve-git-safe.py、auto-approve-gh-mcp.pyの3本柱を紹介し、「止めるべきものは確実に止め、通すべきものはスムーズに通す」設計を運用してきました。
結論から言うと、この3つのPythonスクリプトは全部削除しました。現在はPermissions(permissions.deny)のワイルドカードパターンだけで安全設計を完結させています。
「え、あんなに力説してたのに?」...はい。でも「より良い手段が見つかったら乗り換える」のがエンジニアの仕事です。この記事では、なぜHooksをやめたのか、deny rulesだけで十分だった理由、そして移行する場合の判断基準を解説します。
この記事で学べること
- HooksベースからPermissionsベースに移行した具体的な理由
- deny rulesのワイルドカードパターンでHooksの動的検査を代替する方法
- Before/After の設定差分と実際のコミット履歴
- HooksとPermissions、どちらを選ぶべきかの判断フローチャート
前提知識
- 【Claude Code Hooks 安全設計】170超のdenyルールとPreToolUseフックで「やらかし」を防ぐ実践パターン — Before の状態
- 【Claude Code Hooks】テスト自動化で品質を仕組み化 — Hooks活用の別パターン
- 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
git add && git commitのようなチェーンコマンドを分割- 全パートが安全リストに含まれていれば自動承認
auto-approve-gh-mcp.py
mcp__gh__プレフィックスのツール名で判定- 破壊的キーワード(merge, delete, secret等)を含まなければ自動承認
この設計は動いていました。事故もゼロ。ではなぜ移行したのか。
転換点:なぜHooksをやめたのか
理由1:メンテナンスコストが高い
Pythonスクリプト3本、合計200行超。動くには動くけど、変更のたびに3箇所を意識する必要がある。
settings.json → denyリストの追加・修正
protect-branches.py → refspec解析ロジックの修正
auto-approve-*.py → 安全リスト・除外リストの修正
新しい保護パターンを追加するとき、「denyリストに追加すればいいのか、Pythonスクリプトにも手を入れるべきか」の判断が毎回必要。設定が1箇所に集約されていない時点で、運用コストが高い。
理由2:deny rulesのワイルドカードが想像以上に強力だった
前回の記事を書いた時点では、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のrefspec解析がやっていた仕事の大部分をカバーできることに気づいた。git push feature:mainもgit push +HEAD:refs/heads/mainも、*:mainのワイルドカードで引っかかる。
Pythonで80行書いてrefspecをパースするのと、denyリストに5行追加するの、どちらが安全で保守しやすいか。答えは明白でした。
理由3:auto-approveの仕組みが不要になった
auto-approve系フックは「Bashコマンドがデフォルトで承認確認を要求する環境」で価値がありました。安全なgitコマンドまでいちいち確認ダイアログが出るのを避けるために、フックでallowを返していた。
ところが、permissions.allowに"Bash"を含めると、Bashコマンドはデフォルトで許可されます。危険なコマンドだけpermissions.denyでブロックすれば、auto-approve用のPythonスクリプトは完全に不要。
"permissions": {
"allow": ["Bash", "Read", "Write", "Edit", "MultiEdit", "Task", "Skill", "WebSearch", "mcp__*"],
"deny": ["... 危険なパターンだけを列挙 ..."]
}
「denyが常にallowより優先される」というClaude Codeの仕様を活用して、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つのPythonスクリプトが登録されていたこのセクションが、空配列になっています。
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する」パターンを全てキャッチします。refspecパーサーで解析していたgit push origin feature:mainも、このパターンで静的にブロックされる。
検査2:force push → 全パターンのdeny
"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:ブランチ削除 → 明示的deny
"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が得意な仕事には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。守備範囲で使い分ける。
Before/After 比較
設定ファイルの変化
| 項目 | Before | After |
|---|---|---|
| PreToolUseフック | 3スクリプト登録 | 空配列 |
| Pythonスクリプト | 3本(200行超) | 0本(メモリ監視除く) |
| deny rules数 | 170超 | 170超(微増) |
| 設定ファイル数 | settings.json + 3つの.py | settings.jsonのみ |
| 依存 | Python3実行環境 | なし |
実際のコミット履歴
移行は2026年3月6日に4つのコミットで実行されました。
| 順序 | コミット | 内容 |
|---|---|---|
| 1 | a694285 | 未使用MCP serverの設定とhookを削除 |
| 2 | f322fe5 | gh MCP用の自動承認hookスクリプトを削除 |
| 3 | 436c0d3 | Bash全許可済みのため冗長なgit auto-approve hookを削除 |
| 4 | c5a6053 | protect-branches hookを削除しdenyルールで代替 |
段階的に削除していった理由は、各ステップで「本当に安全か」を確認しながら進めるため。一括削除して事故が起きたら、どの変更が原因か特定しにくい。
メンテナンスの変化
| 観点 | Before | After |
|---|---|---|
| 新パターン追加 | denyリスト or Python修正の判断が必要 | denyリストに1行追加するだけ |
| デバッグ | Python実行ログ + deny判定の2箇所確認 | denyリストの文字列マッチのみ |
| 新規メンバーの理解 | settings.json + Python3本の読解 | settings.json1ファイルの読解 |
| 環境依存 | Python3が必要 | なし |
判断基準:どちらを選ぶべきか
Permissions(deny rules)が適切なケース
- パターンマッチで判定可能な操作のブロック(git push、rm -rf等)
- ワイルドカードで表現できる禁止パターン(
*:main、--force:*等) - 設定を1ファイルに集約したい場合
- メンテナンスの簡素さを重視する場合
Hooks(PreToolUse)が適切なケース
- 外部APIの呼び出しが必要な判定(gh API、データベース参照等)
- 実行時のコンテキスト(現在のブランチ名、ファイル内容等)に基づく判定
- 複雑な条件分岐が必要な場合(AかつBだがCでない、等)
- イベント通知・監視系の処理(メモリ監視、デスクトップ通知等)
判断フローチャート
「この操作を制御したい」
├─ ブロック or 許可?
│ ├─ ブロック → パターンマッチで判定可能?
│ │ ├─ Yes → permissions.deny に追加
│ │ └─ No → 外部API/実行時コンテキスト必要?
│ │ ├─ Yes → PreToolUse Hook
│ │ └─ No → deny rulesのワイルドカードで再検討
│ └─ 通知/監視/初期化?
│ └─ → SessionStart / PostToolUse / Notification Hook
└─ 自動承認?
└─ permissions.allow で対応(Hookは不要)
移行チェックリスト
もし現在Hooksベースで安全設計をしていて、Permissionsベースへの移行を検討するなら、以下を確認してください。
- PreToolUseフックが「パターンマッチ」しかしていない → deny rulesで代替可能
- auto-approveフックが存在する →
permissions.allowで代替可能 - 外部API呼び出しをしているフックがある → そのフックはHooksとして残す
- Python実行環境への依存を減らしたい → deny rulesへの移行が有効
まとめ
Claude Code Hooksの安全設計は、Hooksの機能が悪かったから移行したわけではありません。deny rulesの表現力が十分に高く、Hooksのオーバーヘッドなしに同等の安全性を実現できたから移行しました。
移行の本質は「3つのPythonスクリプトをやめた」ことではなく、「設定を1ファイルに集約し、メンテナンスコストを下げた」こと。170超のdenyルールは多く見えますが、JSONの1行追加で保護パターンを増やせるのは、Pythonスクリプトを修正するより圧倒的にシンプルです。
ただし、HooksとPermissionsは排他的な選択ではありません。現に私たちはメモリ監視やゾンビプロセスkillにはHooksを使い続けています。「ブロック」はdeny rules、「監視・通知」はHooks。それぞれの得意分野で使い分けるのが、現時点でのベストプラクティスです。
あわせて読みたい
- 【2026年版】AIコーディングツール完全比較 — Claude Code・Codex・Antigravityの選び方
- 【Claude Code Hooks 安全設計】170超のdenyルールとPreToolUseフックで「やらかし」を防ぐ実践パターン — 移行前の設計を詳しく知りたい方へ
- 【Claude Code Hooks】テスト自動化で品質を仕組み化 — Hooksの別の活用パターン
- 【Claude Code】settings.json完全ガイド — Permissions・Hooksの基本設定
AIコーディングツール活用 完全ガイド — この記事を含む13本の記事で、AIコーディングツールの比較・導入から実践活用までを体系的に解説しています。



