Issue 00 · Autumn 2026日本語 · RSS
AgentKoji

Claude Code Hooks Not Working? A Checklist

A hook that couldn't find jq exited 0 and let rm -rf / through. A checklist for hooks that don't fire or block, with the commands we used to reproduce them.

TL;DR

  • A hook that never fires looks the same as one that fired and allowed the call; start with claude doctor, which caught a trailing comma and a misspelled event name in our tests.
  • Check that settings live in a documented location (/status shows loaded sources) and that the matcher matches the exact, case-sensitive tool name.
  • claude doctor doesn't flag a lowercase matcher like "bash"; use /hooks to see which hooks are loaded and where they're defined.

A hook that never fires looks exactly like a hook that fired and allowed the call. Below is the checklist we use when a Claude Code hook seems to do nothing. Each item gives the symptom, a command to check it, and the fix. Where we reproduced the cause, we show the command and its output. Where we did not, the item says “(not reproduced)”. We tested on macOS with Claude Code 2.1.303 and jq 1.7.1, on 2026-10-19, using a dummy project and dummy JSON. We did not run a live Claude Code session. For working examples to compare against, see Claude Code PreToolUse Hook Examples.

Start with claude doctor

claude doctor prints install and settings diagnostics without starting a session (source: https://code.claude.com/docs/en/setup, checked 2026-10-19). Run it from the project directory first. It caught two of the causes below.

Is the hook loaded?

The settings file is in the wrong place

FileScope
~/.claude/settings.jsonAll your projects
.claude/settings.jsonThis project, can be committed
.claude/settings.local.jsonThis project, not shared

Hooks merge across these levels instead of replacing each other (source: https://code.claude.com/docs/en/hooks, checked 2026-10-19). (not reproduced)

The JSON doesn’t parse

We left a trailing comma in a dummy .claude/settings.json:

$ jq . .claude/settings.json
jq: parse error: Expected another key-value pair at line 4, column 3
exit=5

claude doctor reported the same file:

Invalid settings
- <project>/.claude/settings.json: Invalid or malformed JSON

The event name is misspelled

We typed PreToolUSE. Claude Code dropped that event and listed the valid names:

- <project>/.claude/settings.json › hooks.PreToolUSE: Unknown hook event "PreToolUSE" was ignored. Valid events: PreToolUse, PostToolUse, PostToolUseFailure, ...

The matcher doesn’t match the tool name

claude doctor does not catch a wrong-case matcher. With "matcher": "bash" and a valid event name, it reported no invalid settings. We checked the regex behavior with Node:

$ node -e 'console.log(/Edit.*/.test("NotebookEdit"), /^Edit$/.test("NotebookEdit"))'
true false

Whether a bash matcher fails to fire in a live session: (not reproduced)

The change hasn’t loaded, or the folder isn’t trusted

Does the script start?

The script isn’t executable

$ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | sh -c .claude/hooks/block-rm-root.sh
sh: .claude/hooks/block-rm-root.sh: Permission denied
exit=126

The path is relative

Run from src/:

sh: .claude/hooks/block-rm-root.sh: No such file or directory
exit=127

Both 126 and 127 are non-blocking errors, so the tool call goes ahead. To block when a hook can’t start, set "onFailure": "block" (v2.1.295 or later) (source: https://code.claude.com/docs/en/hooks, checked 2026-10-19).

jq is missing

This was the worst case we found. We ran a working rm guard with jq removed from PATH:

.claude/hooks/block-rm-root.sh: line 3: jq: command not found
exit=0

cmd=$(jq ...) came back empty, nothing matched, and the script reached exit 0. It allowed rm -rf / with no error notice. Make a guard script fail closed: check command -v jq first and exit 2 if it’s missing.

Does it block?

exit 1 instead of exit 2

Blocked: rm
exit=1

The docs say any exit code other than 0 or 2 is a non-blocking error, including 1 (same source). That the tool then runs in a live session: (not reproduced)

The reason goes to stdout

$ ... | .claude/hooks/stdout.sh 1>/dev/null
exit=2

Nothing was printed, so stderr was empty.

The hook times out

The JSON decision is ignored

Read the debug log

claude --debug-file <path> writes hook execution details to a file. claude --debug writes to ~/.claude/debug/<session-id>.txt. On v2.1.296 or later, a command hook on a tool event leaves a Hooks: line with its exit status and duration when it finishes (source: https://code.claude.com/docs/en/hooks, checked 2026-10-19). We confirmed both flags appear in claude --help for 2.1.303. We did not read a log from a live session. (not reproduced)

Environment variables that don’t reach hooks are a separate problem: Claude Code Env Vars Not Working. For why exit codes matter, see Claude Code Hooks in Practice.

Summary