注目
Hooks Automation Configuration Development
Claude Code Hooks開発ガイド:完全実践マニュアル
入出力仕様、スクリプトテンプレート、よくあるエラーの解決策まで、Claude Code Hooks開発を完全マスター。
2026年1月17日 • 20分で読める • 著者:Claude World
HooksはClaude Codeの強力な拡張メカニズムで、ツール実行の前後にカスタムロジックを注入できます。本ガイドでは実践経験をまとめ、よくある落とし穴を避けてHooks開発を素早くマスターできるようにします。
Hookタイプ概要
| Hookタイプ | トリガータイミング | 用途 |
|---|---|---|
| PreToolUse | ツール実行前 | 危険な操作をブロック、コンテキスト追加 |
| PostToolUse | ツール実行後 | 操作を記録、後処理 |
| SessionStart | セッション開始時 | 環境読み込み、情報表示 |
| Stop | 自動化ループ終了時 | ループを継続するか決定 |
入出力仕様
入力形式(stdin JSON)
すべてのHookはJSON形式の入力を受け取ります:
{
"tool_name": "Bash",
"tool_input": {
"command": "ls -la",
"file_path": "/path/to/file",
"content": "file content..."
},
"tool_response": "command output..."
}
よく使うフィールド:
tool_name- ツール名(Bash, Write, Edit, Readなど)tool_input.command- Bashコマンドtool_input.file_path- ファイルパスtool_input.content- Writeのファイル内容tool_input.new_string- Editの新しい内容tool_response- ツール実行結果(PostToolUse)
出力形式
1. 操作を許可(出力なし)
exit 0
2. 操作をブロック(exit 2 + stderr)
echo "🛡️ BLOCKED: 理由の説明" >&2
exit 2
3. コンテキストを追加(PreToolUse)
jq -n --arg ctx "コンテキストメッセージ" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": $ctx
}
}'
exit 0
4. Stop Hookで停止を阻止
jq -n --arg reason "継続理由" '{
"decision": "block",
"reason": $reason
}'
exit 0
⚠️ 重要:旧形式はサポートされなくなりました
# ❌ 間違い(hook errorを引き起こす)
echo '{"decision": "allow"}'
# ✅ 正しい
exit 0
スクリプトテンプレート
PreToolUse - ブロック型(Security Guard)
#!/bin/bash
# Security Guard - 危険なコマンドをブロック
# PreToolUse hook for Bash tool
# 入力を読み取り
HOOK_INPUT=$(cat)
COMMAND=$(echo "$HOOK_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")
# コマンドがなければスキップ
[[ -z "$COMMAND" ]] && exit 0
# 危険なパターンをチェック
# 1. ルートディレクトリを削除
if echo "$COMMAND" | grep -qE 'rm\s+-[rf]+\s+/($|[^a-zA-Z])' 2>/dev/null; then
echo "🛡️ BLOCKED: Delete root filesystem (rm -rf /)" >&2
exit 2
fi
# 2. リモートスクリプトをシェルにパイプ
if echo "$COMMAND" | grep -qE '(curl|wget).*\|\s*(sh|bash)' 2>/dev/null; then
echo "🛡️ BLOCKED: Pipe remote script to shell" >&2
exit 2
fi
# 3. main/masterへのforce push
if echo "$COMMAND" | grep -qE 'git\s+push.*--force.*\s+(main|master)' 2>/dev/null; then
echo "🛡️ BLOCKED: Force push to main/master" >&2
exit 2
fi
# 実行を許可
exit 0
PreToolUse - コンテキスト型(Git Context)
#!/bin/bash
# Git Context - Gitステータスコンテキストを追加
# PreToolUse hook for Bash tool
# stdinを消費(重要!必要なくても消費する)
cat > /dev/null
# gitリポジトリかチェック
if ! git rev-parse --git-dir &>/dev/null; then
exit 0
fi
# Git情報を収集
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
# コンテキストを出力
jq -n --arg ctx "[Git] Branch: $BRANCH, Uncommitted: $UNCOMMITTED files" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": $ctx
}
}'
exit 0
PreToolUse - 警告型(File Guard)
#!/bin/bash
# File Guard - 機密ファイルを警告
# PreToolUse hook for Write/Edit tools
HOOK_INPUT=$(cat)
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")
[[ -z "$FILE_PATH" ]] && exit 0
FILENAME=$(basename "$FILE_PATH")
WARNINGS=()
# 機密パターンをチェック
if echo "$FILENAME" | grep -qiE '^\.env$|^\.env\.' 2>/dev/null; then
WARNINGS+=("Environment file may contain secrets")
fi
if echo "$FILENAME" | grep -qiE '\.pem$|\.key$|^id_rsa' 2>/dev/null; then
WARNINGS+=("File may be a private key")
fi
# 警告を出力
if [[ ${#WARNINGS[@]} -gt 0 ]]; then
WARNING_TEXT=$(printf '%s; ' "${WARNINGS[@]}")
jq -n --arg warn "⚠️ Sensitive file ($FILENAME): $WARNING_TEXT" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": $warn
}
}'
fi
exit 0
PostToolUse - 記録型
#!/bin/bash
# Log File Change - ファイル変更を記録
# PostToolUse hook for Write/Edit tools
HOOK_INPUT=$(cat)
TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // empty' 2>/dev/null || echo "")
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")
[[ -z "$TOOL_NAME" ]] && exit 0
[[ -z "$FILE_PATH" ]] && exit 0
# ログディレクトリを確保
mkdir -p .claude/logs 2>/dev/null
# 変更を記録
case "$TOOL_NAME" in
Write)
echo "[$(date -Iseconds)] WRITE: $FILE_PATH" >> .claude/logs/file-changes.log
;;
Edit)
echo "[$(date -Iseconds)] EDIT: $FILE_PATH" >> .claude/logs/file-changes.log
;;
esac
exit 0
Stop Hook - ループ制御
#!/bin/bash
# Post Auto Check - 自動化ループを制御
# Stop hook
AUTO_DIR=".auto"
AC_FILE="$AUTO_DIR/acceptance-criteria.json"
STOP_FILE="$AUTO_DIR/stop"
# 状態ディレクトリがなければ停止を許可
if [ ! -d "$AUTO_DIR" ]; then
exit 0
fi
# 手動停止シグナルをチェック
if [ -f "$STOP_FILE" ]; then
rm -rf "$AUTO_DIR"
exit 0
fi
# AC完了率をチェック
if [ -f "$AC_FILE" ] && command -v jq &> /dev/null; then
PASSED=$(jq '[.criteria[] | select(.status=="passed")] | length' "$AC_FILE" 2>/dev/null || echo "0")
TOTAL=$(jq '.criteria | length' "$AC_FILE" 2>/dev/null || echo "0")
if [ "$TOTAL" -gt 0 ] && [ "$PASSED" -lt "$TOTAL" ]; then
# 停止をブロック、ループを継続
jq -n --arg reason "AC完了率: $PASSED/$TOTAL、継続実行" '{
"decision": "block",
"reason": $reason
}'
exit 0
fi
fi
# 停止を許可
exit 0
設計パターン
グローバル共有 + 名前空間
hooksをユーザーレベルにインストールし、すべてのプロジェクトで共有:
~/.claude/hooks/bootstrap-kit/
├── pre-tool-use/
│ ├── security-guard.sh
│ ├── git-context.sh
│ ├── file-guard.sh
│ └── architecture-guard.sh
├── log-bash-event.sh
├── log-file-change.sh
├── session-start/
│ └── session-start.sh
└── auto/
└── post_auto_check.sh
settings.json設定
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/Users/username/.claude/hooks/bootstrap-kit/pre-tool-use/security-guard.sh"
}
]
},
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/Users/username/.claude/hooks/bootstrap-kit/pre-tool-use/file-guard.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/Users/username/.claude/hooks/bootstrap-kit/log-bash-event.sh"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "/Users/username/.claude/hooks/bootstrap-kit/session-start/session-start.sh"
}
]
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "/Users/username/.claude/hooks/bootstrap-kit/auto/post_auto_check.sh"
}
]
}
]
}
}
よくあるエラーと解決策
1. Hook Errorが続く
症状:
PreToolUse:Bash hook error: [/path/to/hook.sh]: ...
原因:旧形式 {"decision": "allow"} を使用
解決:exit 0(出力なし)を使用
2. パスがロードできない
症状:Hookが実行されない
原因:settings.jsonで $HOME や ${HOME} を使用
解決:絶対パスを使用
// ❌ 間違い
"command": "$HOME/.claude/hooks/my-hook.sh"
// ✅ 正しい
"command": "/Users/username/.claude/hooks/my-hook.sh"
3. Hooks同士が上書きし合う
症状:設定したhooksが効かない
原因:複数のsettingsファイルに競合する設定
解決:以下のファイルのhooksをチェックしてクリア:
~/.claude/settings.local.json.claude/settings.local.json
4. stdin未消費によるエラー
症状:Broken pipeまたはhook異常
原因:スクリプトがstdinを読み取っていない
解決:必要なくてもstdinを消費する
cat > /dev/null
# または
HOOK_INPUT=$(cat)
5. JSON出力形式エラー
症状:コンテキストが表示されない、またはhook error
原因:JSON構造が正しくない
解決:jqでJSONを生成
# ❌ 手動連結はエラーを起こしやすい
echo '{"hookSpecificOutput":{"additionalContext":"msg"}}'
# ✅ jqを使用
jq -n --arg ctx "msg" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": $ctx
}
}'
テスト方法
1. 直接実行テスト
# スクリプト構文をテスト
bash -n /path/to/hook.sh
# 入力をシミュレートしてテスト
echo '{"tool_input":{"command":"ls"}}' | /path/to/hook.sh
2. Claude Codeでテスト
# PreToolUse (Bash) をテスト
echo "test command"
# PreToolUse (Write) をテスト - 警告が出るか観察
# .envファイルに書き込み
# Security Guardをテスト - ブロックされるはず
rm -rf /
3. ログをチェック
# ファイル変更ログをチェック
cat .claude/logs/file-changes.log
# イベントログをチェック
cat .claude/logs/events.log
ベストプラクティス
1. 常にstdinを消費する
HOOK_INPUT=$(cat)
# または
cat > /dev/null
2. jqでJSON処理
COMMAND=$(echo "$HOOK_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")
3. 防御的プログラミング
# 空値をチェック
[[ -z "$COMMAND" ]] && exit 0
# エラー処理
command -v jq &> /dev/null || exit 0
4. stderrでブロックメッセージ
echo "🛡️ BLOCKED: reason" >&2
exit 2
5. 絶対パスを使用
"command": "/Users/username/.claude/hooks/..."
6. スクリプト権限
chmod +x /path/to/hook.sh
7. ログ出力はstderrへ
# デバッグメッセージ
echo "[DEBUG] something" >&2
参考資料
本ガイドはClaude Code v2.1.9+のテストに基づいています。動作はバージョン更新により変更される可能性があります。