Claude Code hooks are configured but never run

Nine times out of ten the hook is running. It just has no way to tell you it failed.

Last updated: 2026-09-21 · checked against Claude Code 2.1.278

The short answer

Most “my hook never runs” reports are a hook that does run and cannot tell you anything. A hook’s stderr goes to the debug log only — never to the transcript. So a script that fires and crashes on line one looks exactly like a script that was never wired up.

Before changing any configuration, make it speak:

claude --debug hooks

That prints hook resolution, which file each hook came from, the process spawn, and the error if there is one. In-session, /hooks lists what is actually loaded. If the hook is not in that list, it is a configuration problem. If it is in that list, it is a script problem, and those are two different afternoons.

The five causes, in the order they actually happen

#CauseHow you tell
1The hook runs and fails silently. stderr never reaches the transcriptclaude --debug hooks shows the spawn and the error
2The matcher does not match. A plain tool name is an exact, case-sensitive string, not a globEdit matches; edit and Edit(*) match nothing
3The script exits 1 expecting to blockThe action goes through anyway. Only exit 2 blocks
4The script wants a terminalWorks when you run it by hand, dies as a hook
5It is loaded under an event you did not mean/hooks shows the event and the source file

Note what is not on this list: needing to restart. Claude Code watches the settings files, so an edit is picked up without restarting the session. If restarting appeared to fix it once, something else changed too.

The matcher is the one people get wrong

It is not a glob, and it is not always a regex. Three cases:

You writeIt means
"*", "", or no matcher key at allEvery event of that type
"Edit" or "Edit|Write"Exact tool names, case-sensitive. A pipe or comma separated list means any of them
"mcp__.*"Because it contains regex characters, it is treated as an unanchored regular expression

So Edit(*) is not “Edit with any arguments”. It is a literal string that matches no tool at all. The Bash(npm test:*) shape you may be copying from is permission syntax for permissions.allow, which is a different mechanism with different rules — see Claude Code keeps asking permission.

A hook that formats the file that just changed

The shape, then the three details that decide whether it works:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/format.sh" }
        ]
      }
    ]
  }
}

Where hooks live, and why removing one is harder than adding one

Hooks can arrive from five places at once, and they merge rather than override:

FileScope
~/.claude/settings.jsonEvery project you open
.claude/settings.jsonThis project, committed, shared with the team
.claude/settings.local.jsonThis project, just you, usually gitignored
A managed policy fileSet by an organisation, and not yours to disable
A plugin’s hooks/hooks.jsonWhatever the plugin brought with it

Because they merge, deleting a hook from your project file does not stop it if the same hook also sits in your user file or arrived with a plugin. /hooks names the source file for each one, which is the fastest way to find the copy you forgot about.

If none of that is it

Put the output somewhere you will actually see it. stderr is invisible; a file is not:

claude --debug hooks --debug-file /tmp/cc-debug.log

Then tail -f /tmp/cc-debug.log in a second terminal and trigger the tool. If the hook never appears, it is not loaded, and the problem is in one of the five files above rather than in your script.

The free starter repo ships a PostToolUse formatter and a Stop reminder that both work, which is often a faster fix than debugging your own.

Free starter repo

Related

What does it cost to have someone set this up?

Flat prices, written scope, stop at any rung. No call to book, no retainer.

RungWhat you getClock
Free — starter repoA real setup you copy yourselfnow
$29 — Setup LiteOne project: CLAUDE.md, allowlist, one skill, as a pull request you review24 hours
$99 — Setup Sprint3–5 skills, hooks, up to 3 MCP servers, plus a runbook48 hours
$79 — Surface AuditAlready set up and still fighting it: contradictions and an exact fix list48 hours
Setup Sprint $99 Or start free

Hooks and MCP servers are in Sprint, not Lite — which one you actually need. Clocks run from a complete intake, not from the Stripe receipt.