2026-09-05 · claudecode · ai · configuration · devops
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
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
So this:
{
"hooks": {
"UserPromptSubmit": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "./guard.sh" }] }
]
}
}
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
Write if on anything else and that handler never runs.
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "./init.sh", "if": "Bash(git *)" }] }
]
}
}
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" }
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>
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__.*
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
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
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
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 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
The workflow itself is available
Quartet, the four-persona version, is published free under MIT. Quintet adds a UI Designer persona, review criteria, a per-Issue parallel execution script, and a 10-chapter guide.
See the free version Product page