Plugin-sourced PreToolUse hooks never dispatch for MCP tool-name matchers (mcp__<server>__.*)

Resolved 💬 0 comments Opened Jul 4, 2026 by Synx-x Closed Jul 7, 2026

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 via claude plugin list (version 0.22.1)
  • OS: Arch Linux

Reproduction

  1. In a plugin's hooks/hooks.json, add a PreToolUse entry:

``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.

  1. 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.
  2. Enable the plugin (claude plugin marketplace update <name>, claude plugin update <plugin>@<name>, restart).
  3. Invoke any tool from that MCP server (e.g. mcp__agent-browser__agent_browser_open).
  4. Check the telemetry log: no entry for the hook name ever appears, meaning run_hook.py was 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 validate passes 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 PreToolUse entries in the same hooks.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 (checked ps -o lstart against 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.

View original on GitHub ↗