以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/11/25/234913より取得しました。


Claude Codeのフックってなんだ?

これは、なにをしたくて書いたもの?

Claude Codeのユーザーが拡張できる機能を見ていってみよう、というお題のひとつです。

今回はフックについて見ていきます。

毎度おなじみですが、Geminiの無料版でClaude Codeを使おうとするとレートリミット的に厳しかったです。

フック

フックは、Claude Codeの動作時の様々なポイントに組み込めるコールバックの仕組みです。実際にはシェルコマンドが
動作します。

Get started with Claude Code hooks - Claude Code Docs

リファレンスはこちら。

Hooks reference - Claude Code Docs

フックはサブエージェントやスキルなどと異なり、LLMによる判断に依存せずに実行されます。よって、Claude Codeが
ある処理を行った後に特定の動作を確実に行われるようになります。

よって用途は以下のようなものになります。

  • 通知
  • フォーマッターの適用
  • ログの記録
  • フィードバック

フックはClaude Codeを実行しているユーザーの権限で動作するため、現在の環境の認証情報などの権限をそのまま
使うことに注意する必要があります。

フック可能なイベントはこちら。

  • PreToolUse … ツール呼び出しの前(ブロック可能)
  • PermissionRequest … 使用許可を求める時(許可または拒否が可能)
  • PostToolUse … ツール呼び出しの完了後
  • UserPromptSubmit … ユーザーがプロンプトを呼び出し、Claudeが処理する前
  • Notification … Claude Codeが通知を送信した時
  • Stop … Claude Codeが応答を終了した時
  • SubagentStop … サブエージェントがタスクを完了した時
  • PreCompact … Claude CodeがCompactionを実行する前
  • SessionStart … Claude Codeが新しいセッションを開始するか、既存のセッションを再開する時
  • SessionEnd … Claude Codeがセッションを終了する時

Get started with Claude Code hooks / Hook Events Overview

フックを作成するには/hooksスラッシュコマンドを使うようです。

Get started with Claude Code hooks / Quickstart

あとはフックの例が続きます。たとえばツールの実行コマンドと説明をログに保存するフック。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"\\(.tool_input.command) - \\(.tool_input.description // \"No description\")\"' >> ~/.claude/bash-command-log.txt"
          }
        ]
      }
    ]
  }
}

ですが、カスタムスラッシュコマンドなどに比べると、少し説明が薄いですね。

リファレンスも見ていきましょう。

Hooks reference - Claude Code Docs

フックはsettings.jsonに書くようですが、フックの有効なスコープも同じになります。

  • $HOME/.claude/settings.json … ユーザーが操作できるプロジェクト全体で共通のフック
  • .claude/settings.json … プロジェクト単位のチームで共有できるフック
  • .claude/settings.local.json … プロジェクト単位だが、個人で利用するフック
  • エンタープライズ管理ポリシーでの設定

Hooks reference / Configuration

フックの定義方法はこんな感じですね。hooks配下に関心のあるイベントを書いていくようです。

{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here"
          }
        ]
      }
    ]
  }
}

Hooks reference / Structure

具体例。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
          }
        ]
      }
    ]
  }
}

少し要素を見ていきます。

  • matcher … ツール名に一致するパターン。PreToolUse、PostToolUse、PermissionRequestのみに提供
    • 単純な文字列による完全一致、正規表現の利用が可能。*と書くとすべてのツールに一致する
  • hooks … パターンが一致した時に実行するフックの配列
    • type … フック実行タイプ。Bashコマンド(command)またはLLMベースの評価用のプロンプト(prompt)を指定する
    • command … typeがcommandの場合に実行するコマンド。環境変数$CLAUDE_PROJECT_DIRを利用可能
    • prompt … typeがpromptの場合に、評価用にLLMに送信するプロンプト
    • timeout … (オプション)特定のフックをキャンセルするまでのタイムアウト

typeがpromptの場合の例。これはClaudeが終了した時にタスクの内容をすべて完了させているかチェックするプロンプトの
ようです。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks are complete."
          }
        ]
      }
    ]
  }
}

これはプロンプトベースのフックと呼ばれるようです。

Hooks reference / Prompt-Based Hooks

最初の説明を見ると、StopとSubagentStopイベントのみで使えるフックのように書かれているのですが。

Prompt-based hooks are currently only supported for Stop and SubagentStop hooks, where they enable intelligent, context-aware decisions.

その後の説明を見ると、どのフックイベントでも使えますよ、みたいなことが書かれています。どっちでしょう…?

Prompt-based hooks work with any hook event, but are most useful for:

Hooks reference / Prompt-Based Hooks / Supported hook events

bashコマンドでのフックとの使い分けはこちら。

Hooks reference / Prompt-Based Hooks / Comparison with bash command hooks

決まったことを高速に実行したい場合はbashコマンドでのフックですね。

各フックイベントの詳細。どのようなmatcherが指定可能かが書かれています。

Hooks reference / Hook Events

MCPに対するmatcherも書けるようです。

Hooks reference / Working with MCP Tools

フックの入力について。

Hooks reference / Hook Input

フックには、セッション情報とイベント固有のデータを含むJSONが渡されるようです。

{
  // Common fields
  session_id: string
  transcript_path: string  // Path to conversation JSON
  cwd: string              // The current working directory when the hook is invoked
  permission_mode: string  // Current permission mode: "default", "plan", "acceptEdits", or "bypassPermissions"

  // Event-specific fields
  hook_event_name: string
  ...
}

たとえばPreToolUseイベントだと、このようなJSONになるようです。

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  },
  "tool_use_id": "toolu_01ABC123..."
}

フックの出力について。フックによって起動されるbashコマンドは、終了コードや標準出力、標準エラー出力の内容で
Claude Codeにフィードバックするようです。

Hooks reference / Hook Output

プロンプトベースのフックの場合はまた違うようです。

Hooks reference / Prompt-Based Hooks / Response schema

bashコマンドでのフックについて見ていきましょう。

まずは終了コードの扱いから。

  • 終了コードが0 … 成功
    • 標準出力の内容がClaude Codeのコンテキストに追加される
    • JSON出力を返すことでより詳細に制御させることができる
  • 終了コードが2 … ブロッキングエラー
    • フィードバックには標準エラー出力のみが使われる。形式は[command]: {stderr}
    • JSON出力は使われない
  • それ以外の終了コード … 非ブロッキングエラー
    • 標準エラー出力の内容が、ユーザーにFailed with non-blocking status code: {stderr}という形式で表示される
    • 標準エラー出力に書き出さなかった場合は、ユーザーにはNo stderr outputと表示される

Hooks reference / Hook Output / Simple: Exit Code

終了コードが0以外の時には、標準出力は読まれないことに注意が必要ですね。

終了コード2を返した時の動作はこちら。たとえばPreToolUseではツールの呼び出しをブロックし、PermissionRequestでは
権限を拒否します。

Hooks reference / Hook Output / Simple: Exit Code / Exit Code 2 Behavior

JSON出力する場合の内容はこちら。

Hooks reference / Hook Output / Advanced: JSON Output

イベントの種類によらず、共通のフィールドはこちら。

{
  "continue": true, // Whether Claude should continue after hook execution (default: true)
  "stopReason": "string", // Message shown when continue is false

  "suppressOutput": true, // Hide stdout from transcript mode (default: false)
  "systemMessage": "string" // Optional warning message shown to the user
}

continueがfalseの場合は、Claudeはフック実行後に処理を停止します。stopReasonはcontinueがfalseの時に有効で、
ユーザーには表示されClaudeには表示されない停止理由が入ります。

suppressOutputはtrueにするとトランスクリプトモードの時に標準出力を含めないようにするもので、systemMessageは
オプションのユーザーに表示する警告メッセージというもののようです。

あとはイベント固有の出力があるようです。

たとえばPreToolUse。

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow"
    "permissionDecisionReason": "My reason here",
    "updatedInput": {
      "field_to_modify": "new value"
    }
  }
}

このあたりは各イベントの内容を見ておきましょう。

あとはセキュリティーに関する注意事項、フックの実行に関する詳細、デバッグ方法などが書かれています。

特にセキュリティーまわりは見ておいた方がよいでしょうね。コマンドは実行しているOSユーザーの権限で動作しますし、
ファイルアクセスなどもフックの定義内容に完全に依存します。また入力値をバリデーション、サニタイズしたり
ディレクトリートラバーサルのような脆弱性を作り込まないように注意が必要です。

単純にコマンドを実行しているだけだと思うので当たり前といえば当たり前ですが、フックを呼び出す元になるデータは
LLMが決定するのでそこがポイントなんでしょうね。

実行の詳細については、デフォルトでタイムアウトが60秒だったり、フックは並列実行されることなどがポイントですね。

では、フックを使ってみましょう。Claude Code+Claude Code Router(Gemini)で試します。

環境

今回の環境はこちら。

$ claude --version
2.0.53 (Claude Code)


$ ccr version
claude-code-router version: 1.0.71

Claude Code RouterはGeminiを使うように設定しています。

$HOME/.claude-code-router/config.json

{
  "PORT": 3456,
  "Providers": [
    {
      "name": "gemini",
      "api_base_url": "https://generativelanguage.googleapis.com/v1beta/models/",
      "api_key": "xxxxx",
      "models": ["gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.5-pro"],
      "transformer": {
        "use": ["gemini"]
      }
    }
  ],
  "Router": {
    "default": "gemini,gemini-2.5-flash",
    "think": "gemini,gemini-2.5-flash",
    "webSearch": "gemini,gemini-2.5-flash"
  }
}

起動。

$ ccr code

フックを使ってみる

まずは呼ばれたかどうかわかるフックを作ってみましょう。

.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "cat >> log.txt"
          }
        ]
      }
    ]
  }
}
> greeting.txtを作成してください。中身は こんにちは と書いてください。

● Write(greeting.txt)
  ⎿  Wrote 1 lines to greeting.txt
     こんにちは

● greeting.txt が作成され、内容として「こんにちは」が書き込まれました。

ログを見てみましょう。

log.txt

{"session_id":"bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe","transcript_path":"/home/user/.claude/projects/-home-user-project/bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe.jsonl","cwd":"/home/user/project","permission_mode":"default","hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"greeting.txt","content":"こんにちは"},"tool_use_id":"ccr_tool_uc482hl90ya"}

記録されていますね。

今度はファイルを読み込ませてみます。

> @greeting.txt
  ⎿  Read greeting.txt (1 lines)

● Read(greeting.txt)
  ⎿  Read 1 line

● 1→こんにちは

ログに追記されました。が、改行を入れるべきでした…。

log.txt

{"session_id":"bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe","transcript_path":"/home/user/.claude/projects/-home-user-project/bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe.jsonl","cwd":"/home/user/project","permission_mode":"default","hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"greeting.txt","content":"こんにちは"},"tool_use_id":"ccr_tool_uc482hl90ya"}{"session_id":"bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe","transcript_path":"/home/user/.claude/projects/-home-user-project/bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe.jsonl","cwd":"/home/user/project","permission_mode":"default","hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"greeting.txt"},"tool_use_id":"ccr_tool_vzenmmxczmj"}

ちょっとフォーマットしてみましょう。

{
  "session_id": "bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe",
  "transcript_path": "/home/user/.claude/projects/-home-user-project/bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe.jsonl",
  "cwd": "/home/user/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "greeting.txt",
    "content": "こんにちは"
  },
  "tool_use_id": "ccr_tool_uc482hl90ya"
}
{
  "session_id": "bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe",
  "transcript_path": "/home/user/.claude/projects/-home-user-project/bf5df2eb-5cdb-48d2-a6fd-5ea043e58dfe.jsonl",
  "cwd": "/home/user/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Read",
  "tool_input": {
    "file_path": "greeting.txt"
  },
  "tool_use_id": "ccr_tool_vzenmmxczmj"
}

こちらと見比べると雰囲気がわかりますね。

Hooks reference / Hook Input / PreToolUse Input

次は、Quickstartのサンプルを使ってみましょう。

Get started with Claude Code hooks / Quickstart

matcherにBashを追加。

.claude/settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "cat >> log.txt"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"\\(.tool_input.command) - \\(.tool_input.description // \"No description\")\"' >> bash-command-log.txt"
          }
        ]
      }
    ]
  }
}

呼び出したコマンドとdescriptionが記録されるはずです。

確認してみましょう。

> 現在の環境のPythonのバージョンを教えてください
  ⎿  Error: Exit code 127
     /bin/bash: 行 1: python: コマンドが見つかりません

● Bash(python3 --version)
  ⎿  Python 3.12.3

● 現在の環境のPythonのバージョンは3.12.3です。

結果。

bash-command-log.txt

python --version - Get Python version
python3 --version - Get Python 3 version

OKですね。

雰囲気はわかった気がします。

おわりに

Claude Codeのフックを試してみました。

今回はドキュメントを読むとだいたいわかった気がしますね。単純に特定のイベントに対するコールバックの仕組みなので。

ただ、それでもレートリミットにしょっちゅう引っかかるのは相変わらずなのですが。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/11/25/234913より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14