Hooks で Claude Code に「お約束」を覚えさせよう
Claude Code を使っていて、こんな経験はありませんか?
- 「ファイルを編集するたびに Prettier を手動で実行するのが面倒」
- 「
.envファイルを書き換えられてヒヤッとした」 - 「Claude の作業が終わったのに気づかず、しばらく放置してしまった」
こうした「毎回やるべきこと」「絶対にやってほしくないこと」を Claude に任せる仕組みが Hooks です。
Hooks は、Claude Code のライフサイクルの特定のタイミングで 自動的にシェルコマンドを実行する 機能です。スキルやサブエージェントが「Claude の能力を拡張する」ものなら、Hooks は「Claude の行動にガードレールやトリガーを仕掛ける」ものだと考えてください。
ポイントは、LLM の判断に頼らず 確実に実行される ということ。「Prettier を通してね」とプロンプトでお願いするのではなく、「ファイルが編集されたら必ず Prettier を実行する」と設定できるわけです。
Hooks をはじめる最速の方法
設定ファイルを手書きすることもできますが、一番簡単なのは /hooks メニューを使う方法です。
デスクトップ通知 Hook を作ってみよう
Claude が許可を求めて待っているのに気づかない…という問題を解決する通知 Hook を作ってみましょう。
1. /hooks を開く
Claude Code のプロンプトで /hooks と入力すると、利用可能なイベントの一覧が表示されます。ここから Notification を選びます。
2. マッチャーを設定する
マッチャーは「どんな条件で Hook を発火させるか」を決めるフィルターです。* を設定すればすべての通知で発火します。
3. コマンドを入力する
OS ごとに適切なコマンドを入力します。
macOS の場合:
osascript -e 'display notification "Claude が入力を待っています" with title "Claude Code"'
Linux の場合:
notify-send 'Claude Code' 'Claude が入力を待っています'
Windows の場合:
powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude が入力を待っています', 'Claude Code')"
4. 保存先を選ぶ
User settings を選べばすべてのプロジェクトで有効に、Project settings なら今のプロジェクトだけで有効になります。
これで完成です。Claude が許可を求めるタイミングでデスクトップ通知が届くようになります。
知っておきたい Hook イベント一覧
Hooks は Claude Code のライフサイクル上の「フック ポイント」にコマンドを差し込む仕組みです。よく使うイベントをピックアップして紹介します。
| イベント | 発火タイミング | 代表的な使い方 |
|---|---|---|
PreToolUse |
ツール実行の直前 | 危険なコマンドをブロック |
PostToolUse |
ツール実行の直後 | 編集後に自動フォーマット |
Notification |
Claude が通知を送るとき | デスクトップ通知 |
SessionStart |
セッション開始・再開時 | コンテキストの再注入 |
Stop |
Claude が応答を終えたとき | タスク完了チェック |
ConfigChange |
設定ファイルが変更されたとき | 変更の監査ログ |
他にも UserPromptSubmit(プロンプト送信時)、SubagentStart/Stop(サブエージェント起動・終了時)、PreCompact/PostCompact(圧縮前後)など、全部で 20 以上のイベントがあります。
Tip:
PreToolUseは ブロックできる イベントです。終了コード 2 を返すと、Claude はそのツール呼び出しを中止します。一方PostToolUseは実行済みなので、ログや後処理にしか使えません。この違いを押さえておくと設計がスムーズです。
実践レシピ集
レシピ 1:ファイル編集後に自動フォーマットする
Claude がコードを編集するたびに Prettier を自動実行します。手動でフォーマットし忘れることがなくなります。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
この設定を .claude/settings.json に追加します。
matcher が "Edit|Write" なので、ファイル編集ツールが使われたときだけ発火します。Bash の実行や Read には反応しません。
jq コマンドで Hook に渡される JSON から編集されたファイルパスを取り出し、Prettier に渡しています。
注意: この例では
jqが必要です。macOS ならbrew install jq、Ubuntu ならapt-get install jqでインストールできます。
レシピ 2:機密ファイルへの編集をブロックする
.env や package-lock.json への変更を防ぎたい場面は多いですよね。PreToolUse Hook でブロックできます。
まず .claude/hooks/protect-files.sh を作成します。
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "ブロック: $FILE_PATH は保護対象です($pattern)" >&2
exit 2
fi
done
exit 0
スクリプトを実行可能にして、.claude/settings.json に登録します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
ポイントは 終了コード 2 です。これが「ブロック」を意味し、stderr に書いた理由が Claude にフィードバックされます。Claude は「あ、このファイルは触れないんだな」と理解して、別のアプローチを提案してくれます。
レシピ 3:圧縮後に大事な情報を再注入する
長い会話をしていると、Claude のコンテキストウィンドウがいっぱいになり「圧縮」が走ります。圧縮すると会話が要約されるため、重要なルールを忘れてしまうことがあります。
SessionStart イベントの compact マッチャーを使えば、圧縮後に必ずリマインドを差し込めます。
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'リマインド: npm ではなく bun を使うこと。コミット前に bun test を実行すること。'"
}
]
}
]
}
}
stdout に書き出した内容が Claude のコンテキストに追加されるので、echo の代わりに git log --oneline -5 で最近のコミットを注入するなど、動的な情報も扱えます。
Hook の入出力を理解する
Hook の仕組みを正しく使うには、入力(stdin)と出力(終了コード・stdout・stderr)の流れを押さえておく必要があります。
入力:JSON で情報を受け取る
Hook が実行されると、stdin にイベントの詳細が JSON として渡されます。たとえば PreToolUse の場合:
{
"session_id": "abc123",
"cwd": "/home/user/my-project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/cache"
}
}
この JSON を jq や任意の言語でパースして、条件分岐に使います。
出力:終了コードで制御する
| 終了コード | 意味 |
|---|---|
| 0 | 正常終了。アクションを続行する |
| 2 | ブロック。stderr の内容が Claude へのフィードバックになる |
| その他 | 正常終了扱い。stderr はログに記録されるが Claude には見えない |
つまり、許可するなら exit 0、ブロックするなら exit 2 という 2 択がベースです。
構造化 JSON で細かく制御する
終了コードだけでは「許可 or ブロック」の 2 択しかできません。より細かい制御が必要なら、終了コード 0 で JSON を stdout に出力します。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "本番DBへの操作なのでユーザー確認が必要です"
}
}
permissionDecision には 3 つの選択肢があります。
"allow"— 許可プロンプトなしで続行"deny"— ツール呼び出しをキャンセル"ask"— ユーザーに許可プロンプトを表示
マッチャーでフィルタリングする
マッチャーを設定しないと、Hook はそのイベントのすべての発生で発火します。マッチャーを使うと、特定の条件のときだけ発火させられます。
マッチャーは正規表現をサポートしているので、柔軟なパターンが書けます。
"Edit|Write" → Edit または Write ツールにマッチ
"Bash" → Bash ツールのみにマッチ
"mcp__github__.*" → GitHub MCP サーバーの全ツールにマッチ
イベントによってマッチ対象が異なります。
PreToolUse/PostToolUse→ ツール名にマッチSessionStart→ 開始方法(startup,resume,compactなど)にマッチNotification→ 通知タイプ(permission_prompt,idle_promptなど)にマッチ
設定ファイルの置き場所
Hook をどこに置くかでスコープが変わります。
| 場所 | 適用範囲 | 共有 |
|---|---|---|
~/.claude/settings.json |
全プロジェクト | 自分だけ |
.claude/settings.json |
このプロジェクトのみ | チームで共有可(git コミット) |
.claude/settings.local.json |
このプロジェクトのみ | 自分だけ(gitignore 対象) |
Tip: 「Prettier の自動フォーマット」のようにプロジェクト全体のルールは
.claude/settings.jsonに、「デスクトップ通知」のような個人的な設定は~/.claude/settings.jsonに置くのがおすすめです。
プロンプト hooks で「判断が必要な」チェックをする
ここまでの Hook は「ファイル名に .env が含まれていたらブロック」のような決定論的なルールでした。でも、「このコードの品質は十分か?」のような判断が必要なチェックにはシェルスクリプトでは対応できません。
そこで使えるのが type: "prompt" の Hook です。Claude Code が別の LLM(デフォルトは Haiku)にプロンプトを送り、yes / no の判断を返してもらいます。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "ユーザーが依頼したタスクがすべて完了しているか確認してください。未完了のものがあれば {\"ok\": false, \"reason\": \"残りのタスク: ...\"} と返してください。"
}
]
}
]
}
}
LLM が "ok": false を返すと、Claude は作業を続行します。"reason" の内容が次の指示として使われるので、「テストを書き忘れている」「エラーハンドリングが抜けている」といった指摘が自動的にフィードバックされます。
エージェント hooks で「検証が必要な」チェックをする
プロンプト hooks は 1 回の LLM 呼び出しで判断しますが、「テストを実際に実行して結果を確認する」のようにファイルの読み取りやコマンド実行が必要な場合は エージェント hooks を使います。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "テストスイートを実行して、すべてのテストが通ることを確認してください。",
"timeout": 120
}
]
}
]
}
}
エージェント hooks はサブエージェントを生成し、ファイルを読んだりコマンドを実行したりしながら検証を行います。デフォルトのタイムアウトは 60 秒、最大 50 ターンです。
使い分けのコツ: Hook に渡される JSON データだけで判断できるならプロンプト hooks、実際にコードベースを調べる必要があるならエージェント hooks を選びましょう。
HTTP hooks で外部サービスと連携する
チーム全体の監査ログを残したり、外部の CI/CD サービスと連携したい場合は type: "http" が便利です。
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}
イベントデータが JSON として POST され、レスポンスボディで結果を返します。ヘッダーには $VAR_NAME で環境変数を埋め込めますが、allowedEnvVars にリストした変数だけが展開されるので安全です。
よくあるトラブルと解決法
Hook が発火しない
/hooksメニューで正しいイベントに登録されているか確認- マッチャーは大文字小文字を区別するので、
bashではなくBashと書く - 設定ファイルを手動で編集した場合は
/hooksを開くかセッションを再起動
exit 2 で止めたのに Claude が続行する
- stderr にメッセージを書いていないと、Claude にブロック理由が伝わりません
echo "理由" >&2を忘れずに
Stop Hook が無限ループする
Stop Hook は Claude が応答を終えるたびに発火します。Hook が "ok": false を返し続けると、Claude は永遠に作業を続けてしまいます。
stop_hook_active フラグをチェックして、2 回目以降はスキップしましょう。
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
# ここにチェックロジック
JSON パースエラーが出る
シェルの起動スクリプト(~/.zshrc など)に echo が書いてあると、Hook の JSON 出力の前に余計なテキストが混ざります。
# ~/.zshrc 内
if [[ $- == *i* ]]; then
echo "Shell ready" # インタラクティブシェルでだけ表示
fi
まとめ:Hooks でプロジェクトの「お作法」を自動化しよう
Hooks を活用すれば、Claude Code に「この操作の前後には必ずこれをやる」というルールを確実に守らせることができます。
- 決定論的なルール → コマンド hooks(
type: "command") - 判断が必要なチェック → プロンプト hooks(
type: "prompt") - 実際に検証が必要なチェック → エージェント hooks(
type: "agent") - 外部連携 → HTTP hooks(
type: "http")
まずは通知 Hook のような簡単なものから始めて、プロジェクトのルールに合わせて徐々に追加していくのがおすすめです。