カテゴリー / MCP・スキル・プラグイン

Claude Code hooksの設定先と終了コード、動かない時の確認手順

この記事の目次

Claude Code hooksは、ツール実行の前後や応答の終了、通知といった決まったタイミングで、登録したシェルコマンドを自動で実行する仕組みです。設定ファイルのhooksキーにイベント名ごとにmatcher(対象の絞り込み)と実行するコマンドを書き、PreToolUseなどブロックできるイベントでは、コマンドが終了コード2で終わればその操作を止められます。書いたら/hooksで読み込まれたかを確かめ、動かなければ置き場所、matcherの綴り、JSONの構文、実行権限の順に見ます。以下は公式のhooksガイドとhooksリファレンスの記述だけで構成しています。

Claude Code hooksはCLAUDE.mdと何が違うのか

公式ガイドはhooksを、LLMが実行を選ぶかどうかに頼らず、決まった動作を毎回起こすための仕組みだと説明しています。CLAUDE.mdに「編集後はフォーマッタを実行する」と書いてもClaudeへの指示にとどまりますが、hooksなら編集ツールが成功するたびにフォーマッタが走ります。

毎回同じ結果が要る処理(整形、危険な操作の阻止、通知、記録)はhooks、判断を伴う方針はCLAUDE.mdに分けると役割が重なりません。公式も、セッション開始のたびに文脈を足したいならCLAUDE.mdを検討するよう書いています。CLAUDE.mdの中身はCLAUDE.mdの書き方で扱っています。

どのファイルに書けば、誰に効くのか

書く場所 効く範囲 共有
~/.claude/settings.json 自分の全プロジェクト しない
.claude/settings.json そのプロジェクト リポジトリにコミットできる
.claude/settings.local.json そのプロジェクト しない(Claude Codeが設定を保存する時にgitignoreされる)
組織の管理設定 組織全体 管理者が管理
プラグインのhooks/hooks.json プラグインが有効な間 プラグインに同梱

このほか、スキルやサブエージェントのfrontmatterにも書けます。適用範囲が個人の環境に限られる通知は~/.claude/settings.json、チームで同じ動作を期待する整形は.claude/settings.jsonが自然な置き場所です。設定の場所と設定ファイル全体の構造はsettings.jsonの解説に譲ります。

すべて止めたい時は"disableAllHooks": trueを指定します。ただし管理設定のhooksは、管理設定の側でも指定しない限り動き続けます。実行中に設定ファイルを編集した場合、通常はファイル監視が変更を拾います。

設定はどう書くのか

公式ガイドの最初の例は、Claudeが入力や権限の確認を待っている時にmacOSの通知を出すフックです。~/.claude/settings.jsonに書きます。

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

既にhooksキーがある場合は、オブジェクトごと置き換えず、既存のイベントキーと並べて追加します。

matcherが絞り込む対象はイベントごとに違います。PreToolUseやPostToolUseなどのツール系イベントではツール名(Bash、Edit|Write、mcp__.*など)、SessionStartでは始まり方(startup、resume、compactなど)、Notificationでは通知の種類です。UserPromptSubmitやStopはmatcherに対応せず、毎回発火します。matcherを書かなければ、そのイベントのたびに発火します。

引数まで条件にしたい時は、ハンドラーにifフィールドを足します。権限ルールと同じ構文("Bash(git *)"など)でツール名と引数をあわせて判定しますが、使えるのはツール系イベントだけで、他のイベントに付けるとフックが動かなくなります。公式はこの判定をベストエフォートとし、確実な許可や拒否には権限システムを使うよう書いています。

ハンドラーのtypeはほとんどがcommandです。ほかにhttp、mcp_tool、prompt、agentがあり、agentは実験的な機能とされています。

どのイベントを使えばよいのか

イベント 発火する時 主な用途
SessionStart セッションの開始時と再開時 圧縮後に文脈を戻す
UserPromptSubmit プロンプト送信時(処理の前) 補足情報の追加
PreToolUse ツール呼び出しの実行前(ブロック可) 危険な操作を止める
PermissionRequest 権限の判断が必要な時 決まった確認の自動承認
PostToolUse ツール呼び出しの成功後 整形、記録
Notification 通知を送る時 デスクトップ通知
Stop Claudeが応答を終えた時 終了前のチェック

このほかにも、サブエージェントの開始と終了、設定ファイルの変更、監視対象ファイルの変更などのイベントがあります。

選ぶ時の注意は3つです。PostToolUseは実行済みの操作を取り消せないので、止めたいならPreToolUseを使います。Stopはタスク完了時だけでなく応答を終えるたびに発火します。Claudeはシェルコマンドでもファイルを書き換えるため、Edit|Writeのmatcherではそれを拾えません。すべての変更を見たいなら、公式は1ターンに1回作業ツリーを調べるStopフックを勧めています。

終了コード0・2・それ以外で何が起きるのか

フックには、イベントの情報がJSONで標準入力から渡されます。PreToolUseならtool_nameとtool_input(Bashならtool_input.command)が入り、スクリプトはこれをjqなどで読んで、終了コードで次の動きを伝えます。

終了コード 起きること
0 異議なし。PreToolUseでは承認の意味にならず、通常の権限確認が行われる。SessionStartなどでは標準出力の平文がClaudeの文脈に加わる
2 操作をブロック。理由は標準エラー出力に書く。SessionStartなどブロックできないイベントでは、表示だけして処理は続く
それ以外 多くのイベントで非ブロッキングのエラー。操作は続き、hook errorの通知が出る

終了状態をより細かく制御するなら、終了コード0で標準出力にJSONを返します。公式は、1つのフックでは「exit 2」か「exit 0とJSON」のどちらか一方を選ぶよう書いています。PreToolUseではhookSpecificOutputの中のpermissionDecisionに"deny"(取り消して理由をClaudeへ返す)、"ask"(通常どおり確認を出す)、"allow"(確認を省く。設定の拒否ルールは引き続き効く)を返せます。

複数のフックが一致した場合は並列で実行され、PreToolUseの判断は最も厳しいものが採用されます。ただし1つがdenyを返しても、他のフックの実行は止まりません。

公式の例で始める2つのフック

編集したファイルを自動で整形する

PostToolUseにEdit|Writeのmatcherを付け、編集したファイルのパスをjqで取り出してPrettierに渡します。公式はプロジェクトルートの.claude/settings.jsonに置く例として示しています。

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

成功しても会話には何も出ないので、ファイルが整形されたかで確かめます。jqが入っていなければ先にインストールします。

保護したいファイルへの編集を止める

.claude/hooks/protect-files.shに、対象パスが保護パターンに一致したら終了コード2で止めるスクリプトを置きます。

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

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

# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"

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

macOSとLinuxでは実行権限が要ります。

chmod +x .claude/hooks/protect-files.sh

.claude/settings.jsonには、Edit|WriteのPreToolUseフックとして"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"を登録します。$CLAUDE_PROJECT_DIRはセッションを始めたプロジェクトのルートです。Claudeに.envの編集を頼むと実行前に止まり、Blocked:のメッセージがClaudeに渡るので、Claudeは別のやり方に切り替えられます。

macOSで通知のフックが動かない場合は、osascriptが使うScript Editorに通知の許可がない可能性があります。この時コマンドは何も言わずに失敗するので、システム設定の「通知」でScript Editorを許可します。

hooksと権限モードはどちらが優先されるのか

PreToolUseフックは、すべての権限モードで、権限モードの判定より前に発火します。permissionDecision: "deny"を返すフックは、bypassPermissionsモードや--dangerously-skip-permissionsでもツールを止めます。権限モードを変えても外せないルールを作れるということです。

逆は成り立ちません。hooksは制限を強められても、権限ルールが許す範囲より緩めることはできません。PermissionRequestフックで自動承認する場合、matcherを空や.*にするとファイル書き込みやシェルコマンドを含むすべての確認が自動承認されるので、公式はできるだけ狭くするよう注意しています。権限モードの選び方は権限モードの解説で扱っています。

フックが動かない時はどこを確認するのか

  1. /hooksに出ているか: 目的のイベントの下にあるかを見ます。
  2. 出ていなければ設定ファイル: 数秒待っても出ないならセッションを再起動します。JSONの末尾カンマやコメントは使えません。置き場所はプロジェクト用が.claude/settings.json、全体用が~/.claude/settings.jsonです。「Only hooks from managed settings run here」と出るなら、組織がallowManagedHooksOnlyを設定しており、個人やプロジェクトのhooksは動きません。
  3. 出ているのに発火しないならmatcher: 発火条件としてツール名と正確に一致する必要があり、大文字と小文字を区別します。
  4. エラーが出るならスクリプト単体: サンプルのJSONを流し込み、終了コードを確かめます。
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?  # Check the exit code

「command not found」ならスクリプトをフルパスか${CLAUDE_PROJECT_DIR}で指定し、スクリプトがまったく動かないならchmod +xで実行権限を付けます。また対話セッションでは、フォルダの信頼ダイアログを受け入れるまで、自分の~/.claude/settings.jsonを含むすべての設定ファイルのhooksが保留されます。

JSONを返しているのにエラーも出ず効かない場合、公式は2つの原因を挙げています。一つは、シェルのプロファイルにある無条件のechoが先に出力され、出力が{で始まらず平文扱いになるケースです。もう一つは、permissionDecisionなどをhookSpecificOutputの外に置いて黙って無視されるケースです。どちらもclaude --debug-file /tmp/claude.logで起動するか、セッション中に/debugを実行して、デバッグログで確かめられます。

Stopフックで応答が終わらなくなったらどうするのか

Stopフックで「まだ終わっていない」とブロックすると、Claudeは作業を続け、また同じフックが発火します。公式によると、Claudeのツール呼び出しを挟まずに8回続けてブロックすると、Claude Codeがそのフックを上書きして止めます。フック側では、入力のstop_hook_activeがtrue(すでにストップフックの結果として続行中)なら早めに終了させます。

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # Allow Claude to stop
fi
# ... rest of your hook logic

8回を超える繰り返しが必要なら、環境変数CLAUDE_CODE_STOP_HOOK_BLOCK_CAPで上限を上げられます。なおタイムアウトの既定値はcommandが通常10分、UserPromptSubmit・PreModelSwitch・PostModelSwitchでは30秒で、timeoutフィールド(秒)で変えられます。既定値やイベントの種類は更新で変わることがあるので、設定する時点の公式ページと照らし合わせてください。

チームで共有する前に何を決めておくべきか

commandフックは、あなたのユーザー権限のすべてでシェルコマンドを実行し、そのアカウントが触れるファイルを変更も削除もできます。公式は、設定に加える前にすべてのコマンドを読んでテストするよう警告しています。

.claude/settings.jsonはリポジトリで共有でき、信頼ダイアログや組織の管理ポリシーの条件を満たす各自の環境でそのコマンドが動きます。特にclaude -pやSDKのセッションは信頼ダイアログを出さず、フォルダを信頼済みとして扱います。自分が書いていないリポジトリでclaude -pを回す前には、.claude/の設定を確認するか、--settings '{"disableAllHooks": true}'でその回だけhooksを切ります(管理設定のhooksは、管理設定側でも無効化されていない限り動きます)。スクリプトでは、シェル変数を"$VAR"と引用符で囲む、パスの..を確認する、.envや鍵などの機密ファイルを対象から外す、が公式の実践項目です。

共有前には、誰がフックの中身をレビューするかと、依存するツール(jqやフォーマッタ)を全員の環境にどう揃えるかを決めておきます。チームでの承認の流れはClaude Codeの運用ルール、hooksをプラグインとして配る方法はプラグインの作り方を参照してください。

hooksの設計を含め、Claude Codeの設定と運用をチームの型として揃えたい場合は、研修という選択肢もあります。