「このコマンドは使わないで」と指示ファイルに書いたのに、AIコーディングエージェントがまた同じ操作をしてしまうことがあります。
そんな場面に、心当たりはありませんか?
実は CLAUDE.md や AGENTS.md のような指示ファイルは、守るかどうかの判断をモデルに委ねる仕組みです。
これに対して、決まった時点で必ず実行される処理を差し込めるのが「フック(hooks)」です。
一言で表すなら、指示ファイルは「お願い」、フックは「強制」という違いになります。
指示ファイルは「お願い」、フックは「強制」
指示ファイルのルールは、モデルの文脈に読み込まれる「文章」にすぎません。
読んだうえでどう動くかはモデル次第なので、読んでいても守られないことがあるわけです。
指示ファイルの役割と書き方は、こちらにまとめました。
一方、Claude Code の公式ガイド Automate actions with hooks は、フックを「ライフサイクルの決まった時点で実行される、利用者定義のシェルコマンド」と説明しています。
利点は、LLM が実行を選ぶかどうかに頼らず、処理が必ず起きる「決定的な制御」にあると書かれています。
Zenn の ルールを文書に足しても違反は減らなかったという記録 でも、フックや検査スクリプトでコード側に止めた8件は再発0件、文書に書いただけの16件は全件が再発していました。
n=1 の実測で時間経過の影響も否定できないと投稿者自身が断っているものの、文章で頼むより仕組みで止めたほうが再発しにくいという方向性は公式の考え方と噛み合います。
フックを差し込めるのは「決まった時点」
フックは、エージェントの動きの中に用意された「イベント」に結び付けて登録する仕組みです。
Claude Code のフックリファレンス を見ると、代表的なイベントは次のとおりでした。
- SessionStart / SessionEnd: セッションの開始時・終了時
- UserPromptSubmit: プロンプトを送信し、Claude が処理する前
- PreToolUse / PostToolUse: ツールの実行前・実行が成功した後
- Stop: Claude が応答を終えたとき
2026年9月時点のリファレンスには30を超えるイベントが並び、中でも使いどころが多いのがツールの実行直前に割り込める PreToolUseです。
設定は ~/.claude/settings.json やリポジトリの .claude/settings.json に、「イベント → matcher(対象の絞り込み)→ 実行する処理」の3段で書きます。
Bash ツールの実行前に Python スクリプトを呼ぶ例を、公式の書式に沿って示すと次のとおりです。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/block_rm.py"]
}
]
}
]
}
}
これは書式の例で、私の手元の設定には登録していません。
Cursor の Hooks も同じ発想の仕組みで、イベント名の体系は異なるものの、終了コード 2 で止める点は Claude Code に合わせてあると明記されていました。
止めるときは「終了コード 2」か JSON で返す
フックには、これから起きる操作の中身が JSON で渡されます。
コマンド型なら標準入力で受け取り、終了コードで結果を返すのが基本で、2026年9月時点のリファレンスが定める意味は次の3つです。
- 0: 成功。何も出力しなければ「判断なし」で、通常の許可確認へ進む
- 2: ブロック。PreToolUse ならツールを止め、標準エラー出力が理由として Claude に届く
- それ以外(1 など): 多くのイベントでは、それだけではブロックしないエラー扱いで処理が続く
一般的な失敗の合図である 1 では止まらない点に注意してください。
止める側のスクリプトは、概念を示すと次のような形になります。
import json
import sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
if "rm -rf" in command:
print("Blocked: rm -rf is not allowed in this project", file=sys.stderr)
sys.exit(2) # 2 で止める(1 では止まらない)
sys.exit(0) # 判断なし=通常の許可確認へ進む
スクリプト単体に JSON を流して試すと、rm -rf を含む入力では終了コード 2、ls では 0 で終わりました(Python 3.12.10・Claude Code には未登録)。
止められた理由は Claude に届くので、エージェントは別のやり方を考え直せるわけです。
PreToolUse なら、0 で終えて JSON を返すことで deny・allow・ask・defer(拒否・許可・確認・保留)も指示できます。
公式ガイドによれば、deny は権限確認を省く bypassPermissions モードでも効く一方、allow では設定の拒否ルールを覆せません。
つまり、フックは制限を強められても、権限設定より緩めることはできないという関係です。
向いている用途と、向かない用途
フックが得意なのは、「必ず起きてほしいこと」と「毎回同じ基準で判定できること」です。
- 危険な操作の拒否(PreToolUse で特定のコマンドや .env への書き込みを止める)
- 整形の自動実行(PostToolUse で編集後に整形ツールを走らせる)
- 記録と通知(実行したコマンドのログ、入力待ちの通知)
どれも人が思い出さなくても同じ手順が確実に回るところに価値があります。
危険な操作の拒否を、個人情報の外部送信を止める用途で組んだ記録はこちらです。
逆に、「読みやすい設計か」のように判断が要るルールは、文字列の照合では書き切れません。
そうしたルールは、指示ファイルに残しておくのが自然です。
また、PostToolUse は実行後に動くので、操作を取り消せない点にも注意が必要です。
取り入れる前に知っておきたい落とし穴
強制力があるぶん、フックには指示ファイルにない注意点もあります。
フックは任意のコードを実行する
リファレンスは、コマンド型のフックが利用者と同じ権限でシェルコマンドを実行すると警告しています。
特に -p の非対話実行では、リポジトリの .claude/settings.json のフックが確認なしで動くため、他人の設定は中身を読んでから取り込むことが大前提です。
別のツールが書き残したフック設定がエラーを招いた調査記録は、こちらに書いています。
遅いフックはエージェントを待たせる
フックは既定で、終わるまで Claude の実行を待たせる仕様です。
公式は、matcher や if 条件で対象を絞る方法や、長い処理を async で裏に回す方法を紹介しています。
しかも PreToolUse のコマンド型フックは、時間切れになるとツール呼び出しを止めずに通してしまうと明記されていました。
イベントが必ず発火するとは限らない
終了時の SessionEnd フックは、セッションが終わらない限り呼ばれません。
note の 作業時間の記録がゼロのままだった話 では、6日以上閉じなかったセッションで送信が一度も起きず、ターンの終わりに動く Stop フックへ移して直したそうです。
公式も、Stop はユーザーの中断では発火しないこと、@ で参照したファイルの読み込みでは PreToolUse が発火しないことを挙げています。
リファレンス自身も、if 条件の絞り込みはベストエフォートだとして、厳密な許可・拒否には権限設定を使うよう勧めているのです。
フックは権限設定やサンドボックスと重ねて使う一枚の層と捉えておくと、期待しすぎずに済みます。
最後に
指示ファイルのルールは、モデルが読んで判断する「お願い」にとどまります。
必ず守らせたいルールだけを、決まった時点で必ず動くフックに移すのが、両者の役割分担でしょう。
違反を繰り返したルールから順にフックへ移す進め方なら、効果も確かめやすいと思います。
私もまだ学んでいる途中なので、まずは指示ファイルから「毎回同じ基準で判定できるルール」を探すところから始めてみます。
以上です。














コメントを残す