Claude Code hooks not working? Why they don't fire
Run /hooks first: a hook it doesn't list was never loaded, usually because it sits outside the "hooks" key of a settings file, in ~/.claude.json, or under an array matcher. A listed hook that never fires usually has a matcher that matches nothing, such as a lowercase or misspelt tool name.
Contents
When Claude Code hooks are not working, type /hooks before you change anything. A hook missing from that list was never loaded, so the problem is where or how you wrote it. A hook that is listed but never fires usually has a matcher that matches no tool. Those two cases have different causes, and Anthropic's debug guide lists both.
Is the hook loaded? Check /hooks first#
/hooks opens a read-only list of every hook registered in the session, grouped by event, and labels each with where it came from: user settings, project settings, local settings or a plugin. Select one to see the command it runs and the file it lives in.
Two notices in that menu explain an empty list on their own. Only hooks from managed settings run here means your organisation set allowManagedHooksOnly, so hooks in your own files don't run and aren't listed. A hooks-disabled notice means "disableAllHooks": true is in effect, from a settings file or a --settings flag. Claude Code uses the value left after precedence, so a true in .claude/settings.local.json overrides a false in your user file.
You don't have to restart after an edit. Claude Code picks up a changed settings file after a short delay; if /hooks still shows the old version, run it again.
Hook not showing in /hooks: the causes#
Each of these keeps a hook from loading at all.
- The hook is in a file of its own. There is no standalone hooks file for your user or project setup. Hooks go under the
"hooks"key of~/.claude/settings.json,.claude/settings.jsonor.claude/settings.local.json. Only plugins load a separatehooks/hooks.json. - It is in
~/.claude.json. That file holds app state and UI toggles.hooks,permissionsandenvbelong in~/.claude/settings.json, a different file one folder down. - The matcher is an array, as in
"matcher": ["Edit", "Write"]. Claude Code reports it as an invalid setting when an interactive session starts and inclaude doctor. UnderPreToolUseorPermissionRequestit costs more: none of that file's other hooks load either. Write one string,"Edit|Write". - The file has a
//comment or a trailing comma. Settings files are strict JSON, and either one breaks the whole file: an interactive session opens with a Settings Error, and aclaude -prun skips the file without a dialog. A misspelt event name such asPretoolUsecosts only that entry; Claude Code shows a Settings Warning and loads the rest. - It is a skill's frontmatter hook, and nobody has invoked the skill yet in this session. Run the skill, or ask Claude to, then open
/hooksagain.
For comparison, this is the shape that loads, in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "/Users/you/.claude/hooks/check.sh" }
]
}
]
}
}
Hook listed but not firing: the matcher#
The matcher for tool events is compared with the tool's name, and a matcher that matches nothing fails silently.
- Matching is case-sensitive.
"bash"matches nothing, because tool names are capitalised:Bash,Edit,Write,Read. - Since v2.1.191, a comma between names works:
"Edit,Write"matches like"Edit|Write". In older versions a comma was taken literally and the matcher never matched. Checkclaude --version, and update Claude Code or switch to|. - A misspelt tool name, such as
"Writes", matches no tool. - A matcher made only of letters, digits,
_,-, spaces, commas and|is read as exact names, somcp__memorymatches no tool. To catch every tool of that server, write the regular expressionmcp__memory__.*.
Or it's a different event#
Some "not firing" reports are the right hook on the wrong event.
Stopruns when Claude finishes responding, not when you press Esc. A turn that ends on an API error firesStopFailureinstead.PostToolUseruns only after a tool succeeds; failures go toPostToolUseFailure.- An
Edit|Writehook doesn't run when a Bash command such assed -irewrites the file. PermissionRequestfires only when Claude Code is about to ask you for permission, or to deny a call that can't ask.PreToolUsefires before every tool call its matcher matches.
Then there's folder trust. In an interactive session, Claude Code holds back hooks from every settings file, your own ~/.claude/settings.json included, until you accept the workspace trust dialog for that folder.
It fires, but nothing happens#
A hook that runs and fails can look like one that never ran. Press Ctrl+O for the transcript view: a failed hook shows a <hook name> hook error notice there.
- Exit code 1 doesn't block. It's a non-blocking error and the tool call goes ahead. A
PreToolUseguard has to exit 2, as the blocking ones in our eight hooks examples do. - A notice ending in
No such file or directorymeans the script never started. Pointcommandat an absolute path or at"$CLAUDE_PROJECT_DIR"/.claude/hooks/…, runchmod +xon the script, and installjqif the notice names it. - A slow hook times out. Command hooks get 10 minutes by default (30 seconds on
UserPromptSubmit). APreToolUsehook that times out doesn't block the call. - JSON output can be ignored. If your shell profile prints something first, the output no longer starts with
{and Claude Code reads it as plain text. ApermissionDecisionat the top level instead of insidehookSpecificOutputis dropped without an error.
Outside Claude Code, pipe the script the JSON a Bash call would send and print its exit code. A guard should print 2 for a call it refuses and 0 for one it lets through:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $?
Watch it live with claude --debug#
Start a session with claude --debug, trigger the tool, and read the log at ~/.claude/debug/<session-id>.txt; it doesn't print to the terminal. claude --debug-file /tmp/claude.log writes to a path you choose, so you can tail -f it in another window, and /debug turns logging on mid-session. The log records each event, which matchers were checked, and the hook's exit code and output. Since v2.1.296, a command hook on a tool event leaves a line when it finishes, such as Hooks: PostToolUse:Write [.claude/hooks/log-write.sh] finished with status 0 (31ms).
Search it for Hook output does not start with { and Hook JSON output had unrecognized keys when the JSON is the suspect. Setting CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose adds matcher counts; our environment variables reference lists the others that change how Claude Code runs.
If a hook still behaves oddly, claude --safe-mode starts a session with your CLAUDE.md, skills, plugins, hooks and MCP servers turned off. When the problem goes away there, one of your customisations causes it, for example a plugin with its own hook on the same tool. When a hook's job is to block something, a deny rule may be the simpler tool: allow, ask and deny rules shows how they compare.
Hooks in Shuriken#
If the hook you're fixing exists only to tell you Claude needs you, Shuriken does that without a hook: a desktop notification when a chat asks for approval or an answer, finishes or fails, for every agent and project, and never for the chat you're looking at (how the notifications work).
The hooks you keep still run. An ordinary Shuriken chat in a project starts Claude Code with your user, project and local settings, the same files the terminal reads, and Shuriken adds its own hooks next to yours, not in their place.
If /hooks lists entries whose command contains hooks claude and falls back to shuriken, they come from Shuriken's Status from agents in terminals setting, which is off by default. It adds them to ~/.claude/settings.json beside your own. They do nothing outside a Shuriken terminal, and switching the setting off removes only Shuriken's entries. To get notifications without writing hooks at all, download Shuriken.
Sources
- Debug your configuration: Check hooks (Claude Code docs) (accessed 24 October 2026)
- Hooks reference (Claude Code docs) (accessed 24 October 2026)
- Automate actions with hooks: Limitations and troubleshooting (Claude Code docs) (accessed 24 October 2026)
- Settings: Fix a broken settings file (Claude Code docs) (accessed 24 October 2026)
- CLI reference: --debug and --safe-mode (Claude Code docs) (accessed 24 October 2026)
- Claude Code changelog, v2.1.191 (accessed 24 October 2026)
Frequently asked questions
Do I need to restart Claude Code after editing a hook?
No. Claude Code picks up an edited settings file after a short delay, even a .claude folder created after the session started (since v2.1.257). If /hooks still shows the old hook after a few seconds, run it again, and restart the session if that doesn't help.
Why doesn't exit code 1 block the tool call?
Exit code 1 is a non-blocking error in Claude Code: you see a hook error notice and the action goes ahead. To block a PreToolUse call, exit with code 2 and write the reason to stderr.
Why doesn't my Stop hook run when I press Esc?
Stop hooks don't run when you interrupt Claude. They run when Claude finishes responding, and a turn that ends on an API error fires StopFailure instead.
Can I turn off one hook without deleting it?
No. Anthropic's reference says there is no way to disable a single hook while keeping it in the configuration. "disableAllHooks": true turns off all of them, except hooks from managed settings.
Does claude --safe-mode turn hooks off?
Yes, it starts a session without your hooks, CLAUDE.md, skills, plugins and MCP servers. Hooks set in your organisation's managed settings still run.