DEV Community

Cover image for Your Hook Is Configured Correctly and Never Runs
quintetkit
quintetkit

Posted on

Your Hook Is Configured Correctly and Never Runs

You write a hook. The JSON is valid. The keys are spelled right. No error appears.

Nothing happens.

The hard part is that nothing points at the mistake. Startup is clean, --debug
says nothing, and the hook simply does not run.

.claude/ has several states that are syntactically correct and semantically
inert.
Below are the ones the official documentation explicitly describes as
ignored or non-functional.

1. Some events silently discard matcher

This one is stated outright:

If you add a matcher field to an event without matcher support, it is silently ignored.
Hooks

Silently ignored. Not an error.

The events with no matcher support, taken from the documentation's own table:

CwdChanged, UserPromptSubmit, PostToolBatch, Stop, TeammateIdle,
TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, MessageDisplay
Enter fullscreen mode Exit fullscreen mode

So this:

{
  "hooks": {
    "UserPromptSubmit": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "./guard.sh" }] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The matcher disappears. guard.sh runs on every prompt submission.

The person who wrote it believes it fires only for Bash. It fires every time, and
nothing in the system suggests otherwise.

This is the failure where a filter you thought you applied was never applied. It
can be worse than the hook not running at all.

2. if is only evaluated on tool events

if narrows on the tool name and its arguments together.

For tool events, you can filter more narrowly by setting the if field on
individual hook handlers.

Tool events is the condition. There are five:

PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied
Enter fullscreen mode Exit fullscreen mode

Write if on anything else and that handler never runs.

{
  "hooks": {
    "SessionStart": [
      { "hooks": [{ "type": "command", "command": "./init.sh", "if": "Bash(git *)" }] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

SessionStart has no tool. There is nothing for the condition to match, so it
never fires.

The two failures point in opposite directions:

what you wrote what happens
matcher ignored narrowed runs every time
if not evaluated conditional never runs

Neither produces an error.

3. Deprecated keys still parse

Old settings survive in copied configs and old blog posts.

key in your file what replaced it
ignorePatterns permissions.deny
includeCoAuthoredBy attribution
disableArtifact enableArtifact (the boolean inverts)
voiceEnabled voice.enabled

disableArtifact is the trap. The replacement inverts the sense. Rewriting
"disableArtifact": false to "enableArtifact": false mechanically flips the
meaning.

4. Required handler fields depend on the type

{ "type": "command" }
Enter fullscreen mode Exit fullscreen mode

No command. Nothing happens.

type required
command command
http url
mcp_tool server, tool
prompt prompt
agent prompt

The common shape is changing the type and leaving the old field behind
switching command to http, forgetting to delete command, forgetting to add
url.

5. Plugin-provided MCP tools have different names

Here the matcher is written and simply never matches.

Tools from an MCP server bundled by a plugin carry the plugin name in the tool
name
:

mcp__plugin_<plugin-name>_<server-name>__<tool>
Enter fullscreen mode Exit fullscreen mode

A matcher written against the bare server key will never fire for them.

✗ mcp__db__.*                    misses everything from the plugin
✓ mcp__plugin_my-plugin_db__.*
Enter fullscreen mode Exit fullscreen mode

Why this class is the bad one

They share one property: the failure is not observable.

typo         -> error at startup, fixed in a minute
silently ignored -> nothing happens, and it is operated for months
                    by someone who believes it works
Enter fullscreen mode Exit fullscreen mode

Hooks are especially bad for this. If you write a hook to block dangerous
commands and it is inert, you find out on the day it fails to block one.

The config file cannot tell you this

The only way is to check it from outside. I wrote a small tool that reads
.claude/ and reports only what the documentation explicitly calls ignored or
broken
:

$ ccheck
error .claude/settings.json  matcher on CwdChanged is silently ignored
      https://code.claude.com/docs/en/hooks
warn  .claude/settings.json  ignorePatterns is deprecated; use permissions.deny
      https://code.claude.com/docs/en/settings-reference
Enter fullscreen mode Exit fullscreen mode

I gave it one constraint:

A rule may exist only if the official documentation says the thing errors, is
skipped, or is ignored.

That is why every finding carries a source URL. A rule I cannot cite does not get
written
, because at that point it is my taste, not a defect.

The constraint left a lot unchecked: model name validity, path pattern
correctness, unknown keys. All of them look useful. All of them would be guesses,
and a checker that guesses produces false positives.

A checker stops being read after the first false positive.

MIT:

https://github.com/quintetkit/ccheck

Every event, what its matcher is tested against, and where if applies — in one table:

https://quintetkit.github.io/en/reference/claude-code-hooks.html

Takeaways

  • .claude/ has states that are valid syntax and inert semantics, with no error
  • Some events silently drop matcher — the hook then runs every time
  • if is evaluated on five tool events only; elsewhere the handler never runs
  • Deprecated keys still parse. disableArtifact inverts when replaced
  • Required handler fields vary by type, and the old field survives a type change
  • Plugin-provided MCP tools carry the plugin name; a bare matcher misses them
  • Configuration that fails silently has to be checked from outside
  • The rule for the checker: if you cannot cite it, do not report it

Related


I publish the configuration for splitting Claude Code into separate personas —
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run
./setup.sh, and it works. It does not depend on your tech stack.

https://github.com/quintetkit/quartet

I built one real tool using nothing but this workflow. Every Issue, PR, review
and merge is still there. The parts that went wrong were not deleted.

https://github.com/quintetkit/mdlinkcheck

The version that adds a UI Designer persona, review criteria, a per-Issue
parallel execution script and a 10-chapter guide is on the
product page.

The full kit — five personas, the scripts and the complete guide in English and Japanese — is on BOOTH, a Japanese store with an English interface that takes international cards.

https://quartet-dev.booth.pm/items/8807156

Top comments (0)