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完全ガイドで解説しているため、この記事では「なぜ動かないのか」という切り分けにしぼります。
- 最初に/hooksでHookが登録されているか確認する
- Hooksはhooks.jsonではなくsettings.jsonへ書く
- settings.jsonのJSONエラーでHook全体が読み込まれていない可能性がある
- claude doctorでSettings Errorを確認する
- Hookの基本構造を理解する
- PreToolUseとPostToolUseを間違えていないか確認する
- matcherはToolの入力内容ではなく基本的にTool名へ一致させる
- Tool名のスペルミスはエラーではなく何も起こらない
- matcherを省略すればすべてに一致する
- matcherの正規表現が広すぎる場合もある
- MCP Toolはmcp__サーバー名__ツール名になる
- Bashの中身を絞るならifを使う
- UserPromptSubmitやStopにはmatcherを付けても意味がない
- Hookへ渡されるJSONはstdinから読む
- tool_inputのフィールドはToolごとに違う
- デバッグ中はstdinのJSONをファイルへ保存すると分かりやすい
- exit 0は「許可」ではなく正常終了
- exit 1ではPreToolUseをBlockできない
- PreToolUseを止めるならexit 2
- PostToolUseでexit 2しても実行済みToolは元に戻らない
- Eventによってexit 2の意味が違う
- JSONを返せばexit codeより細かく制御できる
- JSONを返す場合は基本的にexit 0を使う
- 古い記事のpermissionDecision形式をそのまま使わない
- stdoutにJSON以外を混ぜるとParseに失敗する
- JSONの先頭と末尾も重要
- JSON Schemaが間違っているとActionはそのまま進むことがある
- exit 2はJSONのallowでも上書きできない
- Bash Scriptの最後のCommandがexit codeになることにも注意する
- Script単体では動くのにClaude Codeからは動かない場合
- Script Pathは${CLAUDE_PROJECT_DIR}を使うと安定しやすい
- argsを指定するとShellを通らない
- Windowsでは.cmdをExec Formで直接起動できないことがある
- WindowsではPowerShellを明示できる
- Permission Hookを作るならexit 1ではなく仕様に合ったDecisionを使う
- PermissionRequestではexit 2の意味が違う
- async: trueではBlockできない
- MCP Tool HookはSessionStartで動かない場合がある
- workspaceをTrustしていないためHookが保留されている可能性がある
- disableAllHooksが有効になっていないか確認する
- Managed設定でUserやProjectのHookが禁止されている場合もある
- User・Project・LocalのHookは単純な上書きではなく追加される
- 同じEventの複数Hookは並列実行される
- Scriptが存在しない場合もBlockされず処理が続くことがある
- Linux・macOSでは実行権限を確認する
- jqがPATHにないケースもある
- stderrとstdoutを使い分ける
- PostToolUseのWarningをClaudeに見せたいならexit 0では足りない
- Stop Hookの無限ループにも注意する
- JSON制御を始める前にexit 0とexit 2だけで確認する
- claude –debugでHookの評価を直接確認する
- settings.json変更後は毎回再起動しなくてもよい
- Hooksが動かないときは「登録・一致・実行・結果」の4段階で確認する
- Claude CodeのHooksが動かない原因に関するよくある質問
- 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などを確認できます。
たとえば、
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を実行するなら、次のように設定できます。
{
"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"
}
とします。現在のバージョンでは、
{
"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(PreToolUse、PostToolUse、Stop、SessionStartなど)があります。その中にmatcherがあります。さらにその中に、実際に実行するHandler(command、http、mcp_tool、prompt、agentなど)があります。
たとえば、
{
"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を使用します。
{
"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です。
PreToolUseやPostToolUseの場合、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では、
{ "matcher": "*" }
{ "matcher": "" }
と、matcher自体の省略は、Match Allとして扱われます。
たとえば、
{
"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なら、
{ "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__.*" }
とします。単にmemoryやmcp__memoryとしても、期待するToolとは一致しません。MCP ToolのMatcherにはmcp__<server>__<tool>形式を使用してください。
Bashの中身を絞るならifを使う
現在のClaude Code Hooksには、Tool Event向けにifがあります。すべてのBash ToolへHookを起動するのではなく、gitコマンドだけに限定したい場合は、
{
"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まで確認します。ifはPreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDeniedなどのTool Eventでのみ評価されます。Tool Event以外へifを設定すると、そのHook Handlerは実行されません。
「Matcherを直しても実行されない」という場合は、Handler内部のifも確認してください。
UserPromptSubmitやStopにはmatcherを付けても意味がない
すべてのHook EventがMatcherを利用するわけではありません。たとえばUserPromptSubmit、PostToolBatch、Stop、TaskCreated、TaskCompleted、MessageDisplayなどには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なら概念的に次のような内容です。
{
"session_id": "abc123",
"cwd": "/home/user/my-project",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
現在のClaude Codeでは、共通フィールドとしてsession_idやcwdなどが渡され、Tool Eventではtool_name、tool_input、tool_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が入ります。
そのため、
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をそのまま保存すると原因を調べやすくなります。
#!/bin/bash cat >> /tmp/claude-hook-input.jsonl exit 0
Hookを発火させた後に、
cat /tmp/claude-hook-input.jsonl
を確認すれば、実際にClaude CodeがHookへ渡した入力を確認できます。そのJSONを見ながらtool_name、tool_input、cwd、hook_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なら、次のようにできます。
#!/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
設定は、
{
"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を実行し、
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を利用できます。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "本番環境への書き込みは禁止されています"
}
}
Scriptから返すなら、概念的には、
#!/bin/bash
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "本番環境への書き込みは禁止されています"
}
}
JSON
exit 0
とします。現在のPreToolUseではpermissionDecisionとしてallow、deny、ask、deferなどを利用できます。
JSONを返す場合は基本的にexit 0を使う
Structured JSONでDecisionを返す場合は、JSONをstdoutへ出力し、exit 0という組み合わせが基本です。exit codeによる単純なBlockか、exit 0とJSONによるStructured Controlのどちらかを選ぶ方法が推奨されています。
たとえば、
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なら、
{
"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で、
{ "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を返すつもりなら、
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にはcommandとargsがあります。argsを指定するとExec Formになり、Shellを通さずExecutableを直接起動します。
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check.js"]
}
この場合、argsの各要素はそのまま1つのArgumentになります。一方、
{
"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できません。
たとえば、
{
"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"}を指定できます。
{
"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なら、
echo "Dangerous command blocked" >&2 exit 2
とできます。より細かく制御したいなら、
{
"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}を付けることがあります。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests.sh",
"args": [],
"async": true
}
]
}
]
}
}
この場合HookはBackgroundで実行され、Claudeは完了を待たずに作業を続けます。そのためAsync Hookからdecision、permissionDecision、continueなどを返しても、すでに対象処理が進んでいるためControlには利用できません。
Async HookはClaudeの動作をBlockまたは制御できません。「exit 2にしているのに止まらない」という場合はasync: trueになっていないか確認してください。
MCP Tool HookはSessionStartで動かない場合がある
現在のClaude CodeではHook Handler自体としてMCP Toolを呼び出すこともできます。
{
"type": "mcp_tool",
"server": "my_server",
"tool": "security_scan"
}
しかしMCP Tool Hookには利用可能になるタイミングがあります。初回起動時のSessionStartはMCP ServerがHookから利用可能になる前に発火します。そのためLaunch直後のSessionStartにmcp_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から、
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がよく使われます。
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には表示されません。PostToolUseやPostToolUseFailureで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を実行していない、matcherがWritesになっている、といった問題を探します。
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 1とexit 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完全ガイドもあわせて参考にしてください。
