Claude Code Hooks 完全ガイド2026:PreToolUse/PostToolUse/Stopの実践設定と活用パターン
Claude Code Hooksの全イベント種別・設定方法・exit codeの動作を体系的に解説。危険コマンドブロック・linting自動化・デスクトップ通知など実際に使える設定例を多数収録。
エンジニアのゆとです。
Claude Code Hooksを使い始めるきっかけは大体「rm -rfを実行されそうになって怖い」か「ファイルを編集したあとlintが走ってほしい」のどちらかだと思う。
実際、Hooksはその両方を解決できる。PreToolUseでツール実行前に介入し、PostToolUseで実行後にlintを走らせ、Stopで応答終了後にテストを検証できる。Claudeが何かをする前、した後、喋り終えた後の3タイミングで自分のスクリプトを割り込ませられる仕組みだ。
設定はsettings.jsonに書くだけで、シェルスクリプト・Python・HTTPエンドポイントなどを呼び出せる。一度理解すると「なぜ今まで設定していなかったのか」と思うくらいには実用的だった。
この記事でHooksの全体像を整理する。
Hooksとは何か:ライフサイクルへの割り込み
HooksはClaude Codeのライフサイクルに自分のスクリプトをフックする仕組みだ。具体的には以下のタイミングで発火する。
SessionStart(セッション開始)
↓
UserPromptSubmit(プロンプト送信時)
↓
PreToolUse(ツール実行前) ← ブロック可能
↓
[ツール実行]
↓
PostToolUse(ツール実行後)
↓
Stop(応答終了後) ← ブロック可能
最重要の特性はブロック可否だ。PreToolUseとStopはブロック可能。PostToolUseはすでに実行済みなのでブロックできない。
ユースケースは大きく分けると3類型になる。
- 安全ガード:危険なBashコマンドやファイル操作を事前にブロック
- 品質自動化:ファイル編集後のlint・フォーマット・テスト実行
- UX改善:デスクトップ通知、ログ記録、環境の自動セットアップ
フック種別と設定方法
設定ファイルの場所
HooksはClaude Codeのsettings.jsonに記述する。スコープによって配置場所が違う。
| ファイル | スコープ | Git管理 |
|---|---|---|
~/.claude/settings.json | 全プロジェクト共通 | しない |
.claude/settings.json | プロジェクト固有 | できる |
.claude/settings.local.json | プロジェクト固有(個人設定) | しない |
チーム共有したいルールはプロジェクトの.claude/settings.jsonに書く。個人的な好みはsettings.local.jsonか~/.claude/settings.jsonへ。
基本構造
設定は3層構造になっている。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/script.sh"
}
]
}
]
}
}
- イベント名(
PreToolUseなど) - matcher(どのツールに反応するか)
- hookハンドラ(何を実行するか)
matcherの書き方
matcherはツール名でフィルタリングする。
"matcher": "Bash" // Bashツールのみ
"matcher": "Edit|Write" // EditまたはWrite
"matcher": "mcp__memory__.*" // memoryサーバーの全ツール(正規表現)
"matcher": "*" // 全ツール
MCP toolの場合はmcp__サーバー名__ツール名という形式で指定する。
Hookハンドラの種類
1. command(最も基本的)
シェルコマンドを実行する。stdinでJSONを受け取り、stdoutでJSONを返す。
{
"type": "command",
"command": "/path/to/script.sh",
"timeout": 60,
"async": false
}
argsを指定するとExec形式、省略するとShell形式になる。
// Shell形式(パイプや&&が使える)
{ "command": "npm run lint && echo done" }
// Exec形式(パス展開が安全)
{ "command": "node", "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check.js"] }
${CLAUDE_PROJECT_DIR}はプロジェクトルートに展開される。
2. http(外部サービス連携)
JSONをPOSTして、レスポンスで判断を返す。
{
"type": "http",
"url": "http://localhost:9000/validate",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
},
"allowedEnvVars": ["API_TOKEN"]
}
チーム内の検証サーバーや監査システムに繋ぎたい場合に使う。
3. prompt(AI評価)
Claudeに評価プロンプトを送って判断させる。
{
"type": "prompt",
"prompt": "このBashコマンドは安全ですか?$ARGUMENTS",
"model": "claude-opus-4-8",
"timeout": 30
}
判断ロジックを自分で書きたくないときに便利だが、レイテンシが増える。
Exit codeとstdoutの動作
ここを理解していないとHooksが意図通り動かない。
| exit code | 意味 | JSONの扱い |
|---|---|---|
| 0 | 成功 | stdoutのJSONを処理 |
| 2 | ブロックエラー | JSONは無視、stderrの内容がClaudeに渡る |
| その他 | 非ブロックエラー | JSONは無視、stderrが表示される |
重要なのはexit 2はツール呼び出しを拒否するが、exit 1はエラーを報告して処理を続行する点だ。
#!/bin/bash
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
if echo "$command" | grep -qE 'rm -rf'; then
echo "危険なコマンドです" >&2
exit 2 # ブロック
fi
exit 0 # 通過
主要イベントの使い方
PreToolUse:実行前にブロック・変換
最も活用機会が多いイベント。permissionDecisionで判断を返す。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rfは禁止しています"
}
}
permissionDecisionの値:
"allow"— 実行を許可"deny"— 実行を拒否"ask"— ユーザーに確認ダイアログを出す"defer"— 通常フローに任せる(返さなくても同じ)
入力の変換もできる。updatedInputに置換後のツール入力を渡せば、Claudeが指定したコマンドではなく書き換えたものが実行される。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": { "command": "echo 'replaced'" }
}
}
PostToolUse:実行後にlint・フォーマット
ファイル編集後の自動検証に使う。ブロックはできないが、additionalContextでClaudeに追加情報を渡せる。
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "ESLintエラー: no-unused-vars at line 12"
}
}
これでClaudeが自動的にlintエラーを修正する動きになる。
Python実装例:
#!/usr/bin/env python3
import json, sys, subprocess
data = json.load(sys.stdin)
file_path = data.get('tool_input', {}).get('file_path', '')
if not file_path.endswith(('.ts', '.tsx')):
sys.exit(0)
result = subprocess.run(['npx', 'eslint', '--format=compact', file_path],
capture_output=True, text=True)
if result.returncode != 0:
output = {
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": f"ESLintエラー:\n{result.stdout[:500]}"
}
}
print(json.dumps(output))
sys.exit(0)
Stop:応答完了後に検証
Claudeが返答し終えたタイミングで発火する。テスト実行やデプロイ前チェックに使う。
exit 2を返すとターンを継続させられる(エラーメッセージとともにClaudeに渡る)。
#!/bin/bash
result=$(npm test 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "$result" >&2
exit 2 # テスト失敗をClaudeに伝えてリトライを促す
fi
exit 0
Notification:デスクトップ通知
Claude Codeが通知を出すタイミングで発火する。terminalSequenceを返すとターミナルのOSCシーケンスとして送信される。
#!/bin/bash
input=$(cat)
body=$(jq -r '.message // "Needs attention"' <<<"$input")
# macOS通知(osascript)
osascript -e "display notification \"$body\" with title \"Claude Code\""
Ghostty/Warpではターミナルシーケンス方式も使える。
{
"terminalSequence": "\033]777;notify;Claude Code;作業が完了しました\007"
}
実用的な設定例
危険コマンドブロック(最低限入れたい設定)
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bash-guard.sh"
}
]
}
]
}
}
# .claude/hooks/bash-guard.sh
#!/bin/bash
set -euo pipefail
input=$(cat)
command=$(jq -r '.tool_input.command // ""' <<<"$input")
# 破壊的操作をブロック
if echo "$command" | grep -qE '(rm -rf /|truncate -s 0|> /dev/sda|dd if=)'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "破壊的コマンドはHooksによりブロックされています"
}
}'
exit 0
fi
exit 0
TypeScript/Pythonファイル編集時の自動lint
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash",
"args": ["./.claude/hooks/lint.sh"],
"timeout": 30
}
]
}
]
}
}
# .claude/hooks/lint.sh
#!/bin/bash
input=$(cat)
file=$(jq -r '.tool_input.file_path // ""' <<<"$input")
case "$file" in
*.ts|*.tsx)
result=$(npx eslint --format=compact "$file" 2>&1 | head -20)
if [ -n "$result" ]; then
jq -n --arg ctx "ESLint: $result" '{
hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}
}'
fi
;;
*.py)
result=$(python3 -m ruff check "$file" 2>&1 | head -10)
if [ -n "$result" ]; then
jq -n --arg ctx "Ruff: $result" '{
hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $ctx}
}'
fi
;;
esac
exit 0
Gitコミット前の自動テスト(Stopフック)
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/test-on-stop.sh",
"timeout": 120
}
]
}
]
}
}
# .claude/hooks/test-on-stop.sh
#!/bin/bash
# ステージングにファイルがある場合のみテスト実行
staged=$(git diff --cached --name-only 2>/dev/null)
[ -z "$staged" ] && exit 0
result=$(npm test 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "テスト失敗。修正してください:" >&2
echo "$result" | tail -20 >&2
exit 2 # Claudeにフィードバックして継続
fi
exit 0
SessionStartで環境確認
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-env.sh"
}
]
}
]
}
}
# .claude/hooks/check-env.sh
#!/bin/bash
issues=()
command -v node &>/dev/null || issues+=("Node.js未インストール")
command -v docker &>/dev/null || issues+=("Docker未インストール")
[ -f ".env" ] || issues+=(".envファイルなし")
if [ ${#issues[@]} -gt 0 ]; then
ctx=$(printf '%s\n' "${issues[@]}")
jq -n --arg ctx "⚠️ 環境確認:\n$ctx" '{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: $ctx
}
}'
fi
exit 0
Hooksの確認・デバッグ
設定が正しく読み込まれているかは/hooksコマンドで確認できる。
/hooks
PreToolUse [1 hook]
[command] bash-guard.sh
matcher: Bash
PostToolUse [1 hook]
[command] lint.sh
matcher: Write|Edit
デバッグ時はMCP_TOOL_TIMEOUT環境変数でタイムアウトを延ばしたり、スクリプトにset -xを入れてトレースを出力するのが早い。
全フックを一時的に無効化したいときはsettings.jsonに"disableAllHooks": trueを追加する。
よくある落とし穴
exit codeの混同が最も多い。exit 2はブロック用で、単なるスクリプトエラーでも意図せずブロックが発動することがある。エラー系は全部exit 1に統一してstderrに出力するのが安全。
jsonの出力を忘れるパターンも多い。exit 0でもJSONを出力しなければpermissionDecisionはdefer扱いになる。明示的に許可したい場合は"allow"を返す。
timeoutのデフォルトは60秒。重いテストをStopフックで実行する場合は"timeout": 120などを明示しておかないと途中で打ち切られる。
まとめ
Hooksを使うと、Claude Codeの動作に「自分のルール」を組み込める。最低限やっておきたいのはPreToolUseでの危険コマンドガードとPostToolUseでのlint自動化の2つ。この2つだけでも開発体験がかなり変わる。
設定はプロジェクトの.claude/settings.jsonに書けばGitで共有できるので、チーム全員に同じガードが適用される。「うっかりClaudeにrm -rfを実行させてしまった」という事故を防ぐためにも、早めに設定しておくことをすすめる。