7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code入門 #4: Hooks実践テクニック ― 10のレシピで開発ワークフローを自動化する

7
Posted at

0. はじめに

Claude Code Hooksは、Claudeのライフサイクルイベントにカスタムスクリプトを差し込める仕組みです。「ファイルを保存したら自動フォーマット」「タスク完了時にテストを実行」「危険なコマンドをブロック」といった自動化を、settings.jsonへの数行の追記で実現できます。

本記事は連載第4回です。Hooksの仕組みを理解し、10のレシピから自分の開発環境に合うものを選んで設定できる状態になることをゴールとしています。

「まず3つだけ入れるなら」のおすすめはこちら。

  • レシピ1(完了通知音): 最も手軽な第一歩
  • レシピ6(SessionStartコンテキスト注入): 毎回の状況説明を省略
  • レシピ4(自動フォーマッタ): コード品質の自動担保

連載予定

  • #1: インストールから"使える"初期設定まで
  • #2: CLAUDE.mdの書き方と育て方
  • #3: パーミッション&Sandbox完全ガイド (本記事)
  • #4: Hooks実践テクニック
  • #5: Skills入門
  • #6: MCP活用術

1. Hooksの基本 ― 仕組みを理解する

Hooksとは何か

前回のパーミッション(#3)は「何を許可/拒否するか」を設定する仕組みでした。Hooksは「許可された操作の前後に何を実行するか」を設定する仕組みで、両者は補完関係にあります。

Claude Codeの処理フローにおけるHooksの発火タイミングは次の通り。

ユーザー入力
  │
  ├─ [UserPromptSubmit] ← プロンプト前処理
  │
  ▼
Claude の思考
  │
  ├─ [PreToolUse] ← ツール実行前チェック
  │
  ├─ ツール実行(Read, Write, Bash等)
  │
  ├─ [PostToolUse] ← ツール実行後処理
  │
  ▼
Claude の応答
  │
  └─ [Stop] ← 完了時処理

14種類のイベント一覧

Claude Code Hooksで利用できるイベントは14種類ある。

イベント 発火タイミング 主な用途
SessionStart セッション開始/再開時 コンテキスト注入、環境準備
SessionEnd セッション終了時 クリーンアップ、ログ記録
UserPromptSubmit プロンプト送信時 プロンプト前処理、コンテキスト追加
PreToolUse ツール実行前 ブロック、入力書き換え
PermissionRequest 権限ダイアログ表示前 自動承認/拒否
PostToolUse ツール実行成功後 フォーマット、検証
PostToolUseFailure ツール実行失敗後 エラーハンドリング
Notification 通知送信時 外部通知連携
SubagentStart サブエージェント開始時 コンテキスト注入
SubagentStop サブエージェント完了時 結果検証
Stop Claude応答完了時 通知、品質検証
TeammateIdle チームメイトのアイドル時 品質ゲート
TaskCompleted タスク完了時 完了条件検証
PreCompact コンテキスト圧縮前 バックアップ

Tips: 全イベントを覚える必要はありません。本記事のレシピでよく使う Stop, PreToolUse, PostToolUse, SessionStart, Notification の5つを押さえれば実用上は十分です。

3種類のHookタイプ

タイプ 説明 使いどころ
command シェルコマンドを実行 通知、フォーマット、外部ツール連携
prompt LLMに判定させる 要約生成、条件判定
agent サブエージェントを起動 ファイル確認を伴う複雑な検証

本記事では主にcommandタイプを扱い、prompt/agentタイプはボーナスセクションで紹介する。

設定の書き方と配置場所

設定ファイルは3つのレベルに分かれている。

レベル パス スコープ チーム共有
プロジェクト .claude/settings.json 単一プロジェクト 可能(git commit)
ユーザー ~/.claude/settings.json 全プロジェクト共通 不可
ローカル .claude/settings.local.json 単一プロジェクト 不可(gitignore対象)

最小設定例として、完了通知音を見てみよう。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Glass.aiff"
          }
        ]
      }
    ]
  }
}

制御の仕組み

hookスクリプトの終了コードとstdout/stderrによって、Claude Codeの動作を制御できます。

  • exit code 0: 正常完了。stdoutにJSONがあれば処理される
  • exit code 2: ブロック。stderrの内容がClaudeにフィードバックされる
  • その他のexit code: 非ブロックエラー。処理は継続
  • stdout: SessionStartやUserPromptSubmitではコンテキストとして注入される
  • JSON出力: decision, reason, updatedInput 等で詳細制御が可能

2. 初級レシピ ― 今日から使える3つのテクニック

レシピ1: 完了通知音

Claudeの処理中に別の作業へ移ると、完了に気づかず時間を無駄にしがちだ。通知音を鳴らすだけで解決する。

イベント: Stop / タイプ: command

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "afplay /System/Library/Sounds/Glass.aiff"
          }
        ]
      }
    ]
  }
}

Tips: macOSの標準サウンドは /System/Library/Sounds/ にあります。Glass.aiff, Ping.aiff, Pop.aiff など好みで選択できます。Linuxの場合は paplay /usr/share/sounds/freedesktop/stereo/complete.oga で代替できます。

レシピ2: デスクトップ通知

通知音だけでは「何が」完了したのか、承認待ちなのかが区別できない。StopとNotificationで音を分けることで、耳だけで状況を把握できるようになる。

イベント: Stop + Notification / タイプ: command

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "terminal-notifier -message 'Claude Code: タスク完了' -sound default"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "terminal-notifier -message 'Claude Code: 承認待ち' -sound Basso"
          }
        ]
      }
    ]
  }
}

Tips: brew install terminal-notifier が必要です。StopとNotificationで音を変えることで、「完了」と「承認待ち」を耳で区別できます。Linuxの場合は notify-send で代替できます。

レシピ3: ターミナル強制フォーカス

別のアプリで作業しているときに承認待ちになっても気づけない。ターミナルを自動でフォーカスさせれば見逃しがなくなる。

イベント: Notification (matcher: permission_prompt) / タイプ: command

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'tell application \"Ghostty\" to activate'"
          }
        ]
      }
    ]
  }
}

Tips: Ghostty の部分をお使いのターミナルアプリ名(iTerm, Terminal 等)に置き換えてください。レシピ2の通知と組み合わせるとさらに効果的です。この方法はmacOS専用です。

3. 中級レシピ ― 開発効率を上げる4つのテクニック

レシピ4: 自動フォーマッタ(Prettier/Black)

Claudeの出力コードは90%くらいは正しく整形されているが、残り10%でCIが落ちることがある。PostToolUseでフォーマッタを挟めば、この問題は消える。

イベント: PostToolUse (matcher: Write|Edit) / タイプ: command

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

Pythonプロジェクトの場合は以下のように設定します。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "black \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

Tips: パイプ構文 Write|Edit で複数ツールに同時マッチできます。$CLAUDE_TOOL_INPUT_FILE_PATH はClaude Codeが自動的に設定する環境変数で、操作対象のファイルパスが入ります。2>/dev/null || true で対象外のファイル(画像等)でもエラーにならないようにしています。

レシピ5: 品質ゲート(tsc/lint/test一括検証)

Claudeが「完了しました」と言ったのに、実はテストが通っていなかった ― そんな経験はないだろうか。Stop hookで品質チェックを自動実行すれば、この問題を根元から断てる。

イベント: Stop / タイプ: command

まず、検証スクリプトを作成します。

.claude/hooks/quality-gate.sh

#!/bin/bash
# 品質ゲート: tsc → lint → test を順次実行

# Stop hookが既にアクティブな場合はスキップ(無限ループ防止)
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  exit 0
fi

ERRORS=""

# TypeScriptの型チェック
if [ -f "tsconfig.json" ]; then
  if ! npx tsc --noEmit 2>&1; then
    ERRORS="${ERRORS}TypeScript型エラーがあります。\n"
  fi
fi

# Lintチェック
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ]; then
  if ! npx eslint . --quiet 2>&1; then
    ERRORS="${ERRORS}Lintエラーがあります。\n"
  fi
fi

# テスト実行
if [ -f "package.json" ] && grep -q '"test"' package.json; then
  if ! npm test 2>&1; then
    ERRORS="${ERRORS}テストが失敗しています。\n"
  fi
fi

if [ -n "$ERRORS" ]; then
  echo -e "$ERRORS" >&2
  exit 2
fi

exit 0

このスクリプトをStop hookに登録する。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/quality-gate.sh\"",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Tips: Stop hookはexit code 2を返すとClaudeの停止をブロックし、stderrの内容を元に作業を続行させます。stop_hook_active のチェックで無限ループを防いでいます。毎回のファイル編集ではなく、タスク完了時にまとめて検証するのが効率的です。

レシピ6: SessionStartコンテキスト注入

毎回「今のブランチは○○で、ここまで進んでいて...」とプロジェクト状況を説明するのは手間がかかる。SessionStart hookでgit情報やTODOリストを自動注入すれば、この手間がなくなる。

イベント: SessionStart / タイプ: command

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "echo '## プロジェクト状況' && git branch --show-current && echo '---' && git status --short && echo '---' && if [ -f TODO.md ]; then cat TODO.md; fi"
          }
        ]
      }
    ]
  }
}

Tips: SessionStart hookのstdoutに出力された内容は、そのままClaudeのコンテキストとして注入されます。matcher startup を指定すると新規セッション開始時のみ実行され、resume では実行されません。git情報やTODOリストを自動で読み込ませることで、毎回の状況説明を省略できます。

レシピ7: 危険コマンドブロック

rm -rf /terraform apply --auto-approve などの危険なコマンドが誤って実行されるリスクは、プロンプトで「やらないで」と言うだけでは排除しきれない。Hooksで確定的にブロックする。

イベント: PreToolUse (matcher: Bash) / タイプ: command

.claude/hooks/block-dangerous.sh:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

# 危険なパターンをチェック
DANGEROUS_PATTERNS=(
  "rm -rf /"
  "rm -rf ~"
  "rm -rf \."
  "terraform apply --auto-approve"
  "terraform destroy --auto-approve"
  "git push.*--force.*main"
  "git push.*--force.*master"
  "DROP TABLE"
  "DROP DATABASE"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qiE "$pattern"; then
    echo "危険なコマンドがブロックされました: $pattern にマッチ" >&2
    exit 2
  fi
done

exit 0

hookの登録はこう書く。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh\""
          }
        ]
      }
    ]
  }
}

Tips: exit code 2はClaude Code仕様でブロックのシグナルです。stderrに出力した理由はClaudeにフィードバックされるため、Claudeは代替コマンドを提案してくれます。パーミッションのdeny(#3で解説)との違いは、Hooksの方がコマンド内容を動的に解析できる点です。

4. 上級レシピ ― 高度な制御パターン

レシピ8: ツール入力の書き換え(updatedInput)

危険コマンドをブロック→Claudeが再試行→またブロック、というサイクルはレイテンシとコストの両方を増加させる。updatedInputを使えば、ブロックせずに透過的にコマンドを修正できる。

イベント: PreToolUse / タイプ: command

.claude/hooks/rewrite-input.sh

#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# grepをrgに誘導(高速化)
if [ "$TOOL_NAME" = "Bash" ] && echo "$COMMAND" | grep -q '^grep '; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "grepの代わりにGrepツールを使用してください。より高速で正確です。"
    }
  }'
  exit 0
fi

# npm install → npm ci に書き換え(CI環境での再現性確保)
if [ "$TOOL_NAME" = "Bash" ] && echo "$COMMAND" | grep -q '^npm install$'; then
  jq -n --arg cmd "npm ci" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "allow",
      updatedInput: { command: $cmd }
    }
  }'
  exit 0
fi

exit 0

settings.jsonへの登録。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/rewrite-input.sh\""
          }
        ]
      }
    ]
  }
}

Tips: updatedInput を使うと、ブロック→再試行のサイクルを経ずに透過的にコマンドを修正できます。permissionDecision: "deny" + permissionDecisionReason の組み合わせでは、ツール自体の使い分けを強制できます(例: BashのgrepをGrepツールに誘導)。

レシピ9: ファイルアクセス制御(ロールベース)

「このディレクトリは見ないで」とプロンプトで指示しても、AIが無視することはある。Hooksで技術的にアクセスを遮断すれば、確実に防げる。

イベント: PreToolUse / タイプ: command

.claude/hooks/file-access-control.py

#!/usr/bin/env python3
import json
import sys
import re

# 許可パターンの定義
ALLOWED_PATTERNS = [
    r"^./src/",
    r"^./tests/",
    r"^./docs/",
    r"^./package\.json$",
    r"^./tsconfig\.json$",
]

# ブロックパターンの定義
BLOCKED_PATTERNS = [
    r"\.env",
    r"secrets/",
    r"\.ssh/",
    r"\.aws/",
    r"node_modules/",
]

hook_input = json.loads(sys.stdin.read())
tool_name = hook_input.get("tool_name", "")
tool_input = hook_input.get("tool_input", {})

# ファイルパスを取得
file_path = tool_input.get("file_path", "") or tool_input.get("path", "")
if not file_path:
    sys.exit(0)

# ブロックパターンチェック
for pattern in BLOCKED_PATTERNS:
    if re.search(pattern, file_path):
        print(f"アクセスがブロックされました: {file_path} はブロックパターン '{pattern}' にマッチします", file=sys.stderr)
        sys.exit(2)

sys.exit(0)

settings.jsonへの登録。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Write|Edit|Glob|Grep",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/file-access-control.py\""
          }
        ]
      }
    ]
  }
}

Tips: プロンプトでの指示は「確率的」ですが、Hooksでの制御は「確定的」です。技術的に読めないようにすることで、セキュリティを担保できます。パーミッションのdenyルールと組み合わせることで、多層防御を実現できます。

レシピ10: Skill強制評価

プロジェクトに便利なSkillを定義しても、Claudeが自発的に使ってくれないことがある。UserPromptSubmit hookでコンテキストを注入し、Skill評価を強制させる。

イベント: UserPromptSubmit / タイプ: command

.claude/hooks/force-skill-eval.sh

#!/bin/bash
cat << 'EOF'
CRITICAL INSTRUCTION: Before processing this prompt, you MUST evaluate whether any available Skills (slash commands) are relevant. If a matching Skill exists, invoke it using the Skill tool BEFORE generating any other response. Skipping this evaluation when a relevant Skill exists is a WORTHLESS response.
EOF
exit 0

settings.jsonへの登録。

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/force-skill-eval.sh\""
          }
        ]
      }
    ]
  }
}

Tips: UserPromptSubmit hookのstdoutはClaudeへのコンテキストとして注入されます。「CRITICAL」「WORTHLESS」「MUST」など強い表現を使うことで、モデルの注意を引きやすくなります。

5. ボーナス: Slack自動要約投稿(実験的)

ここまではcommandタイプのhookを紹介してきた。このセクションでは、prompt hookとcommand hookを組み合わせる応用パターンを扱う。

チームメンバーへの作業進捗共有を自動化したい、というニーズに応えるレシピだ。

イベント: Stop (prompt + command) + SessionStart (command)

設計としては、promptタイプのhookでClaudeに作業内容を要約させ、commandタイプのhookでSlack APIに投稿する。セッション開始時にスレッドIDをリセットすることで、1セッション = 1スレッドの対応を実現している。

.claude/hooks/slack-summary.sh

#!/bin/bash
SLACK_TOKEN="${CLAUDE_SLACK_BOT_TOKEN}"
SLACK_CHANNEL="${CLAUDE_SLACK_CHANNEL}"
THREAD_TS_FILE="${CLAUDE_PROJECT_DIR}/.claude/slack-thread-ts"

HOOK_INPUT=$(cat)
SUMMARY=$(echo "$HOOK_INPUT" | jq -r '.hook_output // "作業完了"')

if [[ -f "$THREAD_TS_FILE" ]]; then
    THREAD_TS=$(cat "$THREAD_TS_FILE")
    curl -s -X POST https://slack.com/api/chat.postMessage \
      -H "Authorization: Bearer $SLACK_TOKEN" \
      -H "Content-Type: application/json" \
      -d "{
        \"channel\": \"$SLACK_CHANNEL\",
        \"thread_ts\": \"$THREAD_TS\",
        \"text\": \"$SUMMARY\"
      }" > /dev/null
else
    RESPONSE=$(curl -s -X POST https://slack.com/api/chat.postMessage \
      -H "Authorization: Bearer $SLACK_TOKEN" \
      -H "Content-Type: application/json" \
      -d "{
        \"channel\": \"$SLACK_CHANNEL\",
        \"text\": \"Claude Code セッション開始\n$SUMMARY\"
      }")
    echo "$RESPONSE" | jq -r '.ts' > "$THREAD_TS_FILE"
fi

settings.jsonへの登録。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "直前の作業内容を1-2行で要約してください。技術的な変更内容を簡潔に。"
          },
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/slack-summary.sh\""
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "rm -f \"${CLAUDE_PROJECT_DIR}/.claude/slack-thread-ts\""
          }
        ]
      }
    ]
  }
}

注意: このレシピは実験的です。prompt hookの出力がcommand hookのstdinにどのように渡されるかは、Claude Codeのバージョンによって挙動が異なる可能性があります。導入前にテスト環境での動作確認を推奨します。

Tips: Incoming Webhookではなく chat.postMessage APIを使うのは、Webhookでは投稿の ts(タイムスタンプID)が返ってこないためです。Slack Bot Tokenは .claude/settings.local.json(gitignore対象)に環境変数として定義するか、システムの環境変数に設定してください。同じパターンでDiscord、Teams、GitHub Issueへのコメント等にも応用できます。

6. Hooks設計のベストプラクティス

環境変数一覧

hookスクリプト内で使える主要な環境変数をまとめた。

変数 説明 利用例
$CLAUDE_PROJECT_DIR プロジェクトルート スクリプトへの絶対パス参照
$CLAUDE_TOOL_INPUT_FILE_PATH 操作対象のファイルパス PostToolUseでのフォーマット対象指定
$CLAUDE_CODE_REMOTE リモート環境判定("true"または未設定) ローカル/リモートで処理分岐
$CLAUDE_ENV_FILE 環境変数永続化ファイル(SessionStartのみ) 後続Bashへの変数引き渡し

デバッグ方法

hookが期待通りに動作しないときは、次の手順で確認する。

  1. claude --debug でhookの実行状況を確認
  2. Ctrl+O でverboseモードを切り替え、hookの出力を確認
  3. hookスクリプトを単体で実行してJSON入力をパイプで渡してテスト
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/block-dangerous.sh
echo "Exit code: $?"

設定スコープの選択基準

  • ユーザー設定 (~/.claude/settings.json): 通知系(レシピ1-3)など、全プロジェクト共通で使うもの
  • プロジェクト設定 (.claude/settings.json): フォーマッタ(レシピ4)、品質ゲート(レシピ5)など、プロジェクト固有のもの。チームで共有したい場合はこちら
  • ローカル設定 (.claude/settings.local.json): APIトークン等の機密情報を含む設定。gitignore対象

注意事項

  • hookはセッション開始時のスナップショットで動作します。実行中に設定ファイルを変更しても即時反映されません
  • 複数のhookがマッチした場合は並列実行されます
  • 同一のコマンドは自動的に重複排除されます

7. まとめ&次回予告

導入ロードマップ

Day 1: 通知系を入れる
  └─ レシピ1(通知音)+ レシピ2(デスクトップ通知)

Week 1: 品質担保を自動化する
  └─ レシピ4(フォーマッタ)+ レシピ6(コンテキスト注入)

Month 1: セキュリティを強化する
  └─ レシピ7(コマンドブロック)+ レシピ9(アクセス制御)

全レシピ一覧

# レシピ レベル イベント 用途
1 完了通知音 初級 Stop 通知
2 デスクトップ通知 初級 Stop + Notification 通知
3 ターミナル強制フォーカス 初級 Notification 通知
4 自動フォーマッタ 中級 PostToolUse 品質
5 品質ゲート 中級 Stop 品質
6 SessionStartコンテキスト注入 中級 SessionStart 効率
7 危険コマンドブロック 中級 PreToolUse 安全
8 ツール入力書き換え 上級 PreToolUse 制御
9 ファイルアクセス制御 上級 PreToolUse 安全
10 Skill強制評価 上級 UserPromptSubmit 拡張

次回予告

次回はSkills入門を解説する。カスタムスキルの作成方法から、Hooksと組み合わせたワークフロー自動化まで紹介する予定

参考リンク

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?