Claude Code's hook matcher mcp__lab fired 0 times in 42 calls, though the same string worked as a permission rule A developer's 14 headless test runs of Claude Code 2.1.289 found that a hook matcher of `mcp__lab` fired 0 times across 42 calls to that MCP server's tools, while `mcp__lab__.*` fired 42 of 42, even though the same bare prefix passed to `--allowedTools` approved every call. The tests also showed an `if` condition of `mcp__lab` stayed silent (0 of 42) and that `mcp__lab__.*` itself fired 0 of 9 times once the server was bundled in a plugin, where tools use the scoped `mcp__plugin____` naming. The finding highlights three conflicting grammars for the same string: exact-match hook matcher, whole-server permission rule, and permission-style `if` filter. Across 14 headless runs of Claude Code 2.1.289, a hook whose matcher was the server prefix mcp lab fired 0 times in 42 calls to that server's tools, while mcp lab . fired 42 of 42, and the same bare mcp lab passed to --allowedTools approved every call it covered. Two more strings stayed silent: an if condition of mcp lab 0 of 42 , and the documented fix mcp lab . itself once the same server was bundled in a plugin 0 of 9 . A hook on an MCP server is usually written for one of two jobs: log every call to that server, or check a call before it runs. Either way the hook sits behind a matcher , and the natural thing to type there is the server's prefix, mcp github or mcp memory , because that is how a permission rule names a whole server. Claude Code's hooks reference says that string does nothing: it "is compared as an exact string and matches no tool". A matcher that matches nothing doesn't break anything you can see. The settings file loads, the session runs, and the hook just never starts. We wanted to see that happen, and to see which other plausible strings behave the same way, so we built a three-tool MCP server, registered 22 hook groups against it side by side, and counted which ones fired for which tool. Everything below ran on 2026-10-05 between 16:24 and 16:39 UTC with Claude Code 2.1.289 claude --version and claude-opus-5-5 selected with --model opus . The documentation quotes come from https://code.claude.com/docs/en/hooks and hooks.md , permissions.md , mcp.md and cli-reference.md on the same site, plus the Claude Code CHANGELOG.md on GitHub, all fetched between 16:20 and 16:25 UTC the same day. The hooks page sorts matchers by the characters they contain. Its table has three rows, quoted as fetched: " " , "" , or omitted: "Match all", which "fires on every occurrence of the event". , - , spaces, , , and | ": "Exact string, or list of exact strings separated by The next paragraph adds that a regular-expression matcher "is tested with JavaScript's RegExp.prototype.test , which succeeds on a match anywhere in the value", and that you should wrap a pattern in ^ and $ "when you need a whole-string match". For tool events PreToolUse , PostToolUse and three others the value being tested is tool name . The MCP part is short. Tools are named mcp