Claude CodeのHooksが動かない原因|matcher・exit code・JSONの確認方法

Claude CodeのHooksが動かない原因|matcher・exit code・JSONの確認方法 AI開発

Claude CodeではHooksを設定することで、ファイル編集後にFormatterを実行したり、Bashコマンドを実行する前に危険な操作を検査したり、Claudeが作業を終了する前にテストを確認したりできます。

しかし、.claude/settings.jsonへHookを書いたのにまったく実行されないことがあります。

また、Hook自体は実行されているものの、

よくある疰状
exit 1にしたのにコマンドが止まらない

JSONでdenyを返したのに無視される

といった問題も発生します。

Claude CodeのHooksでは、Hook Event、matcher、Handler、exit code、stdoutへ返すJSONがそれぞれ別の役割を持っています。どこか1つでも設定が合っていなければ、Hookが実行されなかったり、実行されても期待した制御にならなかったりします。

現在のClaude Codeでは、HookはEventが発生し、matcherが一致した場合に実行されます。Command HookにはそのEventの情報がJSONとしてstdinから渡され、Hook側はexit code、stdout、stderrを使ってClaude Codeへ結果を返します。

この記事では、Claude CodeのHooksが動かないときに確認したいsettings.json、Hook Event、matcher、Tool名、exit code、JSON入出力、Script Pathなどを順番に解説します。Command/HTTP/Prompt/Agentという各Hook Typeの基本的な使い方や実践パターンはClaude Code Hooks完全ガイドで解説しているため、この記事では「なぜ動かないのか」という切り分けにしぼります。

スポンサーリンク
  1. 最初に/hooksでHookが登録されているか確認する
  2. Hooksはhooks.jsonではなくsettings.jsonへ書く
  3. settings.jsonのJSONエラーでHook全体が読み込まれていない可能性がある
  4. claude doctorでSettings Errorを確認する
  5. Hookの基本構造を理解する
  6. PreToolUseとPostToolUseを間違えていないか確認する
  7. matcherはToolの入力内容ではなく基本的にTool名へ一致させる
  8. Tool名のスペルミスはエラーではなく何も起こらない
  9. matcherを省略すればすべてに一致する
  10. matcherの正規表現が広すぎる場合もある
  11. MCP Toolはmcp__サーバー名__ツール名になる
  12. Bashの中身を絞るならifを使う
  13. UserPromptSubmitやStopにはmatcherを付けても意味がない
  14. Hookへ渡されるJSONはstdinから読む
  15. tool_inputのフィールドはToolごとに違う
  16. デバッグ中はstdinのJSONをファイルへ保存すると分かりやすい
  17. exit 0は「許可」ではなく正常終了
  18. exit 1ではPreToolUseをBlockできない
  19. PreToolUseを止めるならexit 2
  20. PostToolUseでexit 2しても実行済みToolは元に戻らない
  21. Eventによってexit 2の意味が違う
  22. JSONを返せばexit codeより細かく制御できる
  23. JSONを返す場合は基本的にexit 0を使う
  24. 古い記事のpermissionDecision形式をそのまま使わない
  25. stdoutにJSON以外を混ぜるとParseに失敗する
  26. JSONの先頭と末尾も重要
  27. JSON Schemaが間違っているとActionはそのまま進むことがある
  28. exit 2はJSONのallowでも上書きできない
  29. Bash Scriptの最後のCommandがexit codeになることにも注意する
  30. Script単体では動くのにClaude Codeからは動かない場合
  31. Script Pathは${CLAUDE_PROJECT_DIR}を使うと安定しやすい
  32. argsを指定するとShellを通らない
  33. Windowsでは.cmdをExec Formで直接起動できないことがある
  34. WindowsではPowerShellを明示できる
  35. Permission Hookを作るならexit 1ではなく仕様に合ったDecisionを使う
  36. PermissionRequestではexit 2の意味が違う
  37. async: trueではBlockできない
  38. MCP Tool HookはSessionStartで動かない場合がある
  39. workspaceをTrustしていないためHookが保留されている可能性がある
  40. disableAllHooksが有効になっていないか確認する
  41. Managed設定でUserやProjectのHookが禁止されている場合もある
  42. User・Project・LocalのHookは単純な上書きではなく追加される
  43. 同じEventの複数Hookは並列実行される
  44. Scriptが存在しない場合もBlockされず処理が続くことがある
  45. Linux・macOSでは実行権限を確認する
  46. jqがPATHにないケースもある
  47. stderrとstdoutを使い分ける
  48. PostToolUseのWarningをClaudeに見せたいならexit 0では足りない
  49. Stop Hookの無限ループにも注意する
  50. JSON制御を始める前にexit 0とexit 2だけで確認する
  51. claude –debugでHookの評価を直接確認する
  52. settings.json変更後は毎回再起動しなくてもよい
  53. Hooksが動かないときは「登録・一致・実行・結果」の4段階で確認する
  54. Claude CodeのHooksが動かない原因に関するよくある質問
  55. Claude CodeのHooksで特に注意したいのはexit 1とexit 2

最初に/hooksでHookが登録されているか確認する

Hookが動かない場合、最初からScriptを修正する必要はありません。Claude Codeで/hooksを実行します。

現在のClaude Codeでは/hooksから、現在のSessionへ登録されているHookをEventごとに確認できます。そこではHookのEvent、matcher、Hook Type、どの設定ファイルから読み込まれたか、実際のCommandなどを確認できます。

たとえば、

/hooksの例
Project Settings
PostToolUse
matcher: Edit|Write
type: command

のように表示されていれば、少なくとも設定ファイルからHook Definitionは読み込まれています。

逆に、自分が作ったHookが/hooksへまったく表示されないなら、Hook Scriptが失敗している以前の問題です。設定ファイルの場所、JSON Schema、hooksキーの書き方などを確認してください。

Hooksはhooks.jsonではなくsettings.jsonへ書く

Claude CodeのProject Hookは通常.claude/settings.jsonへ書きます。自分だけの設定なら.claude/settings.local.json、すべてのProjectで使う個人用Hookなら~/.claude/settings.jsonです。

たとえばProject内のTypeScriptファイルを編集した後にScriptを実行するなら、次のように設定できます。

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

Hooksはsettings.json内のhooksキーへ記述します。次のように.claude/hooks.jsonという独自ファイルを作っても、通常のProject Hook設定としては読み込まれません。

ただしPluginではhooks/hooks.jsonという専用ファイルを利用できます。通常のProject HookとPlugin Hookを混同しないよう注意してください。

settings.jsonのJSONエラーでHook全体が読み込まれていない可能性がある

/hooksへHookが表示されない場合は、settings.json自体のValidation Errorも確認します。

たとえば、

問題のある例
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": ["Edit", "Write"],
        "hooks": [
          { "type": "command", "command": "./check.sh" }
        ]
      }
    ]
  }
}

のようにmatcherをArrayで書くと問題になります。現在のClaude CodeではmatcherはStringです。

通常のUser、Project、Local SettingsでmatcherをArrayにするとSchema Errorとなり、そのSettings File自体がRejectされるため、そのファイルに含まれるHookが/hooksへ表示されません。

複数のToolを指定する場合はArrayではなく、

正しい例
{
  "matcher": "Edit|Write"
}

とします。現在のバージョンでは、

Comma区切り
{
  "matcher": "Edit, Write"
}

というComma区切りにも対応していますが、古いClaude Codeとの互換性まで考えるなら|を利用するほうが分かりやすいでしょう。Comma区切りがExact Matchの区切りとして扱われるのはClaude Code v2.1.191以降です。

claude doctorでSettings Errorを確認する

JSONやSchemaに問題がありそうならTerminalからclaude doctorを実行できます。Session内なら/doctorも利用できます。

claude doctorでは、Claude Codeを通常Sessionとして起動せずにSettings Validationを確認できます。matcherがArrayになっていたり、設定値の型が違っていたりする場合、Hook Scriptを何度テストしても解決しません。まずConfiguration自体が有効か確認します。

Hookの基本構造を理解する

Claude CodeのHook設定は、大きく3段階になっています。

最初にHook Event(PreToolUsePostToolUseStopSessionStartなど)があります。その中にmatcherがあります。さらにその中に、実際に実行するHandler(commandhttpmcp_toolpromptagentなど)があります。

たとえば、

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "npm run lint" }
        ]
      }
    ]
  }
}

なら、PostToolUseが発生し、Tool名がEditまたはWriteか確認し、一致したらnpm run lintを実行するという動きになります。

現在のClaude CodeにはCommand、HTTP、MCP Tool、Prompt、Agentの5種類のHook Handlerがあります。それぞれの具体的な設定例はClaude Code Hooks完全ガイドで紹介しています。Hookが動かないときは、この3段階のどこで止まっているのかを分けて考えると原因を見つけやすくなります。

PreToolUseとPostToolUseを間違えていないか確認する

よくあるのがHook Eventの選択ミスです。

危険なBashコマンドを実行前に止めたいならPreToolUseを使用します。

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

一方、ファイルを編集した後にFormatterを実行したいならPostToolUseが自然です。

PostToolUseはToolが正常に実行された後に発火します。そのためPostToolUseで「このEditを禁止する」ことはできません。その時点ではすでにEdit Toolが実行されています。

PreToolUseはTool実行前に発生し、Tool CallをBlockできます。一方、PostToolUseはTool成功後に発生します。

matcherはToolの入力内容ではなく基本的にTool名へ一致させる

Tool Eventで特に間違えやすいのがmatcherです。

PreToolUsePostToolUseの場合、matcherは基本的にTool名へ対して評価されます。

一致する例
{ "matcher": "Bash" }

{ "matcher": "Edit|Write" }

一方、次のように、

期待どおりにならない例
{ "matcher": "npm test" }

としても、通常期待する動作にはなりません。npm testはBash Toolへ渡されたCommandであって、Tool名ではないためです。Bash Commandの内容まで条件にしたい場合は、後述するifを利用できます。

現在の公式仕様では、Tool EventのmatcherはJSON入力のtool_nameに対して評価されます。

Tool名のスペルミスはエラーではなく何も起こらない

たとえば、

タイプミス
{ "matcher": "Writes" }

と書いたとします。実際のTool名はWriteです。

この場合、JSON自体はValidなのでHook Definitionが読み込まれる可能性があります。しかしWritesというToolは呼ばれないため、Hookも発火しません。エラーが表示されず、設定は存在するのに何も起こらない状態になります。

Tool名のスペルミスは何にも一致しないMatcherとなり、HookがSilentに発火しない代表的な原因です。/hooksに表示されるのにHookが動かない場合は、MatcherのTool名を最初に確認してください。

matcherを省略すればすべてに一致する

原因調査のために、一時的にmatcherを外す方法もあります。

現在のClaude Codeでは、

Match All
{ "matcher": "*" }

{ "matcher": "" }

と、matcher自体の省略は、Match Allとして扱われます。

たとえば、

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "hooks": [
          { "type": "command", "command": "echo hook-fired >> /tmp/claude-hook.log" }
        ]
      }
    ]
  }
}

とすれば、PostToolUse Eventが発生するたびにCommandが実行されます。これで動くなら、Hook Event・Command・Scriptそのものは正常で、元のmatcherに問題がある可能性が高くなります。

matcherの正規表現が広すぎる場合もある

現在のClaude Codeでは、MatcherにLetters、Digits、_-、Space、,|以外の文字が含まれるとJavaScript Regular Expressionとして評価されます。

たとえば、

前方一致
{ "matcher": "^Notebook" }

ならNotebookで始まるTool名へ一致します。一方、

想定以上に一致する例
{ "matcher": "Edit.*" }

はRegular Expressionとして処理され、EditだけでなくNotebookEditにも一致する可能性があります。完全一致させたいRegular Expressionなら、

Anchorを付ける
{ "matcher": "^Edit$" }

のようにAnchorを付けます。Claude CodeのHooksではRegular ExpressionがUnanchoredで評価されるため、想定より多くHookが発火する場合にもMatcherを確認してください。

MCP Toolはmcp__サーバー名__ツール名になる

MCP ToolへHookを設定している場合はTool名にも注意が必要です。MCP ToolはTool Event上でmcp__<server>__<tool>という名前になります。

たとえばMemory Serverのcreate_entitiesならmcp__memory__create_entitiesです。Memory ServerのToolすべてへHookを付けたい場合は、

サーバー内のすべて
{ "matcher": "mcp__memory__.*" }

とします。単にmemorymcp__memoryとしても、期待するToolとは一致しません。MCP ToolのMatcherにはmcp__<server>__<tool>形式を使用してください。

Bashの中身を絞るならifを使う

現在のClaude Code Hooksには、Tool Event向けにifがあります。すべてのBash ToolへHookを起動するのではなく、gitコマンドだけに限定したい場合は、

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-git.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

とできます。最初にmatcher: BashでTool名を絞り、その後if: Bash(git *)で実際のTool Inputまで確認します。ifPreToolUsePostToolUsePostToolUseFailurePermissionRequestPermissionDeniedなどのTool Eventでのみ評価されます。Tool Event以外へifを設定すると、そのHook Handlerは実行されません。

「Matcherを直しても実行されない」という場合は、Handler内部のifも確認してください。

UserPromptSubmitやStopにはmatcherを付けても意味がない

すべてのHook EventがMatcherを利用するわけではありません。たとえばUserPromptSubmitPostToolBatchStopTaskCreatedTaskCompletedMessageDisplayなどにはMatcherによるFilteringがありません。

現在のClaude Codeでは、MatcherをサポートしないEventへmatcherを設定すると、そのMatcherはSilentに無視されます。

たとえば、

意味のない例
{
  "hooks": {
    "Stop": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./check-tests.sh" }
        ]
      }
    ]
  }
}

としても、「Bashを使った場合だけStop Hookを実行する」という意味にはなりません。StopはClaudeが回答を終了しようとするときのEventです。条件分岐が必要なら、Hookへ渡されるJSONや外部状態をScript側で確認します。

Hookへ渡されるJSONはstdinから読む

Command Hookでは、Eventに関する情報がJSONとしてstdinへ渡されます。たとえばBashのPreToolUseなら概念的に次のような内容です。

stdinのJSON
{
  "session_id": "abc123",
  "cwd": "/home/user/my-project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

現在のClaude Codeでは、共通フィールドとしてsession_idcwdなどが渡され、Tool Eventではtool_nametool_inputtool_use_idなどが追加されます。

そのためScript側で、

何も渡されない可能性
COMMAND="$1"

のようにCommand Line Argumentとして受け取ろうとしても、何も渡されていない場合があります。Bashなら、

正しい取得方法
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

のようにstdinを読みます。

tool_inputのフィールドはToolごとに違う

tool_input内の構造も一律ではありません。Bashなら{"tool_input":{"command":"npm test"}}ですが、Edit ToolではFile Pathや変更内容など別のPropertyが入ります。

そのため、

Edit Hookでは期待した値を取得できない
jq -r '.tool_input.command'

をEdit Hookでも使えば、期待する値を取得できません。EditやWriteのFile Pathを処理したいなら、実際にそのTool Eventへ渡されているJSONを確認する必要があります。Hooks ReferenceではEventごとにInput Schemaが定義されています。

Hook Scriptを他Eventからコピーした場合は、参照しているJSON Propertyも合わせて修正してください。

デバッグ中はstdinのJSONをファイルへ保存すると分かりやすい

何が渡されているか分からない場合は、一時的にstdinをそのまま保存すると原因を調べやすくなります。

.claude/hooks/debug.sh
#!/bin/bash
cat >> /tmp/claude-hook-input.jsonl
exit 0

Hookを発火させた後に、

確認
cat /tmp/claude-hook-input.jsonl

を確認すれば、実際にClaude CodeがHookへ渡した入力を確認できます。そのJSONを見ながらtool_nametool_inputcwdhook_event_nameなどを調べます。想像でProperty名を書くより確実です。

exit 0は「許可」ではなく正常終了

Hookのexit codeで特に誤解しやすいのが0です。一般的なCommandでは0 = 成功です。

Claude Code Hookでもexit 0は成功ですが、「PreToolUseでexit 0したからToolを明示的に許可した」という意味ではありません。JSONによるDecisionを返していないexit 0は、Hookが判断を行わず、通常のPermission Flowへ処理を戻す動作になります。

公式Hook例でも、安全なCommandならexit 0して「No decision; normal permission flow applies」とする構造になっています。

つまりexit 0は「このToolを無条件Allow」ではなく、「Hookとしては問題なし。後は通常処理へ任せる」と考えると分かりやすいでしょう。

exit 1ではPreToolUseをBlockできない

最も重要なのがここです。Shell Scriptでは0 = 成功1 = 失敗という使い方が一般的なので、exit 1にすればClaudeのTool Callを止められると考えがちです。

しかし現在のClaude Code Hooksでは、ほとんどのEventでexit 1だけではBlockになりません。たとえばPreToolUse Hookで、

エラーにならない
#!/bin/bash
echo "危険なコマンドです" >&2
exit 1

としても、これは基本的にNon-blocking Errorです。Tool Callをexit codeだけでBlockしたいならexit 2を利用します。

現行Hooks Referenceでは、ほとんどのHook Eventについて、exit 2がCode単体でBlockingを行う特別な値であり、exit 1などその他のNon-zero Codeは通常Non-blocking Errorとして処理されると説明されています。

PreToolUseを止めるならexit 2

たとえばrm -rfをBlockするHookなら、次のようにできます。

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

if echo "$COMMAND" | grep -q "rm -rf"; then
    echo "rm -rfは実行できません" >&2
    exit 2
fi

exit 0

設定は、

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

とします。PreToolUseでexit 2になるとTool CallがBlockされます。stderrへ書いた内容がBlocking ReasonとしてClaudeへ伝えられます。

PostToolUseでexit 2しても実行済みToolは元に戻らない

PostToolUseでもexit 2を返せますが、Toolはすでに実行済みです。

たとえばEdit Toolでファイルが変更された後にLinterを実行し、

.claude/hooks/lint-check.sh
npm run lint

if [ $? -ne 0 ]; then
    echo "Lint errorが発生しました" >&2
    exit 2
fi

とすれば、ClaudeへLint Errorを伝えて後続対応を促すことはできます。しかし「Edit自体を取り消す」ことにはなりません。

現在のClaude Codeでは、PostToolUseでexit 2になるとstderrがClaudeへ表示されますが、Toolはすでに実行されています。事前に禁止する必要がある処理はPreToolUseで検査してください。

Eventによってexit 2の意味が違う

exit 2はすべてのHookで同じ意味ではありません。

PreToolUseではTool CallをBlockします。UserPromptSubmitならPrompt ProcessingをBlockします。StopならClaudeが終了するのを防ぎ、作業を続けさせます。PreCompactならCompactionをBlockします。一方、Notificationではexit codeやstderrによるDecision Controlは行われません。PostToolUseではTool自体を止めることはできません。

そのため「exit 2にしたのに止まらない」場合は、ScriptではなくHook Eventの仕様を確認してください。

JSONを返せばexit codeより細かく制御できる

Claude Code Hooksではexit codeだけでなく、stdoutへJSONを返して制御できます。

たとえばPreToolUseでToolを拒否する場合は、現在の仕様ではhookSpecificOutputを利用できます。

Structured Output
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "本番環境への書き込みは禁止されています"
  }
}

Scriptから返すなら、概念的には、

.claude/hooks/check.sh
#!/bin/bash
cat <<'JSON'
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "本番環境への書き込みは禁止されています"
  }
}
JSON

exit 0

とします。現在のPreToolUseではpermissionDecisionとしてallowdenyaskdeferなどを利用できます。

JSONを返す場合は基本的にexit 0を使う

Structured JSONでDecisionを返す場合は、JSONをstdoutへ出力し、exit 0という組み合わせが基本です。exit codeによる単純なBlockか、exit 0とJSONによるStructured Controlのどちらかを選ぶ方法が推奨されています。

たとえば、

.claude/hooks/check.sh
echo '{"decision":"block","reason":"テストが失敗しています"}'
exit 0

のような構成です。ただし利用できるJSON FieldはEventによって異なります。PreToolUseではhookSpecificOutput.permissionDecisionを、Stopなどではdecision: "block"を使用します。すべてのHookへ同じJSONを返しても動作しません。

古い記事のpermissionDecision形式をそのまま使わない

Claude Code Hooksは仕様変更が続いているため、古い記事やGitHub上の設定例をそのままコピーすると動かないことがあります。特にDecision ControlはEventごとに異なります。

PreToolUseならhookSpecificOutput.permissionDecisionを、Stopなら、

StopのDecision
{
  "decision": "block",
  "reason": "テストが完了していません"
}

のようにTop-levelのdecisionを利用できます。Hookを作る際は対象Eventの最新Schemaを確認してください。

stdoutにJSON以外を混ぜるとParseに失敗する

JSON Hookで非常に起きやすい問題です。次のScriptを考えます。

問題のある例
#!/bin/bash
echo "checking command..."
echo '{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny"
  }
}'
exit 0

stdoutには通常のログとJSONの両方が出ます。これは「JSON Objectだけ」ではないため、Claude CodeがStructured Hook Outputとして正常にParseできない可能性があります。

現在の公式仕様では、HookのStructured JSONをstdoutへ返す場合、stdoutにはJSON Objectだけを出力する必要があります。Shell Profileなどが追加Textを出力してもJSON Parsingを妨害します。

デバッグLogはstderrへ出し、JSONだけstdoutへ返します。この分離が重要です。

JSONの先頭と末尾も重要

現在のClaude Codeでは、Hookのstdoutが{で始まり}で終わる場合、JSONとしてParseしようとします。それ以外は基本的にPlain Textとして扱われます。

JSON制御を使うHookでは、stdoutはJSON、stderrはログと明確に分離するのが安全です。

JSON Schemaが間違っているとActionはそのまま進むことがある

JSONとしてSyntaxが正しくても、Claude Codeが期待するSchemaに合っていなければHook Errorになります。

たとえばPreToolUseで、

Schema不一致
{ "permissionDecision": "deny" }

とだけ返しても、現在のSchemaとは合いません。必要なのはhookSpecificOutputを含む構造です。

現在のClaude Codeでは、exit 0でJSON Objectを返したもののSchema Validationに失敗した場合、多くのEventではNon-blocking Errorになり、処理は続行します。「JSONを返したから確実にBlockできている」とは限りません。Security目的のHookなら、実際に危険な操作がBlockされることまでテストしてください。

exit 2はJSONのallowでも上書きできない

逆にexit 2は強い意味を持ちます。たとえばScriptがpermissionDecision: "allow"をstdoutへ返していたとしても、Process自身がexit 2していればBlocking Errorになります。

現在のClaude Codeではexit 2のBlock効果はJSONから上書きできません。Structured JSONを使うなら、意図せずexit 2になっていないかも確認してください。Script途中で失敗したCommandのExit Statusをそのまま返していると、予定外のBlockにつながる可能性があります。

Bash Scriptの最後のCommandがexit codeになることにも注意する

Shell Scriptで明示的にexitを書いていなければ、通常最後に実行したCommandのExit StatusがScript全体のExit Statusになります。

特にset -eを使っている場合、途中のCommand失敗でScriptが終了します。Structured JSONを返すつもりなら、

最終的なExit Statusを意識する
if npm run lint; then
    exit 0
else
    echo '{"decision":"block","reason":"Lint failed"}'
    exit 0
fi

など、最終的なExit Statusを意識して実装したほうが安全です。

Script単体では動くのにClaude Codeからは動かない場合

Hook ScriptをTerminalから直接実行すると正常なのに、Claude Code経由では失敗する場合があります。Hookは通常の対話型Shellと実行環境が異なる可能性があります。

Claude CodeのCommand HookはCurrent DirectoryとClaude CodeのEnvironmentを引き継いで実行されます。しかしPATH、cwd、Shell、Environment Variable、stdinなどの条件は、自分でTerminalから実行した場合と同じとは限りません。特にHookはstdinからJSONを受け取ることを前提とするため、単純にScriptだけ実行するとInputが存在しません。

Script Pathは${CLAUDE_PROJECT_DIR}を使うと安定しやすい

次のようなRelative Pathは注意が必要です。

不安定な例
{ "command": ".claude/hooks/check.sh" }

Hookが実行されるCurrent Directoryが想定と異なると、Scriptを見つけられない可能性があります。Project RootにHook Scriptを置くなら、

安定しやすい例
{
  "type": "command",
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check.sh",
  "args": []
}

とする方法があります。${CLAUDE_PROJECT_DIR}はSessionを開始したProject Rootを指します。Worktreeなどへ移動しても、この値は元のProject Rootを指し続けます。

一方、Hook Inputのcwdは現在Claudeが作業しているDirectoryを示します。Project Rootと現在の作業Directoryのどちらが必要なのかで使い分けます。

argsを指定するとShellを通らない

現在のCommand Hookにはcommandargsがあります。argsを指定するとExec Formになり、Shellを通さずExecutableを直接起動します。

Exec Form
{
  "type": "command",
  "command": "node",
  "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check.js"]
}

この場合、argsの各要素はそのまま1つのArgumentになります。一方、

Shell Form
{
  "type": "command",
  "command": "npm test && npm run lint"
}

のようにargsを省略するとShell Formとなり、Pipe、&&、Redirect、Variable Expansionなどが利用できます。

argsが存在するとExec Form、存在しないとShell Formとして処理されます。Hook CommandがTerminalでは動くのにClaude Codeでは失敗する場合、この違いも確認してください。

Windowsでは.cmdをExec Formで直接起動できないことがある

Windowsではさらに注意が必要です。現在のClaude Code HooksではExec Formでcommandに指定するものは実際のExecutableとして解決できる必要があります。npm、npx、eslintなどが作る.cmd.batのShimはExecutableではないため、Exec Formから直接Spawnできません。

たとえば、

Windowsで問題になる例
{
  "type": "command",
  "command": "npx",
  "args": ["eslint", "."]
}

がWindowsで問題になる場合があります。Shell Formを利用するか、Node.js Script本体を、

回避例
{
  "type": "command",
  "command": "node",
  "args": ["node_modules/eslint/bin/eslint.js", "."]
}

のように呼び出す方法を検討します。WindowsだけHookが動かない場合は、このExec Formの違いも確認してください。

WindowsではPowerShellを明示できる

Claude CodeではCommand Hookごとに{"shell": "powershell"}を指定できます。

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "shell": "powershell",
            "command": "Write-Host 'File written'"
          }
        ]
      }
    ]
  }
}

現在のClaude CodeはWindowsでPowerShell 7のpwsh.exeを検出し、なければWindows PowerShell 5.1のpowershell.exeへFallbackします。Bash前提のScriptをWindowsで動かそうとしている場合は、Shellの違いを確認してください。

Permission Hookを作るならexit 1ではなく仕様に合ったDecisionを使う

危険なCommandをBlockする用途ではexit 1だけに頼らないほうが安全です。

単純なPolicyなら、

単純なBlock
echo "Dangerous command blocked" >&2
exit 2

とできます。より細かく制御したいなら、

詳細なDeny
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "本番 DBへの変更は禁止されています"
  }
}

をexit 0で返します。逆に{"permissionDecision": "deny"}のように古い形式や別EventのSchemaを流用すると期待どおりに動かない可能性があります。Policy Hookは「設定した」だけで安心せず、実際に禁止対象をテストすることが重要です。

PermissionRequestではexit 2の意味が違う

PermissionRequest Hookも注意が必要です。現在のClaude Codeでは、PermissionRequestに対するexit 2はPermission Decisionとして利用されません。PermissionをAllowまたはDenyしたい場合は、JSONのhookSpecificOutput.decision.behaviorを利用します。

つまり「PreToolUseでexit 2が効いた」からといって、「PermissionRequestでもexit 2を使えばよい」とは限りません。Hook EventごとにDecision方式を確認する必要があります。

async: trueではBlockできない

長いテストをPostToolUseで実行する場合などに{"async": true}を付けることがあります。

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests.sh",
            "args": [],
            "async": true
          }
        ]
      }
    ]
  }
}

この場合HookはBackgroundで実行され、Claudeは完了を待たずに作業を続けます。そのためAsync HookからdecisionpermissionDecisioncontinueなどを返しても、すでに対象処理が進んでいるためControlには利用できません。

Async HookはClaudeの動作をBlockまたは制御できません。「exit 2にしているのに止まらない」という場合はasync: trueになっていないか確認してください。

MCP Tool HookはSessionStartで動かない場合がある

現在のClaude CodeではHook Handler自体としてMCP Toolを呼び出すこともできます。

MCP Tool Hook
{
  "type": "mcp_tool",
  "server": "my_server",
  "tool": "security_scan"
}

しかしMCP Tool Hookには利用可能になるタイミングがあります。初回起動時のSessionStartはMCP ServerがHookから利用可能になる前に発火します。そのためLaunch直後のSessionStartmcp_tool Hookを設定すると、そのHookはSkipされます。Debug LogにはMCP Client Contextがないため利用できないことが記録されます。

Command Hookは動くのにMCP Tool HookだけSessionStartで動かない場合は、仕様上の実行タイミングを確認してください。

workspaceをTrustしていないためHookが保留されている可能性がある

Interactive SessionではWorkspace Trustも関係します。現在のClaude CodeはSettings Fileから読み込まれたHookを実行する前にWorkspace Trustを確認します。Interactive Sessionでは対象Folder、またはTrustが継承される親Folderを承認するまで、User Settingsを含むSettings FileのHookが実行されません。

そのため新しいRepositoryを開いた直後に「Hookが登録されているはずなのに動かない」場合はWorkspace Trustも確認してください。特に他人からCloneしたRepositoryの.claude/settings.jsonには任意のShell Commandを実行できるHookが含まれる可能性があるため、この制限はSecurity上重要です。

disableAllHooksが有効になっていないか確認する

Claude CodeにはすべてのHookを一時的に無効にする{"disableAllHooks": true}という設定があります。Hook Definitionが正しくても、この設定が最終的に有効ならHookは実行されません。

User Settings、Project Settings、Local SettingsなどのPriorityによって最終値が決まります。またCommand Lineから、

CLIで無効化
claude --settings '{"disableAllHooks": true}'

として起動すれば、そのRunではProjectやLocal設定より優先してHookを無効化できます。以前デバッグ目的で無効化して、そのままになっていないか確認してください。

Managed設定でUserやProjectのHookが禁止されている場合もある

企業環境ではallowManagedHooksOnlyがManaged Settingsで設定されている可能性があります。この設定が有効な場合、User、Project、Local、通常PluginなどのHookがBlockされ、Managed Settingsから許可されたHookを中心に実行する構成になります。

個人PCでは動くのに会社PCではHookが動かない場合は、単純なJSONの違いだけでなくManaged Policyも確認する必要があります。/statusを利用すると、Managed Settingsが有効か確認できます。

User・Project・LocalのHookは単純な上書きではなく追加される

Claude Codeの一般的なScalar SettingはLocal、Project、UserなどのPriorityによって上書きされます。しかしHook Definitionについては少し違います。

現在のHooks仕様では、User、Project、LocalなどのHook Entryは単純に置き換えられるのではなくMergeされます。たとえばUser SettingsにPostToolUse → Formatterがあり、Project SettingsにPostToolUse → Linterがあれば、どちらかだけではなく両方が実行される可能性があります。

「Project Hookを書いたらUser Hookが消える」と考えないよう注意してください。逆に「なぜ同じタイミングで別のScriptまで動くのか」という場合は/hooksでSourceを確認すると原因を見つけやすくなります。

同じEventの複数Hookは並列実行される

同じEventで複数のHook Handlerが一致した場合、それらは順番に実行されるとは限りません。現在のClaude CodeではMatching Hookが並列実行されます。

たとえば「Hook Aでファイルを生成し、Hook Bでそのファイルを読む」という依存関係を、同じEventに別Handlerとして設定するとRace Conditionになる可能性があります。必ず順番に実行したい処理なら、1つのScript内で直列化するほうが確実です。

「Hookがたまに失敗する」という場合は、別Hookとの実行順序を前提にしていないか確認してください。

Scriptが存在しない場合もBlockされず処理が続くことがある

Hook設定にFile Pathを書いたものの、Fileが存在しないとします。この場合Hook Processを正常に起動できません。

しかし、ほとんどのHook EventではScriptを起動できなかったこと自体はNon-blocking Errorとして扱われ、元のActionが進む場合があります。Script Pathが存在しない場合などにShellが127のようなStatusで終了し、Policy HookであってもGateが実質無効になり得るため注意が必要です。

Security目的のHookでは「設定があるから守られている」と考えず、Scriptを意図的に発火させて動作を確認してください。

Linux・macOSでは実行権限を確認する

Shell Scriptへ直接Pathを指定している場合は、実行権限も必要です。

実行権限を付与
chmod +x .claude/hooks/check.sh

を実行します。Script本体が#!/bin/bashで始まり、実行可能になっていることを確認してください。実行権限がなくてもHook Definition自体は/hooksへ表示される可能性があります。

そのため「/hooksにはある、matcherも正しい、でもScriptの処理だけ起きない」という場合はFile Permissionも確認します。

jqがPATHにないケースもある

Hook Scriptの公式例では、stdin JSONを処理するためにjqがよく使われます。

jqで処理
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

しかしHook実行環境のPATH上にjqが存在しなければScriptは失敗します。Bash Exampleでjqを使う場合はInstallし、PATH上から利用できることを確認してください。

TerminalではAliasや特殊なPATH設定で利用できるのに、Hook Processでは見えないケースもあるため、必要ならAbsolute Pathを確認してください。

stderrとstdoutを使い分ける

Command Hookではstdoutとstderrの役割も重要です。Structured JSONをClaude Codeへ返すならstdoutを使い、デバッグログはstderrへ出します。

exit 0でstderrへ書いた内容は通常Debug Logだけに入り、Claudeには見えません。一方、Block可能なEventでexit 2を使った場合は、stderrの内容がBlocking Reasonとして利用されることがあります。

stdoutをJSON、stderrをログ・Blocking Feedbackと分けるとトラブルを減らせます。

PostToolUseのWarningをClaudeに見せたいならexit 0では足りない

たとえばPostToolUse HookでLint Errorを検出し、exit 0でstderrへ書くだけでは、Claudeがそのstderrを見て修正してくれるとは限りません。

現在のClaude Codeでは、exit 0時のstderrはDebug Logへ入り、ClaudeのTranscriptには表示されません。PostToolUsePostToolUseFailureでClaudeへWarningを見せたい場合は、exit 2を利用するとstderrをClaudeへ伝えられます。あるいはStructured JSONのadditionalContextなど、そのEventが対応しているOutput Fieldを利用します。

Stop Hookの無限ループにも注意する

Stop Hookを使って「テストが通るまでClaudeを終了させない」という構成を作れます。テスト失敗時に{"decision":"block","reason":"..."}を返せばClaudeは終了せず作業を続けます。

しかしHook条件を解除できない設計にすると、Stop→HookがBlock→Claudeが追加作業→Stop→再びBlockを繰り返す可能性があります。Hookが「動かない」問題とは逆ですが、Stop Hookでは成功条件を明確にしておくことが重要です。

JSON制御を始める前にexit 0とexit 2だけで確認する

複雑なHookをいきなりStructured JSONで作ると、原因が分かりにくくなります。最初は最小構成にするのがおすすめです。

まずHook自体が発火するか確認し、次にBlockingを確認し、それが動いた後にJSONへ変更します。この順番なら、Event・matcher・Script起動・exit code・JSON Parseを1段階ずつ確認できます。

claude –debugでHookの評価を直接確認する

/hooksにHookが表示されているのに実行されない場合はclaude --debugで起動して確認します。

Debug LogではHook Eventが発生したか、どのMatcherが確認されたか、Hookのexit codeやOutputなどを追跡できます。「Hookは登録されている→PreToolUse Eventも発生している→matcherが一致していない」という状態まで確認できれば、Scriptを調べる必要はありません。逆にMatcher一致後にCommandが実行され、127などで終了していればScript PathやExecutableの問題へ調査対象を移せます。

settings.json変更後は毎回再起動しなくてもよい

現在のClaude Codeでは、settings.jsonの変更は短いFile Stability Delayの後に実行中Sessionへ反映されます。Session開始後に.claude DirectoryやSettings File自体を作成した場合でも、現在のバージョンでは検出できます。

ただし保存直後に/hooksを見ると以前の内容が表示される可能性があります。数秒待ってから再度/hooksを開いて確認します。Claude Code v2.1.257より前では、Session開始後に新しく作られた.claude/ Directoryの変更検出に制限がありました。古いClaude Codeを利用している場合は更新も検討してください。

Hooksが動かないときは「登録・一致・実行・結果」の4段階で確認する

Claude CodeのHooksトラブルは、まとめて調べるより処理を段階的に分けると原因を見つけやすくなります。

まず/hooksを実行してHook DefinitionがSessionへ登録されているか確認します。表示されないならSettings FileやJSON Schemaの問題です。

表示されているなら、次にHook Eventとmatcherを確認します。PostToolUseなのにToolを実行していない、matcherWritesになっている、といった問題を探します。

Matcherも正しいなら、Command自体が起動できているか確認します。Script Path、実行権限、PATH、Shell、Windowsの.cmdなどを調べます。

最後にexit codeとJSON Outputを確認します。Hookは実行されているものの「exit 1を使っていた」あるいは「JSON Schemaが対象Eventと違う」というケースを切り分けます。

Claude CodeのHooksが動かない原因に関するよくある質問

QHookを設定したのにまったく実行されません。何から確認すればよいですか

Aまず/hooksを実行し、現在のSessionへHook Definitionが登録されているか確認してください。表示されないならsettings.json自体のJSON構文エラーやSchema Error(matcherを配列にしているなど)を疑います。表示されているのに動かないなら、matcherやHook Eventの選択ミスを確認します。

Qexit 1にしたのにコマンドがブロックされないのはなぜですか

A現在のClaude Code Hooksでは、ほとんどのEventでツール実行をexit codeだけでブロックできるのはexit 2のみです。exit 1などその他のNon-zero CodeはNon-blocking Errorとして扱われ、Tool Callは通常の権限フローへそのまま進みます。危険な操作を止める設計ならexit 2を使ってください。

Qmatcherを正しく書いたはずなのにHookが発火しません

ATool名のスペルミス(WriteをWritesと書くなど)が典型的な原因です。JSON自体はValidなのでエラーは出ず、単に何にも一致しないため静かに発火しません。一時的にmatcherを省略するか”*”にしてHook・Script自体が動くか確認し、動くならmatcherの文字列を見直してください。

QJSONでpermissionDecision: denyを返したのに無視されるのはなぜですか

AJSONのSchemaが対象のHook Eventと一致していない可能性があります。PreToolUseならhookSpecificOutput.permissionDecisionという構造が必要で、トップレベルにpermissionDecisionだけを置くような古い形式では多くの場合Non-blocking Errorとして扱われ、処理がそのまま進みます。またstdoutにJSON以外の文字列が混在しているとParseに失敗することもあります。

QHook Scriptを直接実行すると動くのに、Claude Code経由だと失敗します

A実行環境の違いが原因であることが多いです。HookはstdinからJSONを受け取ることを前提としているため、Terminalから単体実行するとInputが存在せず失敗します。またPATHやShellの違い、argsを指定した場合のExec Form(Windowsの.cmd/.batを直接起動できない)なども確認してください。

Qasync: trueにしたHookでexit 2を返してもツールがブロックされません

AAsync Hookは仕様上Claudeの動作をブロック・制御できません。Backgroundで実行され、Claudeは完了を待たずに作業を続けるためです。ブロックが必要なHookにはasync: trueを付けないでください。

Q個人PCでは動くHookが会社PCでだけ動きません

AManaged SettingsのallowManagedHooksOnlyが有効になっている可能性があります。この設定が有効な場合、User・Project・LocalなどのHookはブロックされ、Managed Settingsから許可されたHookのみが実行されます。/statusでManaged Settingsが有効か確認してください。

Claude CodeのHooksで特に注意したいのはexit 1とexit 2

Claude CodeのHookを実装するとき、最も覚えておきたい違いのひとつがexit 1exit 2です。

一般的なShell Scriptではexit 1をErrorとして利用しますが、Claude CodeではほとんどのHook Eventについて、exit 1だけではActionをBlockしません。Policy Hookをexit codeだけでBlockするなら、対象Eventが対応していることを確認したうえでexit 2を利用します。

より細かな制御が必要なら、exit 0で正しいJSON Objectをstdoutへ返します。その際はstdoutへDebug Logを混ぜず、対象Eventが要求するSchemaを使ってください。PreToolUseならhookSpecificOutput.permissionDecision、StopなどではTop-levelのdecisionといった違いがあります。

Hookがまったく実行されない場合は/hooksで登録状態を確認し、登録済みなのに動かないならmatcherを疑います。それでも原因が分からなければclaude --debugでEvent、Matcher、Command、exit code、Outputを追跡できます。

Claude Code Hooksのトラブルを直すポイントは、「Hookが動かない」と一括りにせず、設定が読み込まれたか、Matcherが一致したか、Scriptが起動したか、返したexit codeやJSONが正しく解釈されたかを順番に確認することです。各Hook Typeの基本的な使い方や実践パターンはClaude Code Hooks完全ガイドもあわせて参考にしてください。