メインコンテンツへスキップ
注目 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+のテストに基づいています。動作はバージョン更新により変更される可能性があります。