Claude Code の hooks はなぜ CLAUDE.md や指示文では代替できないのか——マルチエージェント環境での安全設計3原則
Claude Code を使い始めてしばらくすると、多くのエンジニアが同じ壁にぶつかります。
「CLAUDE.md にあれだけ丁寧に書いたのに、なぜ守ってくれないのか」
たとえば「本番環境のデプロイコマンドは絶対に実行しないこと」と明記したとします。最初のうちは守られます。しかし数十往復のやり取りを経た後、別のタスクを処理している最中に、何の前触れもなく実行されてしまうことがあります。
この記事では、なぜ CLAUDE.md や指示文は「安全装置」として機能しないのかを整理し、hooks という仕組みがその代替になる理由、そして Amanity が複数エージェントを並走させる環境で実際に入れている安全設計の3原則を紹介します。
そもそも hooks とは何か
LLM の動作に「割り込む」仕組み
hooks は、Claude Code が特定のアクションを実行する直前・直後に、任意のシェルスクリプトを自動実行できる仕組みです。
たとえば「ファイルを書き換えるコマンドが実行される直前に、上書き先のファイルが存在するかチェックする」「セッションが終了するときに未完了タスクが残っていないか確認する」といった処理を、シェルスクリプトで書いて設定ファイルに登録するだけで動きます。
大事な点は、hooks の処理は Claude(LLM)を一切介さないという点です。シェルスクリプトが直接実行されます。後述しますが、これが CLAUDE.md との本質的な違いになります。
主なイベントタイプ
hooks で使える主なイベントタイプは以下のとおりです。
| イベント | タイミング |
|---|---|
PreToolUse | ツール実行の直前(ここでブロックできる) |
PostToolUse | ツール実行の直後 |
Stop | Claude のセッション終了直前 |
SubagentStop | サブエージェントのセッション終了直前 |
設定は .claude/settings.json に JSON 形式で記述します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/check-mv-overwrite.sh"
}
]
}
]
}
}matcher にはツール名(Bash、Write、Edit など)を指定します。上の例では「Bash コマンドが実行される直前に必ず check-mv-overwrite.sh を実行する」という設定です。
CLAUDE.md はなぜ「安全装置」にならないのか
LLM の判断は確率的——同じ指示でも毎回同じ結果は保証されない
CLAUDE.md は Claude への「指示書」です。このプロジェクトでは〜してください という形で書いたルールを、Claude はセッション開始時に読み込んで参考にします。
ただし、ここには根本的な限界があります。Claude は LLM(Large Language Model)であり、その判断はすべて確率的です。
同じルールを書いていても、コンテキストの状態によって「このタイミングでは従う」「今の文脈ではこちらを優先する」という判断が変わります。「絶対に実行しないこと」と書いても、LLM には「絶対」という概念を100%保証する仕組みがありません。
コンテキストが増えるほどルールは薄まる
セッション中に会話が長くなると、CLAUDE.md の内容はコンテキストウィンドウ内で相対的に「薄まって」いきます。
最初は明示的に参照していたルールが、100往復を超えるやり取りの中でいつの間にか優先度が下がってしまうことがあります。これは Claude の注意力が散漫になるのではなく、長いコンテキストの中で過去の指示の重みが自然に低下する構造上の特性です。
hooks が確定的に動く理由——仕組みを理解する
LLM を一切介さないシェルスクリプト
hooks はシェルスクリプトとして実装されます。Claude が「このコマンドを実行しようとしている」と判断したとき、LLM が hooks スクリプトを呼び出すのではありません。Claude Code のランタイムが直接シェルスクリプトを実行します。
つまり、hooks のチェックは Claude の判断とは完全に独立しています。Claude がどれだけ「このコマンドは安全だ」と判断していても、hooks スクリプトが「ブロックする」と返せば実行は止まります。
hooks スクリプトの返り値
hooks スクリプトは、標準入力から実行予定のツール情報を JSON で受け取り、判断結果を返します。
permissionDecision: "ask"→ ユーザーに確認プロンプトを表示するpermissionDecision: "deny"→ ツール呼び出しをキャンセルして Claude に理由を送るexit 2(stderr に理由を出力)→ Claude へのフィードバック付きでブロックするexit 0→ 通常どおりツールが実行される
以下は Amanity が実際に使っている check-mv-overwrite.sh の核心部分です。
#!/usr/bin/env bash
# PreToolUse hook: mv の上書きを事前検知して Yes/No プロンプトを発火
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# mv コマンドでなければ素通し
if [[ ! "$CMD" =~ ^[[:space:]]*mv([[:space:]]|$) ]]; then
exit 0
fi
# 移動先ファイルが既に存在する場合は確認を求める
if [[ -e "$DEST" ]]; then
cat <<JSON
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "上書き警告: $DEST が既に存在します。上書きしますか?"
}
}
JSON
exit 0
fi「mv コマンドが実行されようとしたとき、移動先にファイルが既に存在する場合は確認プロンプトを出す」というロジックです。Claude がどう判断しようとも、このスクリプトが確実に介入します。
.claude/settings.json をコミットすると全エージェントに強制適用される
.claude/settings.json は、プロジェクトのルートディレクトリにある .claude/ フォルダ内に置く設定ファイルです。個人の設定ファイル(~/.claude/settings.json)とは別物で、リポジトリに属する共有設定として扱われます。
このファイルに hooks を定義してリポジトリにコミットすると、そのリポジトリを使うすべての人・すべてのエージェントに同じ hooks 設定が自動で適用されます。「自分のマシンだけに設定した」「このエージェントには伝えたが別のエージェントには伝えていない」といった抜け漏れが構造的に発生しません。
考え方としては、インフラのコード化(IaC)と同じです。「セキュリティポリシーをドキュメントに書く」のではなく「コードとして定義してバージョン管理する」——hooks はその発想をエージェントの安全設計に適用したものです。
~/.claude/settings.json(ユーザーグローバル)に書いた場合はそのマシン上の自分だけに適用されますが、プロジェクトスコープで書くと「このリポジトリを触るエージェントは全員このルールに従う」という設計になります。
マルチエージェント環境で hooks が特に重要な理由
「誰が何のファイルを触っているか分からない」問題
Claude Code でサブエージェント(複数のエージェントが並走する構成)を使い始めると、独特の問題が生まれます。
たとえば秘書エージェントが調整業務をしながら、エンジニアエージェントがコードを修正し、マーケターエージェントがブログ記事を書いている状況があるとします。それぞれのエージェントは独立して動いており、「他のエージェントが今どこを触っているか」を知りません。
人間が1人で Claude Code を使う場合は「自分が何をやっているか」を把握できます。しかし複数エージェントが並走すると、1つのエージェントが「大丈夫だろう」と判断して実行した操作が、別のエージェントが作業中のファイルに影響を与えることがあります。
エージェントはルールを記憶しない——セッションをまたぐと初期状態に戻る
各エージェントは独立したセッションを持ちます。つまり、あるセッションで「このファイルは触らないように」という指示をしても、別のエージェントのセッションにはその記憶が引き継がれません。
CLAUDE.md に書いておけばある程度は補えますが、前述のとおり CLAUDE.md は確率的な参照です。確実に守らせたいルールは hooks に落とし込む必要があります。
1つの hooks 設定で全エージェントに同一のガードレールをかける
ここに hooks の強みがあります。.claude/settings.json に書いた hooks は、そのプロジェクトを触るすべてのエージェントに自動的に適用されます。
「上書き確認が必要な操作は必ず人間に確認する」というルールを hooks に書けば、秘書エージェントであろうとエンジニアエージェントであろうとマーケターエージェントであろうと、同じチェックを通過しない限りその操作は実行されません。個々のエージェントに同じ指示を書く必要はありません。
Amanity の実運用で入れている安全設計3原則
Amanity では、複数のエージェントが同一リポジトリを触る構成を運用しています。その中で実際に入れている hooks を、設計の考え方とともに紹介します。
原則①「戻せない操作」を PreToolUse でブロックする
mv による上書きや cp による上書きは、デフォルトでは確認なく実行されます。ファイルが上書きされてしまった後では元に戻すのが困難です。
Amanity では check-mv-overwrite.sh と check-cp-overwrite.sh を PreToolUse に登録し、上書きが発生するケースでは必ず確認プロンプトを出すようにしています。
# 移動先にファイルが存在する場合は "ask" を返す
if [[ -e "$DEST" ]]; then
cat <<JSON
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "上書き警告: $DEST が既に存在します。上書きしますか?"
}
}
JSON
fi「戻せない操作は hooks で止める」——これが1つ目の原則です。
原則②「in-place 編集」を安全にするバックアップ強制
sed -i でのインプレース編集はバックアップなしに元ファイルを書き換えます。macOS の場合は sed -i ''、GNU の場合は sed -i がバックアップなしの動作になります。
Amanity では check-sed-awk-inplace.sh を使って、バックアップ拡張子なしの sed -i が実行されようとした場合に確認プロンプトを出します。-i.bak のようにバックアップ拡張子が付いている場合はそのまま通過します。
# -i.<拡張子> 形式(-i.bak 等)が含まれていれば素通し
if [[ "$CMD" =~ -i.[a-zA-Z0-9_-]+ ]]; then
: # バックアップありなので安全
# -i '' や -i 単体はバックアップなし → 確認を求める
elif [[ "$CMD" =~ -i[[:space:]]+'' ]]; then
emit_ask "sed -i '' は in-place 編集(バックアップなし)です。実行しますか?"
elif [[ "$CMD" =~ -i[[:space:]] ]] && ! [[ "$CMD" =~ -i.[a-zA-Z0-9_-]+ ]]; then
emit_ask "sed -i は in-place 編集(バックアップなし)です。実行しますか?"
fi「副作用が残る操作には必ずバックアップを強制する」——これが2つ目の原則です。
原則③ 人間の意思決定が必要な場面で承認プロンプトを挟む
Stop イベントに hooks を登録すると、セッションが終了する直前に処理を挿入できます。Amanity では check-process-completion.sh を Stop フックに登録し、未完了の業務プロセス(チェックリスト形式で管理)が残っている場合はセッションの終了をブロックするようにしています。
# 未完了のチェックリスト(- [ ])が残っていたら exit 2 でブロック
UNCHECKED=$(grep -n "^- [ ]" "$file" 2>/dev/null)
if [ -n "$UNCHECKED" ]; then
echo "⚠️ 未完了のプロセスステップがあります。完了してから終了してください。"
exit 2
fi「エージェントが自己判断で完了を宣言する前に、人間が確認できる仕組みを挟む」——これが3つ目の原則です。
hooks 設計を始める前に決めておくべき3つのこと
スコープ設計——ユーザーグローバル vs プロジェクト
hooks は2か所に設定できます。
| 設定ファイル | 適用範囲 |
|---|---|
~/.claude/settings.json | そのマシン上のすべてのプロジェクト |
.claude/settings.json(プロジェクト) | そのプロジェクトを触るすべてのエージェント |
個人の作業習慣に関するルール(フォーマッターの自動実行など)はユーザーグローバルに、プロジェクト固有の安全策(本番 DB への接続禁止・スコープ外ファイルへの書き込み禁止など)はプロジェクトスコープに書くのが適切です。
マルチエージェント構成では、プロジェクトスコープにコミットすることで全エージェントへの一括適用が実現します。
ブロック vs 警告——どちらを選ぶかの判断基準
hooks でブロックするか、警告(ask)にとどめるかは以下の基準で判断します。
- 完全ブロック(exit 2 + stderr): 実行されたら取り返しがつかない操作(本番環境への直接デプロイ、システムファイルの削除など)
- 確認プロンプト(ask): 実行可能だが意図的かどうかを確認したい操作(ファイル上書き、in-place 編集など)
- 通過(exit 0): 問題なく自動実行してよい操作
Amanity の実運用では、mv/cp/sed の上書き・in-place 編集は「確認(ask)」としています。完全にブロックすると正当な操作もできなくなるため、確認プロンプトで人間の判断を挟む設計にしています。
タイムアウト設計——hooks が開発体験を重くしないために
hooks スクリプトが重すぎると、Claude Code の操作のたびにレスポンスが遅くなります。
PreToolUse には「軽い同期チェック」のみを置くことを原則とします。ファイルの存在確認やコマンド文字列のパースは数ミリ秒で終わるため問題ありません。重いテスト実行や外部 API への通信は PostToolUse に置くか、非同期で走らせる設計にします。
まとめ
CLAUDE.md と hooks の役割を整理すると、以下のようになります。
- CLAUDE.md: Claude に「こういう方針で動いてほしい」という意図を伝える指示書。確率的な動作ガイド
- hooks: Claude の動作に割り込んで処理を強制するシェルスクリプト。確定的な安全装置
CLAUDE.md が不要ということではありません。設計の意図や背景を Claude に伝えるには CLAUDE.md が最適です。しかし「絶対に守らせたいルール」「取り返しのつかない操作の防止」は hooks に落とし込む必要があります。
複数のエージェントが並走するマルチエージェント環境では、個々のエージェントへの指示よりも「リポジトリレベルで全員に適用されるルール」が重要になります。.claude/settings.json に hooks を書いてコミットするだけで、それが実現します。
「プロンプトに書けば伝わる」から「仕組みで防ぐ」への転換——これが AI 駆動開発を安全に運用するための次のステップです。
関連記事
株式会社Amanity
福岡を拠点に、AIを活用したモバイル・Web・LINEアプリの受託開発を行っています。マルチエージェント構成の設計や AI 駆動開発の導入についてお気軽にご相談ください。
無料で相談する