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 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 if field 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 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 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