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
matcherfield 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
iffield 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 happensmatcher 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 itignorePatterns
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.
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 -
ifis evaluated on five tool events only; elsewhere the handler never runs - Deprecated keys still parse.
disableArtifactinverts 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
- What Works and What Doesn’t in CLAUDE.md
- What You Refuse to Check Decides the Quality of a Linter
- Assigning 5 Personas to Claude Code for Parallel Development
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 — is available here.