Field note · 24 September 2026 · Daniel Wright

A PreToolUse hook with matcher mcp__memory looks finished and never fires. Official docs: the bare server prefix is an exact string. Use mcp__memory__.*

A cream card printed with mcp__memory lies on a dark wooden desk beside a brass nameplate engraved mcp__memory__.*, with a closed slate-blue box and a navy notebook behind
The cream card is the bare prefix. The brass plate is the matcher that fires.

The annoyance

I set a PreToolUse hook with "matcher": "mcp__memory" so it would fire on every tool from the memory server. /hooks listed it. The JSON looked finished. I called create_entities and nothing happened. Nothing threw.

Same miss with "matcher": "mcp__brave-search". I had the usual suspects: a lowercase tool name, a server file in the wrong place, a hook sitting outside settings. The server was connected. The hook was in settings. Then I changed the matcher to mcp__memory__.* and the same call fired first time.

The bare prefix is an exact string, and no tool is named mcp__memory. The same rule covers PostToolUse.

What Anthropic actually says

From the official hooks reference, under “Matcher patterns”. A matcher stays an exact string when it contains only this set:

Matcher value: “Only letters, digits, `_`, `-`, spaces, `,`, and `|`”
Evaluated as: “Exact string, or list of exact strings separated by `|` or `,` with optional surrounding whitespace”

Any other character puts it on the regular-expression path. The example on that row is the memory server:

“`^Notebook` matches any tool whose name starts with `Notebook`; `mcp__memory__.*` matches every tool from the `memory` server”

Under “Match MCP tools”, the tool name and the miss are explicit. Official text:

“MCP tools follow the naming pattern `mcp__<server>__<tool>`, for example:”
“`mcp__memory__create_entities`: Memory server's create entities tool”
“To match every tool from a server, append `.*` to the server prefix. The `.*` is required: a matcher like `mcp__memory` or `mcp__brave-search` contains only exact-match characters, so it is compared as an exact string and matches no tool.”

So mcp__memory__.* matches every tool from the memory server, and mcp__brave-search__.* matches every tool from a server whose name contains a hyphen. The version line on hyphens matters, because the bare hyphenated prefix used to do the opposite:

“Hyphens in the exact-match set require Claude Code v2.1.195 or later. On earlier versions a bare hyphenated prefix like `mcp__brave-search` is evaluated as an unanchored regular expression and matches every tool from that server. The `mcp__brave-search__.*` form works on every version.”

Plugin-bundled servers add a scoped segment. A matcher written against the bare server key never fires for those tools. Official text:

“Tools from a plugin-bundled MCP server use a scoped server segment that includes the plugin name: `mcp__plugin_<plugin-name>_<server-name>__<tool>`. A matcher written against the bare server key never fires for these tools. For a plugin named `my-plugin` that bundles a server under the key `db`, a `query` tool appears as `mcp__plugin_my-plugin_db__query`, so the matcher for every tool from that server is `mcp__plugin_my-plugin_db__.*`.”

The hooks guide, under “Match MCP tools”, shows the working GitHub form. Official text:

“MCP tools use a different naming convention than built-in tools: `mcp__<server>__<tool>`, where `<server>` is the MCP server name and `<tool>` is the tool it provides. For example, `mcp__github__search_repositories` or `mcp__filesystem__read_file`.”

The JSON under that heading uses "matcher": "mcp__github__.*".

The fix

Here is the matcher I had. It contains only exact-match characters, so it is compared with the whole tool name, and it matches none:

Wrong · mcp__memory · exact string
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"memory hook fired\" >&2"
          }
        ]
      }
    ]
  }
}

Append .* so the matcher leaves the exact-string set. This is every tool from the memory server, under the hooks key in a settings file:

settings.json · matcher mcp__memory__.*
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory__.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"memory hook fired\" >&2"
          }
        ]
      }
    ]
  }
}

A hyphen in the server name uses the same ending: mcp__brave-search__.*. That form works on every version. Before v2.1.195 the bare mcp__brave-search was a regex and matched every tool from that server.

A plugin-bundled server is the scoped name, not the bare key. For the guide’s my-plugin server db, every tool is:

Plugin-bundled server · mcp__plugin_my-plugin_db__.*
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__plugin_my-plugin_db__.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"plugin db hook fired\" >&2"
          }
        ]
      }
    ]
  }
}

Prove it

Prove /hooks shows the regex, then prove one real tool from that server hits it.

Self-check · /hooks then one memory tool
# In-session: /hooks
# Confirm the matcher reads mcp__memory__.*, not mcp__memory
# Trigger one memory tool, for example create_entities
# stderr should show: memory hook fired
# Optional: claude --debug and read which matchers were checked

If /hooks does not list the handler at all, the hook is still outside settings. If the server itself is missing from /mcp, the config never loaded. This note is the case where both of those are fine and the matcher still never fires.

Checklist

  1. Read the matcher. Letters, digits, underscores, hyphens, spaces, commas, and pipes only means an exact string.
  2. Name the tool mcp__<server>__<tool>. Memory’s create tool is mcp__memory__create_entities.
  3. To match every tool from that server, append .*: mcp__memory__.* or mcp__brave-search__.*.
  4. For a plugin-bundled server, use the scoped segment: mcp__plugin_<plugin>_<server>__.*.
  5. On a CLI older than v2.1.195, keep the .* form. A bare hyphenated prefix is a regex there and matches every tool from that server.
  6. Run /hooks, confirm the matcher shown ends in .*, then trigger one tool from that server.

Why it fails

I read mcp__memory as the start of every memory tool. The characters in that string are all in the exact-match set, so Claude Code compares it with the whole tool name. Nothing is named mcp__memory. The hook stays listed and never fires.

Same family as Lowercase Matcher Never Fires, where the string is the wrong exact name, `.mcp.json` Under `.claude/` Never Loads, where the server never arrives, and Standalone Hooks File Never Loads, where the handler is not in settings. This one is in settings, on a live server, and the matcher still matches nothing.

If you want to run this with other operators rather than on your own, we are doing that in the public Skool community, and that is https://www.skool.com/navaigate.

Sources

Make the next AI decision concrete.

NavAIgate helps leadership teams identify high-value AI opportunities, prove them safely and turn the winners into working systems.