【Claude Code 連載 第7回】Hooks——「お願い」ではなく「必ず起きる」を作る

スポンサーリンク
【Claude Code 連載 第7回】Hooks——「お願い」ではなく「必ず起きる」を作る 用語解説
【Claude Code 連載 第7回】Hooks——「お願い」ではなく「必ず起きる」を作る
この記事は約11分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

1. これは一言でいうと何か

Hooks は、Claude Code のライフサイクルの特定のポイントで実行される、ユーザー定義のシェルコマンドです。狙いは一点、決定論的な制御です。LLM が「実行することを選ぶ」のに依存するのではなく、特定のアクションが常に起きることを保証します。

前回までの整理と重ねると位置づけが見えます。CLAUDE.md やスキルに書いた「.env は編集しない」はリクエストであって保証ではありません。フックはそこを埋める層です。プロジェクトルールの強制、反復作業の自動化、既存ツールとの統合に使います。

2. どういう場面で役立つか

  • Claude の待ち時間に気づけないNotification イベントはClaude が入力や許可を待っているときに発火します。ターミナルを監視し続けずに他の作業へ切り替えられます。
  • フォーマットの揺れをレビューで指摘したくないPostToolUseEdit|Write にマッチさせ、Claude が編集したファイルへ Prettier を必ず走らせます。「フォーマットしてください」と CLAUDE.md に書くのとは信頼性が違います。
  • 触られたくないファイルがある.envpackage-lock.json.git/ などへの編集を PreToolUse でブロックできます。Claude にはブロック理由がフィードバックされるため、別のアプローチに切り替えられます。
  • 圧縮で大事な前提が消えるSessionStartcompact マッチャーで使うと、圧縮のたびに規約や現在のスプリント状況を再注入できます。
  • 監査ログを残したいPostToolUseBash マッチャーで実行コマンドを追記したり、ConfigChange で設定・スキルファイルの変更を記録したりできます。

向かないケース・不要なケース。

  1. 毎セッション同じコンテキストを入れたいだけなら、フックではなく CLAUDE.md を検討します(ドキュメント自身がそう案内しています)。フックが要るのは「圧縮後だけ」のようなタイミング条件があるときです。
  2. ハードな allow / deny の強制はフックの仕事ではありません。後述の if フィルタは失敗時オープン(Bash コマンドを解析できなければフックを実行する)でベストエフォートです。確実に許可・拒否したいなら権限システムを使います。
  3. やってしまったことの取り消しはできません。PostToolUse はツールが既に実行された後に発火するため、防ぎたいなら PreToolUse です。
  4. 判断が必要な条件は決定論的ルールでは書けません。その場合は後述の type: "prompt" / type: "agent" を使います。

3. コード・コマンドの実例と解説

前提条件

  • フックは設定ファイルの hooks ブロックに書きます。場所がスコープを決めます。~/.claude/settings.json(全プロジェクト、マシンローカル)、.claude/settings.json(プロジェクト、リポジトリにコミット可)、.claude/settings.local.json(プロジェクト、gitignored)、管理ポリシー設定(組織全体)、プラグインの hooks/hooks.json、スキルやエージェントのフロントマター。
  • 設定済みフックの確認は /hooks です。このメニューは読み取り専用で、追加・変更・削除は JSON を直接編集するか、Claude に依頼します。
  • このページの Bash 例は JSON 解析に jq を使います(brew install jq / apt-get install jq)。
  • すべて無効化するには "disableAllHooks": true。ただし管理設定で設定されたフックは、そこにも同じ設定がない限り実行されます。

最初のフック:入力待ちを通知する

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

~/.claude/settings.json に追加します(例は macOS。Linux は notify-send、Windows は PowerShell を使います)。空の matcherすべての通知タイプで発火します。絞りたい場合は permission_prompt(ツール使用の承認が必要)、idle_prompt(Claude が完了して次のプロンプト待ち)などを指定します。/hooksNotification を選び、登録されているか確認してください。

既に hooks キーがある場合は、オブジェクトごと置き換えるのではなく、イベント名を既存キーの兄弟として追加します。

編集後に必ずフォーマットする

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

プロジェクトルートの .claude/settings.json に追加します。Edit|Write マッチャーによりファイル編集ツールの後だけ実行され、BashRead では発火しません。フックはイベントデータを JSON として stdin で受け取るので、jqfile_path を抜き出して Prettier に渡す、という流れです。

なお、Claude は Bash 経由でもファイルを変更できます。全変更を漏れなく捕捉したい監査用途では、ターンごとに作業ツリーを走査する Stop フックを足す方法がドキュメントで案内されています。

保護ファイルへの編集をブロックする

#!/bin/bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

.claude/hooks/protect-files.sh に保存し、chmod +x で実行可能にします(macOS / Linux では必須)。ここが終了コードの意味を理解する最良の教材です。exit 2 はアクションをブロックし、stderr に書いた理由が Claude へのフィードバックになりますexit 0 は「異議なし」で、通常の許可フローがそのまま適用されます(承認したことにはなりません)。その他の終了コードは、エラー通知を出しつつアクションは続行されます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

.claude/settings.json に登録します。スクリプトの参照には $CLAUDE_PROJECT_DIR を使うのが安全です(相対パスは「command not found」の典型的な原因です)。

圧縮後にコンテキストを再注入する

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
          }
        ]
      }
    ]
  }
}

SessionStartUserPromptSubmitUserPromptExpansion の各フックでは、stdout に書いたテキストがそのまま Claude のコンテキストに追加されますechogit log --oneline -5 のような動的コマンドに差し替えれば、直近のコミットを毎回思い出させることもできます。

判断が要るなら、モデルに聞く

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
          }
        ]
      }
    ]
  }
}

type: "prompt" は、シェルコマンドの代わりにプロンプトとフック入力を Claude モデル(デフォルトは Haiku)に渡し、yes/no を JSON で返させます"ok": false を返すと、Stop の場合は reason が Claude へのフィードバックになり、作業が続行されます。ファイルを読んだりコマンドを実行したりして検証する必要があるなら type: "agent" ですが、こちらは実験的で、本番ワークフローにはコマンドフックが推奨されています。

その他(type: "http" / type: "mcp_tool"、構造化 JSON 出力による permissionDecisionif フィールドによる引数レベルのフィルタ、CwdChangedFileChanged と direnv 連携、PermissionRequest による自動承認、ConfigChange 監査、全 30 弱のイベント一覧、非同期フック)は公式ドキュメントを参照してください。

4. まとめと次回予告

  • フックはライフサイクルの決まった地点で必ず走る、決定論的な層です。「お願い」で足りないものはここに置きます。
  • 基本は 3 つ。PreToolUse で防ぎ、PostToolUse で整え、SessionStart / Notification で流れを整える。
  • 終了コードが契約です。exit 2 でブロック(stderr が Claude へのフィードバック)、exit 0 は異議なし。

権限との関係は必ず押さえてください。 PreToolUse フックは権限モードのチェックより前に発火します。したがって deny を返すフックは、bypassPermissions モードや --dangerously-skip-permissions でもツールをブロックします。逆は成り立ちません。allow を返すフックは、設定の deny ルールを覆せません。フックは制限を厳しくできますが、許可ルールが許す範囲を超えて緩めることはできない、という非対称性です。複数フックが同一イベントにマッチした場合も、最も制限的な答えが勝ちます(denydeferaskallow の順)。

踏みやすい罠を 2 つ。 Stop フックは「タスク完了時」ではなくClaude が応答を終えるたびに発火します。ブロックし続けると無限ループになるため、Claude Code は進捗のないブロックが 8 回連続した時点で上限として打ち切ります。スクリプト側では JSON 入力の stop_hook_active を見て、true なら早期に exit 0 してください。もう 1 つ、シェルプロファイル(~/.zshrc など)に無条件の echo があると、その出力がフックの JSON の前に混ざってパースエラーになります。if [[ $- == *i* ]] でインタラクティブシェルに限定して回避します。

なお、フックはシェルコマンドを自動実行する仕組みです。共有したり本番環境にデプロイしたりする前に、公式の「セキュリティに関する考慮事項」を確認してください。

次回予告:外部サービスとつなぐ「MCP」を取り上げます。

関連ページ(本記事で触れた概念の詳細):全イベントスキーマ・JSON 出力形式・非同期フック →「Hooks リファレンス」、deny ルールと許可モード →「権限」、設定ファイルの優先順位 →「設定」、CLAUDE.md との使い分け →「メモリ」、スキルやエージェントのフロントマターに書くフック →「Skills」「サブエージェント」、配布 →「プラグイン」。


本記事は執筆時点の公式ドキュメント(hooks でアクションを自動化する)に基づきます。最新は公式ドキュメントをご確認ください。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました