CodeCraft Lab

Hooksでワークフローを自動化する

pre-commit的な品質チェック、応答完了時の通知、条件付きフック、複数Hookの組み合わせパターンを実践します

応用15分で読了
Hooks自動化CI通知

このレッスンで学ぶこと

  • pre-commit的なフックでコード品質を自動チェックする実践パターン
  • 応答完了時(Stop)に通知を送る実践パターン
  • ifフィールドを使った条件付きフックの活用方法
  • 複数Hookを組み合わせて1つのワークフローを構成するパターン
  • 実践コースで学んだGitワークフローへのHooks組み込み

前提条件

  • Lesson 4「Hooksの仕組みと基本設定」を修了していること
  • 実践コースLesson 6「Git操作を効率化する」を一読していること(本レッスンの最後で連携します)

Lesson 4ではHooksの基本構造とデバッグ方法を学びました。このレッスンでは、実務で「使える」形に仕上げるための具体的なパターンを4つ紹介します。

パターン1: pre-commit的な品質チェック

実践コースのGit Workflowレッスンでは、Claude Codeにコミットメッセージを生成させる方法を扱いました。ここにHooksを組み合わせると、「コミット前に必ずlintとテストを通す」というルールを、指示し忘れの余地なく強制できます。

設計方針

PreToolUseBashツールを監視し、git commitを含むコマンドが実行されようとしたタイミングでlintとテストを走らせます。失敗した場合はコミット自体をブロックします。

#!/bin/bash
# .claude/hooks/pre-commit-check.sh
#
# git commitの直前にlintとテストを実行し、失敗時はコミットをブロックする
 
command=$(jq -r '.tool_input.command // empty')
 
# git commit を含むコマンドのときだけチェックする
if [[ "$command" != *"git commit"* ]]; then
  exit 0
fi
 
# lintを実行
if ! npm run lint --silent > /tmp/lint-result.log 2>&1; then
  reason=$(tail -n 20 /tmp/lint-result.log)
  jq -n --arg reason "lintエラーのためコミットを中止しました:\n${reason}" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: $reason
    }
  }'
  exit 0
fi
 
# テストを実行
if ! npm test --silent > /tmp/test-result.log 2>&1; then
  reason=$(tail -n 20 /tmp/test-result.log)
  jq -n --arg reason "テスト失敗のためコミットを中止しました:\n${reason}" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: $reason
    }
  }'
  exit 0
fi
 
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-commit-check.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}
if フィールドでより簡潔に絞り込む

上の例ではスクリプト内でgit commitを含むかチェックしていますが、ifフィールドを使えばHookの呼び出し自体を絞り込めます。Bashパターンの評価は&&区切りのサブコマンドも個別にチェックされるため、git add . && git commit -m "..."のような複合コマンドにも対応します。

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "if": "Bash(git commit *)",
      "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-commit-check.sh",
      "timeout": 120
    }
  ]
}

この書き方なら、スクリプト内のコマンド判定ロジックが不要になり、Hook自体が無関係なBashコマンドのたびに起動することもなくなります。

PostToolUseでの軽量な自動フォーマット

コミット前の重いチェックとは別に、ファイル保存のたびに軽量なフォーマッタを走らせるパターンも実務でよく使われます。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx",
            "args": ["prettier", "--write", "${tool_input.file_path}"],
            "timeout": 10
          }
        ]
      }
    ]
  }
}

PreToolUseの重いチェック(lint・テスト)はコミットの瞬間だけ、PostToolUseの軽いチェック(フォーマット)は編集のたびに、と粒度を分けることで、開発のテンポを落とさずに品質を担保できます。

パターン2: 応答完了時の通知

長時間かかるタスクをClaude Codeに任せて離席する場面では、Stopイベント(Claude Codeの応答が完了したタイミング)で通知を送ると便利です。

#!/bin/bash
# .claude/hooks/notify-on-stop.sh
#
# 応答完了時にSlackへ通知を送る
 
# stdinからセッション情報を受け取る(必要なら活用する)
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id')
 
# Slack Webhook URLは環境変数で管理し、ハードコードしない
if [ -z "$SLACK_WEBHOOK_URL" ]; then
  exit 0
fi
 
curl -s -X POST "$SLACK_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{\"text\": \"Claude Codeのタスクが完了しました (session: ${session_id})\"}" \
  > /dev/null
 
exit 0
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/notify-on-stop.sh",
            "timeout": 10,
            "async": true
          }
        ]
      }
    ]
  }
}

Stopはマッチャーを持たないイベントなので、matcherフィールドは省略します。またasync: trueを指定することで、通知の送信を待たずにClaude Codeの制御をユーザーに返せます。通知処理がネットワーク遅延で詰まっても、対話のテンポに影響しません。

Webhook URLやトークンはハードコードしない

CLAUDE.mdの方針どおり、Slack Webhook URLのような認証情報はスクリプト内に直接書き込まず、環境変数経由で渡します。.claude/settings.jsonをチームで共有する場合は特に注意し、シークレットは各自の環境(シェルのプロファイル等)で設定する運用にしましょう。

パターン3: 条件付きフックの活用

すべてのHookを常時有効にすると、開発体験を損なうことがあります。ifフィールドを使えば、特定の条件下でのみHookを発火させられます。

例: 本番環境を指す操作だけを警告する

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(*prod*)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/confirm-prod-operation.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# .claude/hooks/confirm-prod-operation.sh
#
# "prod"という文字列を含むコマンドの実行を一律でブロックし、
# 人間による確認を促す
 
command=$(jq -r '.tool_input.command // empty')
 
jq -n --arg cmd "$command" '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "ask",
    permissionDecisionReason: ("本番環境に関連するコマンドです。内容を確認してください: " + $cmd)
  }
}'
exit 0

permissionDecisionにはdeny(拒否)だけでなくask(人間に確認を求める)も指定できます。完全にブロックするほどではないが、機械的に許可したくない操作にはaskが適しています。

例: 特定のディレクトリ配下だけにフォーマッタを適用する

#!/bin/bash
# .claude/hooks/format-if-src.sh
 
file_path=$(jq -r '.tool_input.file_path // empty')
 
# src/ 配下のファイルのみフォーマットする
if [[ "$file_path" == */src/* ]]; then
  npx prettier --write "$file_path"
fi
 
exit 0

ifフィールドでBashコマンドの内容に基づく絞り込みができる一方、ファイルパスなど他の条件で絞り込みたい場合は、このようにスクリプト内で判定するのが現実的です。

パターン4: 複数Hookの組み合わせ

実務のワークフローは、1つのHookだけで完結しないことがほとんどです。ここでは「コード変更 → コミット前チェック → コミット → 通知」という一連の流れを、複数のHookの組み合わせで構成します。

flowchart TD
    A["ファイル編集"] -->|"PostToolUse: Edit/Write"| B["自動フォーマット"]
    B --> C["git commit実行を指示"]
    C -->|"PreToolUse: Bash(git commit)"| D{"lint/test<br/>パス?"}
    D -->|No| E["コミットをブロック"]
    D -->|Yes| F["コミット実行"]
    F -->|"PostToolUse: Bash(git commit)"| G["コミット内容をログに記録"]
    G --> H["応答完了"]
    H -->|"Stop"| I["Slack通知"]
 
    style B fill:#e8f4fd,stroke:#1a73e8
    style E fill:#fde8e8,stroke:#c0392b
    style I fill:#fff3e0,stroke:#e65100
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx",
            "args": ["prettier", "--write", "${tool_input.file_path}"],
            "timeout": 10
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git commit *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/log-commit.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git commit *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-commit-check.sh",
            "timeout": 120
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/notify-on-stop.sh",
            "timeout": 10,
            "async": true
          }
        ]
      }
    ]
  }
}

同じイベント内に複数のmatcherエントリを並べることで、Edit/Write用とBash用のように異なる対象への処理を1つの設定の中で整理できます。Hookの数が増えてきたら、スクリプトを.claude/hooks/配下に機能ごとに分割し、settings.json側はそれぞれを呼び出すだけのシンプルな構成に保つのが、見通しのよい運用につながります。

Hookを増やしすぎない

Hookは確実に実行される分、増えすぎると「何が起きているか把握しにくい」「実行のたびに時間がかかる」という副作用も生みます。CLAUDE.mdの「長すぎるドキュメントは重要なルールを埋もれさせる」という考え方はHooksにも当てはまります。本当に自動化が必要な処理だけをHookにし、それ以外はCLAUDE.mdでの指示やプロンプトでの都度依頼に任せるバランス感覚が重要です。

実践コースのワークフローとの連携

実践コースLesson 6「Git操作を効率化する」では、コミットメッセージ規約をCLAUDE.mdに書く方法、gh CLIと連携したPR作成ワークフローを学びました。Hooksはこれを補完する形で組み込めます。

実践コースで学んだことHooksで補完できること
CLAUDE.mdにコミットメッセージ規約を書くPreToolUseでコミット前のlint/testを強制する(規約違反のコミットそのものを防ぐ)
gh pr createでPRを作成するPostToolUseでPR作成後に自動的にラベルを付与する、Slackに通知する
ブランチ整理をClaude Codeに依頼するPreToolUsegit push --force等の危険な操作をaskで確認させる
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git push --force*)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/confirm-force-push.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

このように、「CLAUDE.mdで方針を伝える」「Hooksで確実に強制する」という2つの層を組み合わせることで、実践コースで身につけたワークフローを、より安全で再現性の高いものへと発展させられます。

よくある質問

Q. HooksとCLAUDE.mdのルールが矛盾したらどうなりますか?

Hooksはプログラム的に確定的に実行されるため、CLAUDE.mdの指示より優先されます。たとえばCLAUDE.mdに「lintは省略してよい」と書いてあっても、PreToolUseでlintを強制するHookがあれば、そちらが優先して実行されます。チームでHookを導入する際は、CLAUDE.mdの記述と矛盾しないよう整合性を取っておきましょう。

Q. Hookのスクリプトもテスト対象にすべきですか?

すべきです。CLAUDE.mdの「新規コード・変更には必ずテストを作成する」という方針は、Hookスクリプトにも当てはまります。特にコミットをブロックするような重要なHookは、想定する入力(JSON)を使ったユニットテストを用意し、意図どおりにdeny/allowを返すか確認しておくと安心です。

まとめ

  • pre-commit的なチェックはPreToolUse + if: "Bash(git commit *)"で、コミットそのものをブロックできる
  • Stopイベント + async: trueで、対話のテンポを崩さずに通知を送れる
  • ifフィールドで対象を絞り込み、Hookが無関係な場面で発火するのを防ぐ
  • 複数のHookは同じイベント内に複数のmatcherエントリとして整理し、スクリプトは機能ごとに分割する
  • HooksはCLAUDE.mdでの指示を「確実に実行される自動化」として補完する関係にあり、増やしすぎないバランス感覚も重要
応用コースのこの先

MCP(Lesson 1〜3)とHooks(Lesson 4〜5)は、いずれもClaude Codeを拡張する仕組みです。次に学ぶSkills(スラッシュコマンド)やサブエージェントと組み合わせることで、さらに高度な自動化ワークフローを構築できます。組織への展開を検討する場合は、ガイドのClaude Codeセキュリティ設計ガイドも参考にしてください。

次のステップ

応用コースはここからSkills(スラッシュコマンド)の作成、サブエージェントの設計へと続きます。MCPで外部ツールと繋がり、Hooksで確実な自動化を組み込んだ今、次はClaude Code自身の振る舞いをカスタマイズする方法を学んでいきましょう。