Claude Code Hooks Not Working? How to Find Out Why a Hook Doesn’t Fire

Claude Code Hooks Not Working? How to Find Out Why a Hook Doesn’t Fire

When you find Claude Code hooks not working, the cause is almost always one of five things: broken JSON in the settings file, a misspelled event name, a matcher that never matches, a script that can’t run, or the wrong exit code. Claude Code tells you about most of them, but only if you know where to look.

The short fix: run /hooks inside Claude Code. If your hook is not listed, the settings file is the problem, so check for a Settings Error or Settings Warning at startup. If it is listed but nothing happens, check the matcher spelling, then look in the chat for a hook error line, and use exit 2 when the hook is meant to block something.

We broke five hooks on purpose, one cause each, and recorded what Claude Code showed for every one. Then we fixed them and ran the same test again.

This guide is part of our help series. Browse all our Claude Code tutorials for more fixes and how-tos.

Tested with Claude Code v2.1.322 on macOS on November 14, 2026. Every failure below was reproduced in a throwaway project with project-level hooks. The sections marked as coming from the docs follow Anthropic’s official troubleshooting notes, and the Windows note follows the official instructions as well.

Claude Code Hooks Not Working: Quick Summary

Find your symptom in the first column. The first five rows are failures we triggered ourselves, so the wording is what Claude Code v2.1.322 prints.

What you seeCauseFix
Settings Error at startup, /hooks says no hooks yetInvalid JSON in the settings fileRemove trailing commas and comments
Settings Warning: Unknown hook eventMisspelled event nameUse the exact name, such as PreToolUse
Hook is listed in /hooks but never runs, no errorMatcher does not match the tool nameWrite Bash, not bash
hook error with Permission deniedScript is not executableRun chmod +x on the script
Failed with non-blocking status code, action still happensScript exits 1 instead of 2Change it to exit 2
Hook prints JSON but nothing changesExtra output before the JSON, or a field at the wrong levelCheck the debug log (from the docs)
Diagram of five checks to run in order when a Claude Code hook does not fire
Run these five checks in order and stop at the first one that fails.

Why Claude Code Hooks Stop Working

A hook is a command that Claude Code runs at a set moment, such as right before a tool call. You define it in a settings file with three parts: an event name, an optional matcher, and the command to run. If you are new to them, our hooks guide covers the basics.

Each part is a place where things can go wrong. The file has to load, the event name has to exist, the matcher has to match, the script has to start, and the exit code has to say what you mean.

For our test we built a small project with three hooks. One logs every shell command to hook.log, one is meant to stop rm commands, and one writes a time stamp after each command finishes.

A hook that does nothing is rarely silent. The clue is usually on screen at startup, in the /hooks menu or in the chat itself.

Cause 1: Invalid JSON in the Settings File

This is the cause that takes out every hook at once. We added one trailing comma after a hook entry in .claude/settings.json and started Claude Code.

It stopped at a Settings Error screen before the session opened. The message read “Invalid or malformed JSON”, followed by “Files with errors are skipped entirely, not just the invalid settings.”

Claude Code Settings Error screen saying the settings file has invalid or malformed JSON and is skipped entirely
One trailing comma made Claude Code skip the whole settings file.

That second line matters. A single stray comma does not disable one hook. It disables every hook, permission rule and setting in that file.

We chose “Continue without these settings” and ran /hooks. The menu said “no hooks yet”, which confirms nothing was loaded.

The /hooks menu in Claude Code showing no hooks yet after the settings file failed to load
With the file skipped, the /hooks menu is empty.

How to fix it

  1. Run jq empty .claude/settings.json in your terminal. It prints nothing for valid JSON and gives a line and column number for an error.
  2. Remove trailing commas and any comments. The docs say neither is allowed in settings files.
  3. Restart Claude Code and run /hooks again to confirm the hooks appear.

In our test, jq pointed straight at the problem: “Expected another array element at line 8, column 9”. If you are not sure which file Claude Code reads, see our guide on where settings.json lives.

Cause 2: A Misspelled Event Name

Event names must match exactly. We wrote PreToolUsed instead of PreToolUse and started Claude Code again.

This time we got a Settings Warning instead of an error. It said: Unknown hook event “PreToolUsed” was ignored, and then listed every valid event name.

Claude Code Settings Warning saying the unknown hook event PreToolUsed was ignored, with the list of valid events
A typo in the event name gets a warning at startup, along with the full list of valid names.

The warning also said “The values listed above were skipped; the rest of the file is in effect.” So unlike bad JSON, a wrong event name only drops the hooks under that name.

The result was the same for our hook, though. After choosing Continue, /hooks again showed “no hooks yet”, because the only hook in the file sat under the misspelled name.

How to fix it

Copy the name from the warning’s list. Names are written in one word with capital letters inside, such as PreToolUse, PostToolUse, UserPromptSubmit and SessionStart.

Also check that you picked the right event. The docs note that PreToolUse fires before a tool runs and PostToolUse fires after it succeeds, so a hook on the wrong one runs at the wrong time or not when you expect.

Cause 3: A Matcher That Never Matches

This is the quiet one. There is no warning at startup and no error in the chat, and the hook simply never runs.

We set the logging hook’s matcher to bash in lowercase. The tool’s real name is Bash, and the docs state that matchers are case-sensitive.

The /hooks menu listed all three hooks as if everything was fine. Look at the column after each command, though: it shows the matcher, and the first one reads bash while the others read Bash.

The /hooks menu in Claude Code listing three hooks, with the first matcher written as lowercase bash
The hook is loaded, but its matcher is in the wrong case, so it will never fire.

We then asked Claude to run a shell command. The log file was never created.

How the debug log proves it

To see the matching itself, we started Claude Code with a debug file and the verbose log level:

CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose claude --debug-file debug.log

After one shell command, the log showed two matchers found under PreToolUse, but only one matched the query “Bash”. The lowercase one was skipped without a word.

Claude Code debug log lines showing two hook matchers found in settings but only one matched for the Bash tool
The verbose debug log shows how many matchers were found and how many matched.

How to fix it

  • Use the exact tool name: Bash, Edit, Write, Read.
  • For two tools, write Edit|Write. The docs say a comma works the same way.
  • For MCP tools, the name follows the pattern mcp__server__tool, so a matcher like mcp__github__.* covers one server.
  • To match everything, leave the matcher out or use "*".

The docs add one more trap. Events such as Stop and UserPromptSubmit do not support matchers at all, so a matcher there is silently ignored. And the if field only works on tool events; on any other event it stops the hook from running.

Cause 4: The Script Can’t Run

Here the hook fires, but the script behind it fails to start. We left our time stamp script without execute permission.

Claude Code showed the failure right in the chat, under the tool call: “PostToolUse:Bash hook error”, then “Failed with non-blocking status code” and the shell’s own message ending in “Permission denied”.

Claude Code chat showing a PreToolUse hook error that did not block and a PostToolUse hook error with Permission denied
Both failures appear under the tool call, and the command still ran.

The debug log recorded the same run as “finished with status 126”. That is the shell’s code for a file it found but could not execute.

How to fix it

  1. Run ls -l .claude/hooks and look for an x in the permissions of each script.
  2. Run chmod +x .claude/hooks/your-script.sh on any that lack it.
  3. Reference scripts through the project folder, as in "$CLAUDE_PROJECT_DIR"/.claude/hooks/your-script.sh, so the path works from any folder.

The docs list two related messages. “No such file or directory” means the path in your settings is wrong, and “jq: command not found” means the script needs jq installed, or should parse JSON with Python or Node.js instead.

Cause 5: The Wrong Exit Code

This one is the most dangerous, because the hook runs and still fails at its job. Our guard script printed “Blocked: rm commands are not allowed” and ended with exit 1.

Claude asked permission, ran rm old-draft.txt and the file was deleted. The chat showed “PreToolUse:Bash hook error” and “Failed with non-blocking status code: Blocked: rm commands are not allowed”, which is the first box in the screenshot above.

The words “non-blocking” are the giveaway. The docs explain that exit 1 is treated as a hook error, not as a decision, so the action goes ahead.

Diagram showing what hook exit codes 0, 1 and 2 do in Claude Code
Only exit code 2 blocks the action. Exit 1 just reports an error.

Test the script by hand

You can check a hook script without Claude Code. Pipe sample input into it and print the exit code, as the docs suggest:

echo '{"tool_input":{"command":"rm old-draft.txt"}}' | .claude/hooks/block-rm.sh; echo "exit code: $?"

Our broken script printed its message and “exit code: 1”. The same screenshot shows the missing x on stamp.sh from Cause 4 and the jq check from Cause 1.

Terminal showing file permissions of hook scripts, a manual test of a hook script returning exit code 1, and a jq JSON check
Three checks in the terminal: file permissions, a manual run of the script, and a JSON check.

How to fix it

Change exit 1 to exit 2 and write the reason to stderr with >&2. Claude receives that text as feedback.

#!/bin/bash
command=$(jq -r '.tool_input.command')
if [[ "$command" == rm* ]]; then
  echo "Blocked: rm commands are not allowed" >&2
  exit 2
fi
exit 0

The docs also describe onFailure: "block", a setting that makes a hook block the action when it fails to start. It is worth a look for hooks that enforce a rule.

The Fixed Hooks, Tested Again

We corrected all three problems: the matcher became Bash, the script got chmod +x, and the guard used exit 2. Then we asked Claude to run ls and to delete the file.

The ls ran. The rm was stopped with a red error naming the hook script and its message, and Claude said it would not try another way around the hook.

Claude Code chat showing the rm command blocked by the fixed PreToolUse hook with exit code 2
With exit 2, the hook stops the command and Claude gets the reason.

The files on disk told the same story. The log held both commands, the time stamp file existed, and old-draft.txt was still there.

Terminal showing hook.log with two logged commands, the time stamp file and the file that was protected from deletion
Proof that all three hooks ran: the log, the time stamp and the file that survived.

Note that the logging hook recorded the rm command even though it was blocked. The docs explain why: all matching hooks run to completion, and one hook’s block does not stop the others.

More Causes From the Official Docs

We did not reproduce these, but Anthropic’s troubleshooting notes list them, and they are worth checking if the five above don’t fit.

SituationWhat the docs say
Hooks edited while Claude Code is openChanges are normally picked up on their own. If they don’t appear after a few seconds, restart the session.
Hook defined in a skill/hooks doesn’t list it until the skill has been used in the session.
Menu says only hooks from managed settings run hereYour organization set allowManagedHooksOnly. Your own hooks don’t run.
disableAllHooks is set to trueAll hooks are off. A project file can override your user file.
Hook prints JSON but it has no effectYour shell profile printed text first, or a field such as permissionDecision sits outside hookSpecificOutput.
Stop hook keeps Claude workingCheck the stop_hook_active field and exit early when it is true.
Stop hook doesn’t fireIt doesn’t run when you interrupt Claude, and API errors fire StopFailure instead.

For the JSON cases, the docs name the exact debug log lines to search for: “Hook output does not start with {” and “Hook JSON output had unrecognized keys”. For Stop hook patterns that work, see our Stop hook examples.

On Windows, the docs describe one extra message: “blocking because Git Bash wasn’t found”. We tested on macOS; the Windows steps follow Anthropic’s official instructions.

Try It Yourself

You can let Claude run the first round of checks for you. Open your project in Claude Code and paste this prompt:

Check the hooks in .claude/settings.json. For each one, tell me: is the JSON valid, is the event name a real hook event, does the matcher use the exact tool name, does the script exist and have execute permission, and what exit code does it use to block? Do not change anything yet.

Then run /hooks yourself and compare. If the two disagree, trust the menu, because it shows what Claude Code loaded.

Common Mistakes

  • Skipping past the startup warning. The Settings Error and Settings Warning screens name the file and the problem, so read them before pressing Enter.
  • Writing tool names in lowercase. bash, edit and write match nothing.
  • Using exit 1 to block. It is the usual failure code in shell scripts, but for hooks only exit 2 blocks.
  • Putting hooks in the wrong file. Project hooks go in .claude/settings.json and personal ones in ~/.claude/settings.json.
  • Expecting --debug to print on screen. The docs say it writes to a log file instead.
  • Assuming a listed hook is a working hook. The /hooks menu shows what loaded, not what matched or ran.

Pro Tips

  • Use claude --debug-file debug.log so the log lands where you can find it, then search it for lines starting with “Hooks:”. Each one shows the event, the command, the exit status and the time taken.
  • Press Ctrl+O for the transcript view when you want more detail on a hook error.
  • Give each new hook a visible side effect at first, such as a line in a log file. It makes “did it run” a one-second check.
  • Start from hooks that are known to work. Our ready-to-copy hooks are a good base.
  • For wider setup problems, the /doctor command checks your configuration as a whole.
  • If the message you see is not about hooks at all, our list of common Claude Code errors may have it.
Cheat sheet of commands and settings for fixing Claude Code hooks that are not working
Cheat sheet: the commands and settings used in this guide.

Frequently Asked Questions

Why is my Claude Code hook not firing?

The most common reasons are invalid JSON in the settings file, a misspelled event name, or a matcher that doesn’t match the tool name exactly. Run /hooks first. If the hook is missing, fix the file, and if it is listed, check the matcher.

Do I need to restart Claude Code after editing hooks?

Usually not. The docs say edits to settings files are normally picked up automatically. If the change doesn’t show in /hooks after a few seconds, restart the session.

Why did my hook run but not block the command?

It most likely exited with code 1. In our test, a hook that printed a “Blocked” message and exited 1 did not stop the command. Change it to exit 2.

How do I see hook output in Claude Code?

Errors show in the chat under the tool call. For full details, start with claude --debug-file debug.log and read that file, or run /debug in a running session to turn logging on.

Are hook matchers case-sensitive?

Yes. The docs say so, and our test confirmed it: a matcher of bash never fired for the Bash tool. You can read more about the assistant itself on the Claude AI page.

Conclusion

When Claude Code hooks are not working, work through the chain in order: does the settings file load, is the event name real, does the matcher match, can the script run, and does it exit with the right code. Claude Code flags bad JSON and unknown events at startup, /hooks shows what was loaded, the chat shows script errors, and the debug log shows matching and exit codes. The one failure with no message at all is a matcher that doesn’t match, so check the spelling and the case first. For everything else, see the official hooks troubleshooting guide and the hooks reference.

Newsletter

New AI tools in your inbox

The best new tools and the AI news that matters, in one short email. Unsubscribe any time.

Free · No spam · Unsubscribe any time

✓ Looks like you’re subscribed

Loading Google’s sign-up form…

Looks like you’re already subscribed

Thanks for reading! New AI tools and the AI news that matters will land in your inbox.

Not subscribed yet? Refresh the page to load the sign-up form.

The sign-up form couldn’t load. If you use an ad or content blocker, allow news.google.com and reload the page.

Free instant check before you pay. Takes a few seconds.

Scroll to Top