Skip to content

BuildNot yet confirmed elsewhere1 publisher3 min readPublished

Claude Code hook matchers written as a bare MCP server prefix silently match no tool

Claude Code 2.1.289 hooks matched on the bare MCP prefix mcp__lab fired 0 times in 42 tool calls, a test published on dev.to found. Nothing errors, so a hook meant to log or block those calls leaves an empty log that looks like a quiet server.

The Engineer · Build desk

How we use AISend a correction

What happened

  • Appending .* to get mcp__lab__.*, the fix the hooks reference documents, fired the hook on all 42 calls to the standalone server.
  • Passed to --allowedTools as a permission rule, the same bare mcp__lab string approved every call it covered.
  • Written as the hook handler's if condition, mcp__lab also fired 0 of 42 times.
  • Once the same server was bundled inside a plugin, the documented mcp__lab__.* matcher fired 0 of 9 times.

Why it matters

  • contradiction The hooks page calls the if field permission rule syntax, where a bare prefix means a whole server, so the docs promise a server-wide filter that the tested release did not apply.
  • decision Packaging an MCP server as a plugin renames its tools, so every hook matcher written for the standalone server has to gain the plugin segment in the same change.
  • exposure Teams whose bare hyphenated matchers, such as mcp__brave-search, fired by accident before 2.1.195 lost that coverage on upgrade, and the only symptom is a hook that stops running.

Claude Code decides how to read a hook matcher from the characters in it [10]. A string made only of letters, digits, underscores, hyphens, spaces, commas and pipes is an exact name, or a list of exact names [10]. Any other character makes it a JavaScript regular expression, unanchored [10]. For tool events the value tested is tool_name, and MCP tools are named mcp__<server>__<tool> [11][12]. So mcp__lab is compared whole, and no tool has that name [10][12]. The hooks reference says so directly: the bare prefix "is compared as an exact string and matches no tool" [6].

Appending .* adds a character outside the exact set [6][10]. The pattern then runs through RegExp.prototype.test, and that test succeeds on a match anywhere in the value [11]. Plugin-bundled servers get a scoped segment instead: mcp__plugin_<plugin-name>_<server-name>__<tool> [13]. In that name mcp__ is followed by plugin_, so the substring mcp__lab__ never occurs and the unanchored test finds nothing [20]. The docs warn that a matcher written against the bare server key "never fires for these tools" [13]. By the same naming rule, we'd expect mcp__plugin_<plugin-name>_lab__.* to cover the bundled copy [21].

The permissions page reads the same string another way: "mcp__puppeteer matches any tool provided by the puppeteer server" [14]. The --allowedTools result follows from that rule [3]. The hooks page describes each handler's if field as "Permission rule syntax to filter when this hook runs" [15]. In our view the if result is the one to take upstream. The matcher and plugin failures follow rules the docs state, and this one contradicts them [4][15].

Hooks on an MCP server usually exist to log every call or to check a call before it runs, according to the writeup [18]. A matcher that matches nothing gives such a hook an excellent error record. The settings file loads and the session runs, but the hook never starts [7].

Matchers have gone quiet before. The 2.1.191 changelog entry fixed "hooks with comma-separated matchers" that were "silently never firing" [16]. In 2.1.195, hyphenated matchers such as mcp__brave-search stopped "accidentally substring-matching" and now exact-match, with mcp__brave-search__.* given as the way to match a whole server [17].

The counts come from 14 headless runs on 2026-10-05, using Claude Code 2.1.289 and claude-opus-5-5 [1][9]. The test server is a dependency-free Node script that reads newline-delimited JSON-RPC on stdin [19]. The writeup registered 22 hook groups side by side against its three tools and counted which fired for which tool [8]. Matching is string comparison against tool_name, so we'd expect the fire-or-not results to hold under other models [11]. They hold on other releases only while the matcher grammar stays the same, and the changelog records two changes to it, in 2.1.191 and 2.1.195 [16][17].

What to watch

  • A Claude Code release that makes the if field honour a bare server prefix the way permission rules do, or docs that stop calling it permission rule syntax.
  • A load-time warning for exact-string matchers that match no registered tool, the change that would make this failure visible.
  • A rerun of the 22-group test on a release after 2.1.289, since matcher rules changed in both 2.1.191 and 2.1.195.

Clarity's read

What the record supports and how the coverage leans. The claims behind it follow.

Reality

Evidence70
Adoption
Insufficient
Hype gap0
Incentives
Insufficient
Confidence68
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [1]

    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.

    ReportedSupportedSource: rulestack writeup on dev.toView cited source
  2. [2]

    A hook with the matcher mcp__lab__.* fired on 42 of 42 calls to the lab server's tools.

    ReportedSupportedSource: rulestack writeup on dev.toView cited source
  3. [3]

    The same bare string mcp__lab passed to --allowedTools approved every call it covered.

    ReportedSupportedSource: rulestack writeup on dev.toView cited source

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · October 10, 2026

    Claude Code's hook matcher mcp__lab fired 0 times in 42 calls, though the same string worked as a permission rule

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Topics

Loading related stories