Build1 distinct publisher3 min readPublished
The hooks documentation says a matcher on an event without matcher support is ignored rather than rejected. A Bash-scoped guard on UserPromptSubmit therefore fires on every prompt, and nothing at startup or in --debug says so.
The Engineer · Build desk

build
Claude Code hands a hook's exit code 2 back to the model as a refusal reason1 distinct publisher
build
Disproving one pointer in Lemmalog retracts every conclusion that rested on it1 distinct publisher
build
The overnight batch does not die from a bad model, it dies when an MCP server stops answering1 distinct publisher
build
A subshell in the log line zeroed $? before the Claude Code hook could return it1 distinct publisher
Compiled by The EngineerSomething wrong?How this is made
The config loader applies neither scoping field at load time, even though `matcher` and `if` follow different rules. The post quotes the hooks documentation directly: a matcher on an event without matcher support is silently ignored [2]. The `if` field is a narrower filter that tests a tool name together with its arguments, which is why the documentation scopes it to the five tool events [6]. An event with no tool in scope has nothing for the condition to read, so the handler never runs [7].
Put the two lists side by side and none of the ten matcher-less events is one of the five tool events [16]. On `Stop`, then, both scoping fields are inert, and they fail in opposite directions: write `matcher` and the handler runs on every stop, write `if` and it runs on none [16]. Neither state produces an error [8]. The post's judgement is that the over-firing case is the worse of the two, because a filter you believe you applied is worse than a hook you know is absent [5].
A third failure drops nothing at all. Tools from an MCP server bundled by a plugin carry the plugin name inside the tool name, in the form `mcp__plugin_<plugin-name>_<server-name>__<tool>`, so a matcher written against the bare server key, `mcp__db__.*`, is evaluated exactly as written and matches nothing; `mcp__plugin_my-plugin_db__.*` is the form that fires [11].
The same silence covers handler bodies. A handler of `{ "type": "command" }` with no `command` key does nothing, and the shape the post reports most often is a type switched from `command` to `http` with the old `command` left behind and no `url` added [10].
The author gave ccheck one rule: a finding may exist only if the official documentation says the thing errors, is skipped, or is ignored, and every finding prints the URL that says so [12]. Its sample output flags a matcher on `CwdChanged` as an error and `ignorePatterns` as deprecated in favour of `permissions.deny` [13]. That constraint is paid for in coverage: model name validity, path pattern correctness and unknown keys all go unchecked, because each would be a guess, and a checker stops being read after its first false positive [14].
The documentation table has to be the authority on what the loader actually does for the output to be worth anything in your repo, since the tool reports what the docs call ignored, not what the binary ignores [12]. If an event gains matcher support, or the loader diverges from the table, the checker goes stale in the docs' direction.
The silence itself is the post's observation rather than a documented guarantee: clean startup, nothing from `--debug`, and a hook that does not run [1]. Which is why the only place the mismatch is visible is a reader outside the session, comparing each field against the event it was written on.
Ranked by verification strength, evidence, and original report placement.
The dev.to post reports that a hook config can be valid JSON with correctly spelled keys and still never run: startup is clean, --debug reports nothing, and no error appears.
Claude Code's hooks documentation states that if you add a matcher field to an event without matcher support, it is silently ignored rather than treated as an error.
The documentation's own table lists the events with no matcher support: CwdChanged, UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, MessageDisplay.
In the post's example, a UserPromptSubmit hook written with "matcher": "Bash" and a command of ./guard.sh loses the matcher, so guard.sh runs on every prompt submission while the person who wrote it believes it fires only for Bash.
For tool events, the documentation says you can filter more narrowly by setting the if field on individual hook handlers, which narrows on the tool name and its arguments together; there are five tool events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied.
The if field is evaluated only on the five tool events; written anywhere else, such as a SessionStart handler carrying "if": "Bash(git *)", the handler never fires because SessionStart has no tool for the condition to match.
Distinct publishers with included, body-backed reporting in this cluster.
1 article · September 5, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Vendor docs quoted, unchecked by anyone else
The weight sits on two short quotations from Claude Code's hooks and settings pages plus two lists of event names, all transcribed by the same person who then wrote the checker. The quotations are specific and the URLs are printed, so a reader can check them in minutes, but no second publisher did, and nothing in this account dates the documentation, so the ten-event list cannot be tied to a particular Claude Code release.
Released, but unused so far
ccheck exists and is MIT-licensed, and that is where observability ends: no install counts, no other project reporting it in a pipeline, and no case of it catching a real inert hook. The misconfigurations are demonstrated on example JSON the author wrote, so there is also nothing here about how often working codebases carry them.
Runs cooler than its findings
Measured against what the source actually carries, the presentation undersells slightly. The headline behaviour is a quotation from the vendor, the checker is built to report only what the documentation calls ignored, and the author lists the three checks he refused to write rather than claiming coverage he lacks. Pulling the other way is the unevidenced line about silent configs being operated for months, the one place the piece reaches past what it can cite.
Free tool, paid kit, same post
The post that documents the documentation also closes by selling a five-persona Claude Code workflow kit through a product page and BOOTH. ccheck itself is free and MIT-licensed, so the pull is toward the author's paid material rather than toward inflating the tool, and none of it touches the quoted lines, which stand or fall on the vendor's pages.
Single account, self-checking claims
Confidence here is unusual: only one publisher carries the story, yet the load it carries is quotations a reader can confirm at the linked URLs, and the two failure modes are mechanical enough to reproduce in a scratch config. What keeps the number from going higher is version drift, since Claude Code's event set is clearly moving, and the absence of any independent transcription of the tables.