Plugin-sourced PreToolUse hooks never dispatch for MCP tool-name matchers (mcp__<server>__.*)
Description
A PreToolUse hook defined in a plugin's hooks/hooks.json, matching an MCP server's tool namespace (e.g. mcp__<server>__.*), never dispatches — even though the identical matcher string fires reliably when placed in a user's ~/.claude/settings.json instead, and every other (non-MCP-matcher) PreToolUse entry in the same plugin's hooks.json fires correctly.
Environment
- Claude Code version: 2.1.201
- Plugin: harness-kit
scaffold@harness-kit, confirmed active viaclaude plugin list(version 0.22.1) - OS: Arch Linux
Reproduction
- In a plugin's
hooks/hooks.json, add aPreToolUseentry:
``json`
{
"matcher": "mcp__agent-browser__.*",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/run_hook.py\" agent_browser_dashboard",
"timeout": 10
}
]
}
run_hook.py
is a thin dispatcher that writes an unconditional telemetry line ({"hook": "<name>", "event": "fire"}` to a JSONL file) before checking whether the target script exists, then execs it. See hooks/run_hook.py and hooks/hooks.json.
- Register an MCP server named
agent-browser(claude mcp add -s local agent-browser -- agent-browser mcp --tools core,tabs,debug) — any MCP server that exposes tools under that namespace works. - Enable the plugin (
claude plugin marketplace update <name>,claude plugin update <plugin>@<name>, restart). - Invoke any tool from that MCP server (e.g.
mcp__agent-browser__agent_browser_open). - Check the telemetry log: no entry for the hook name ever appears, meaning
run_hook.pywas never invoked — not a crash inside the target script, a dispatch-layer no-op.
Control test (works)
Placing the identical matcher string in the user's own ~/.claude/settings.json PreToolUse block, pointed at the same underlying script logic, fires reliably and repeatably (confirmed via the script's own side effect — starting a local server — across multiple fresh Claude Code sessions).
What's ruled out
- Not a schema/wiring problem: the entry is byte-identical in shape to other working entries in the same file (
claude plugin validatepasses clean). - Not a matcher-syntax problem: per docs,
mcp__<server>__.*is the documented form for hyphenated MCP server names, and matches unanchored regex correctly. - Not generic plugin-hook disablement: other
PreToolUseentries in the samehooks.json, matching built-in tool names (Bash,Write|Edit, etc.), fire correctly and are confirmed via the same telemetry mechanism. - Not plugin version/cache staleness: confirmed correct plugin version live via
claude plugin list, confirmed the exact matcher byte-correct in the resolved plugin cache directory, confirmed via a fresh process (checkedps -o lstartagainst the plugin-update timestamp) that this wasn't a stale in-memory hook set.
Expected behavior
Per docs: "This example is presented generically without any caveat about plugin-specific behavior" — plugin-sourced and settings.json-sourced hooks should resolve MCP tool matchers identically.
Actual behavior
Plugin-sourced PreToolUse + MCP-tool-name matcher: never dispatches.
Settings.json-sourced PreToolUse + identical MCP-tool-name matcher: dispatches reliably.
Additional verification
This finding was independently reviewed by a second AI agent (OpenAI Codex, fresh read of the same repo files, own test execution) which reached the same conclusion after ruling out the same alternative explanations — full review transcript available on request.
Workaround in use
Added a duplicate SessionStart hook (no tool-name matching involved) alongside the still-present PreToolUse entry, so the feature works today and will pick up automatically once this is fixed.