これは、なにをしたくて書いたもの?
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": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh" } ] } ] } }
少し要素を見ていきます。
- matcher … ツール名に一致するパターン。PreToolUse、PostToolUse、PermissionRequestのみに提供
- 単純な文字列による完全一致、正規表現の利用が可能。
*と書くとすべてのツールに一致する
- 単純な文字列による完全一致、正規表現の利用が可能。
- hooks … パターンが一致した時に実行するフックの配列
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が指定可能かが書かれています。
MCPに対するmatcherも書けるようです。
Hooks reference / Working with MCP Tools
フックの入力について。
フックには、セッション情報とイベント固有のデータを含む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 / Prompt-Based Hooks / Response schema
bashコマンドでのフックについて見ていきましょう。
まずは終了コードの扱いから。
- 終了コードが0 … 成功
- 標準出力の内容がClaude Codeのコンテキストに追加される
- JSON出力を返すことでより詳細に制御させることができる
- 終了コードが2 … ブロッキングエラー
- それ以外の終了コード … 非ブロッキングエラー
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" } } }
このあたりは各イベントの内容を見ておきましょう。
あとはセキュリティーに関する注意事項、フックの実行に関する詳細、デバッグ方法などが書かれています。
- Hooks reference / Security Considerations
- Hooks reference / Hook Execution Details
- Hooks reference / Debugging
特にセキュリティーまわりは見ておいた方がよいでしょうね。コマンドは実行している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のフックを試してみました。
今回はドキュメントを読むとだいたいわかった気がしますね。単純に特定のイベントに対するコールバックの仕組みなので。
ただ、それでもレートリミットにしょっちゅう引っかかるのは相変わらずなのですが。