CodeCraft Lab

Hooksの仕組みと基本設定

Hooksのライフサイクル、settings.jsonでの設定方法、基本的なHook実装、デバッグ方法を学びます

応用16分で読了
Hookssettings.jsonライフサイクル自動化

このレッスンで学ぶこと

  • Hooksとは何か、どのライフサイクルイベントで発火するか
  • settings.json内でのhooks設定の書き方とmatcherの仕組み
  • 基本的なHookを実際に実装し、動作させる
  • Hookが正しく動いているかをデバッグする方法

前提条件

  • Lesson 1〜3でMCPの基礎を理解していること(HooksはMCPとは独立した仕組みですが、組み合わせて使うことが多いため)
  • settings.jsonの基本的な構造(.claude/settings.json等)に触れたことがあること

Hooksとは何か

Claude Codeは、ツールを呼び出す・応答を返す・セッションを開始するといった特定のタイミング(ライフサイクルイベント)で動作します。Hooksは、そのタイミングに合わせて自分で指定したコマンドを自動実行する仕組みです。

たとえば「Bashツールでrm -rfが実行されそうになったら止める」「ファイルを保存したら自動的にフォーマッタを走らせる」「セッション開始時にGitのブランチ情報を自動的に伝える」といったことが、プロンプトでの指示なしに、確実に毎回実行されるようになります。

CLAUDE.mdとの違い

CLAUDE.mdに「コミット前にlintを実行してください」と書いても、Claude Codeがその指示を忘れたり、優先度を下げたりする可能性はゼロではありません。Hooksは「忘れる」という概念自体が存在しない、確定的に実行される自動化です。「必ず実行されてほしい処理」はCLAUDE.mdではなくHooksに任せるのが安全です。

Hooksのライフサイクル

Hooksが発火するタイミングは多数定義されていますが、実務でまず押さえておきたいのは以下の代表的なイベントです。

イベント発火タイミング主な用途
SessionStartセッション開始・再開時プロジェクトのコンテキスト(ブランチ名等)を自動注入する
UserPromptSubmitユーザーがプロンプトを送信した直後プロンプトの内容チェック、追加コンテキストの注入
PreToolUseツール実行危険なコマンドのブロック、入力値の検証・書き換え
PostToolUseツール実行成功後自動フォーマット、自動テスト実行、ログ記録
StopClaude Codeの応答が完了した時通知の送信、後処理の実行
SessionEndセッション終了時クリーンアップ処理
flowchart TD
    A["セッション開始"] -->|"SessionStart"| B["ユーザーがプロンプト送信"]
    B -->|"UserPromptSubmit"| C["Claude Codeが処理"]
    C --> D{"ツールを呼ぶ?"}
    D -->|Yes| E["PreToolUse"]
    E --> F["ツール実行"]
    F -->|"成功"| G["PostToolUse"]
    F -->|"失敗"| H["PostToolUseFailure"]
    G --> C
    H --> C
    D -->|No| I["応答完了"]
    I -->|"Stop"| J["セッション終了"]
    J -->|"SessionEnd"| K["完了"]
 
    style E fill:#fde8e8,stroke:#c0392b
    style G fill:#e8f4fd,stroke:#1a73e8

PreToolUseツール実行前に割り込めるため、危険な操作のブロックに向いています。一方PostToolUseは実行が完了した後に発火するため、ブロックはできず、結果に対する後処理(フォーマット、通知、ログ記録など)に向いています。この違いは設計上重要なので覚えておきましょう。

PostToolUseは実行を止められない

PostToolUseの時点では、すでにツールは実行済みです。ここでエラーを返しても「実行を未然に防ぐ」ことはできません。実行そのものを止めたい場合は、必ずPreToolUseを使う必要があります。

settings.jsonでのHooks設定

設定ファイルの置き場所

Hooksは複数の場所のsettings.jsonに書くことができ、スコープによって用途が異なります。

ファイルスコープ共有
~/.claude/settings.json全プロジェクト共通個人のみ(マシンローカル)
.claude/settings.jsonプロジェクト単位チーム共有(git管理対象)
.claude/settings.local.jsonプロジェクト単位個人のみ(gitignore対象)

CLAUDE.mdの運用方針にもあるとおり、チームで共有すべきHookは.claude/settings.jsonに、個人的な実験用のHookは.claude/settings.local.jsonに置くのが基本です。

JSON構造の基本形

{
  "hooks": {
    "イベント名": [
      {
        "matcher": "対象を絞り込む条件",
        "hooks": [
          {
            "type": "command",
            "command": "実行するコマンド"
          }
        ]
      }
    ]
  }
}

3階層になっている理由は、「どのイベントで」「どの対象に」「何を実行するか」を分離して指定できるようにするためです。1つのmatcherに複数のhooksを紐づけることもできます。

matcherの書き方

matcherは主にツール名に対するフィルタです。PreToolUsePostToolUseでは、対象はtool_nameになります。

書き方意味
省略 または "*"すべてのツールにマッチ全ツール共通の処理
ツール名そのもの完全一致"Bash"
|区切り複数ツールの一致"Edit|Write"
正規表現パターンマッチ"^Notebook""mcp__.*__write.*"
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'ファイルが編集されました'"
          }
        ]
      }
    ]
  }
}

MCPツールにマッチさせたい場合は、mcp__<サーバー名>__<ツール名>という命名規則を踏まえて正規表現を書きます。Lesson 3で作ったpricing-toolsサーバーの全ツールにマッチさせるなら"mcp__pricing-tools__.*"のようになります。

実践: 基本的なHookを実装する

危険なBashコマンド(rm -rf等)を検知してブロックする、PreToolUseの基本的なHookを作ってみましょう。

Hookスクリプトを作成する

プロジェクト直下に.claude/hooks/ディレクトリを作り、スクリプトを置きます。

mkdir -p .claude/hooks
#!/bin/bash
# .claude/hooks/block-dangerous-commands.sh
#
# Bashツールで危険なコマンドが実行されようとした場合にブロックするHook。
# stdin経由でPreToolUseイベントのJSONペイロードを受け取る。
 
# jqでtool_input.commandを取り出す
command=$(jq -r '.tool_input.command // empty')
 
# 危険なパターンにマッチするか確認する
if [[ "$command" =~ rm[[:space:]]+-rf[[:space:]]+/ ]] || [[ "$command" =~ ^rm[[:space:]]+-rf[[:space:]]*$ ]]; then
  # permissionDecision: deny でツール実行をブロックする
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "破壊的なrm -rfコマンドはHookによりブロックされました"
    }
  }'
  exit 0
fi
 
# 問題なければ何も出力せず終了(許可)
exit 0
chmod +x .claude/hooks/block-dangerous-commands.sh

settings.jsonにHookを登録する

.claude/settings.jsonに以下を追加します。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous-commands.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

${CLAUDE_PROJECT_DIR}を使うことで、Claude Codeをどのディレクトリから起動してもプロジェクトルートからの相対パスでスクリプトを解決できます。

動作を確認する

新しいセッションを開始し、危険なコマンドを実行させてみます。

カレントディレクトリでrm -rf /を実行してください

ツール実行が拒否され、理由として設定したpermissionDecisionReasonの内容が表示されることを確認します。

正常なコマンドが通ることも確認する

lsコマンドでカレントディレクトリのファイル一覧を見せてください

こちらは正常にツールが実行されることを確認し、Hookが過剰にブロックしていないかをチェックします。

Hookは最後の防衛線であり唯一の対策ではない

Hookによる危険コマンドのブロックは有効な対策ですが、正規表現のパターンを完璧に網羅することは困難です。CLAUDE.mdの禁止事項(rm -rfgit push --force等の破壊的コマンドの非実行)を遵守する運用と組み合わせ、Hookは「最後の安全網」として位置づけましょう。

Hookの入出力の仕組み

入力: stdin経由のJSON

コマンドHookは、stdin経由でそのイベントに関するJSONを受け取ります。PreToolUseの場合、最低限以下のような情報が含まれます。

{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/cache"
  },
  "cwd": "/path/to/project",
  "session_id": "abc123"
}

jqコマンドで必要なフィールドだけを取り出すのが定石です。

出力: 終了コードとJSON

Hookの結果は、終了コード標準出力のJSONの組み合わせで制御します。

終了コード意味
0成功。stdout のJSONで追加の制御を行える
2ブロッキングエラー。ツール実行を止める(stderrに理由を出力)
その他非ブロッキングエラー。stderrは表示されるが処理は続行される

PreToolUseでは、exit 2で止める方法と、JSON出力でpermissionDecision: "deny"を返す方法の2通りがあります。後者の方が「なぜ拒否したか」をUI上にきれいに表示できるため、本レッスンのサンプルでは後者を採用しています。

Hookのデバッグ方法

`/hooks`メニューで設定を確認する

Claude Codeセッション内で次を実行します。

/hooks

現在読み込まれているすべてのHookと、それぞれの設定元([Project][User]等)が一覧表示されます。意図したHookが登録されているかをまず確認しましょう。

スクリプト単体で動作確認する

Hookスクリプトは独立したシェルスクリプトなので、Claude Codeを介さずにテストできます。

echo '{"tool_input":{"command":"rm -rf /"}}' | .claude/hooks/block-dangerous-commands.sh

期待したJSONが出力されるかをここで確認すれば、「スクリプトの問題」か「Claude Code側の設定の問題」かを切り分けられます。

JSON出力の構文を検証する

Hookが期待どおりに動かない場合、出力したJSONの構文ミスが原因であることが多くあります。jqコマンドを通して文法エラーがないか確認しましょう。

.claude/hooks/block-dangerous-commands.sh < test-input.json | jq .

jqがパースエラーを返す場合、Claude Code側でもJSONとして解釈できず、Hookの出力が無視されてしまいます。

timeoutを疑う

Hookが何も起こさず無視されているように見える場合、スクリプトがtimeoutの値(デフォルト600秒だが明示的に短く設定していることもある)を超えて時間がかかっていないか確認します。外部コマンドの呼び出しを含むHookは特に注意が必要です。

exec フォームとシェルフォームの違いに注意

commandに加えてargsを指定すると「exec フォーム」となり、シェルを介さず直接プログラムが実行されます。パスにスペースを含む場合や、引数をシェル展開させたくない場合はこちらが安全です。逆にcommandだけを書く「シェルフォーム」は、パイプ(|)やリダイレクト(>)などシェルの機能を使いたい場合に向いています。意図せず混同すると、コマンドが正しく解釈されないトラブルにつながります。

よくある質問

Q. Hookの実行に失敗すると、Claude Codeの動作全体が止まりますか?

PreToolUseexit 2またはpermissionDecision: denyで意図的に止めた場合は、そのツール呼び出しだけがブロックされます。Hookスクリプト自体が予期しないエラー(構文エラー等)で落ちた場合も、基本的にはそのツール呼び出しに対するエラーとして扱われ、セッション全体が落ちることはありません。ただし、意図しない動作を避けるため、Hookスクリプトには適切なエラーハンドリングを入れておきましょう。

Q. 複数のHookが同じイベントに登録されている場合、どう処理されますか?

PreToolUseでは複数のHookの判定結果のうち、denyが最優先されます(deny > defer > ask > allowの優先順位)。1つでも拒否すれば、他のHookが許可していてもブロックされます。

まとめ

  • Hooksは特定のライフサイクルイベント(PreToolUsePostToolUseSessionStart等)で自動実行されるコマンドの仕組みで、CLAUDE.mdの指示と違い確実に実行される
  • settings.jsonhooksキーに、イベント名 → matcher → 実行コマンドという3階層で定義する
  • PreToolUseは実行前のブロックに、PostToolUseは実行後の後処理に向いている
  • Hookはstdin経由でJSONを受け取り、終了コードとstdoutのJSONで結果を返す
  • /hooksでの設定確認、スクリプト単体でのテスト、JSON構文検証の3段階でデバッグする

次のステップ

基本的なHookの仕組みが分かったところで、次のLesson 5では実務でよく使われるパターン——pre-commit的な品質チェック、応答完了時の通知、条件付きフックの組み合わせ——を扱い、実践コースで学んだワークフローにHooksを組み込んでいきます。