PostToolUse hooks never fire for MCP tool calls — matcher verified correct, native tools fine (v2.1.198; is this intended?)

Open 💬 0 comments Opened Jul 2, 2026 by Chulf58

Environment

  • Claude Code 2.1.198 (latest available at time of writing)
  • WSL2 (Ubuntu, kernel 6.18.33.1-microsoft-standard-WSL2), Node v24.14.0
  • MCP server: stdio transport, bundled with a local plugin (loaded via --plugin-dir), tools surfaced as mcp__plugin_<plugin>_<server>__<tool>
  • Hook: PostToolUse command hook registered in the plugin's hooks/hooks.json

Expected behavior

The hooks documentation shows MCP-style matcher examples (e.g. mcp__memory__.*), and the 2.1.195 changelog entry about hyphenated MCP matchers (mcp__brave-search__.* now exact-matching) implies MCP tool names participate in tool-hook matching. So a PostToolUse hook whose matcher covers an MCP tool name should execute after that tool call completes.

Actual behavior

The hook never executes for MCP tool calls. The same hooks.json fires PostToolUse hooks reliably on native tools (Read, Edit|Write|MultiEdit, WebSearch|WebFetch|Task|Agent) in the same sessions.

What we verified (live probe, 2026-07-02, v2.1.198)

Registration under test:

"PostToolUse": [
  {
    "matcher": "mcp__sterling__knowledge_create|mcp__plugin_sterling_sterling__knowledge_create",
    "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/h11-note-structure.mjs\"", "timeout": 120 }]
  }
]
  1. The matcher is provably correct. A unit test parses hooks.json and asserts new RegExp(matcher).test('mcp__plugin_sterling_sterling__knowledge_create') — passes.
  2. The hook script works when invoked directly. Feeding it a synthesized PostToolUse stdin payload ({cwd, tool_input, tool_response}) produces all expected side effects (it spawns a headless extraction and writes records to a SQLite store). Tested both from source and as the deployed esbuild bundle.
  3. Live MCP tool calls produce none of the hook's observable effects. Two knowledge_create calls from a live session: no records written, none of the hook's fallback bookkeeping rows (it records a row on every failure path — the script cannot fail silently), no hook stdout/stderr surfaced, and no multi-second latency from the subprocess the hook spawns. The hook's own dependencies were ruled out (the CLI it spawns is present at ~/.local/bin/claude).
  4. Native-tool PostToolUse hooks fire in the same configuration. Several other PostToolUse hooks in the same hooks.json (matchers Read, Edit|Write|MultiEdit, WebSearch|WebFetch|Task|Agent) all execute normally, so hook plumbing in general is healthy.

Possible confound worth noting for triage

In the probe sessions the MCP tools were deferred tools (schemas loaded on demand via ToolSearch rather than at session start). We could not separate "PostToolUse never fires for MCP tools" from "PostToolUse doesn't fire for deferred-loaded tools" — if hook matching is bound at session start against the loaded-tool set, deferred loading would explain the symptom. We found no documentation covering the interaction either way.

Relationship to existing issues

  • #24788 ("PostToolUse additionalContext silent for MCP tools") describes the same shape — correctly registered PostToolUse hook, silent only on MCP tool calls — and was closed "not planned" without a stated rationale. If MCP tools are intentionally excluded from PostToolUse, the hooks docs don't say so, and the mcp__* matcher examples actively suggest otherwise.
  • #25981 (hooks silent on Windows, fixed via Git Bash) is not this: we're on WSL/Linux and native-tool hooks fire.
  • No fix appears in the changelog through 2.1.198.

Minimal repro (expected path)

Our observation comes from a plugin-bundled server, but this should reproduce with any project-scope MCP server:

  1. .mcp.json with any stdio MCP server exposing one tool.
  2. .claude/settings.json:

``json
{
"hooks": {
"PostToolUse": [
{ "matcher": "mcp__.*", "hooks": [{ "type": "command", "command": "touch /tmp/posttooluse-fired" }] }
]
}
}
``

  1. Call the MCP tool from a session; check for /tmp/posttooluse-fired.
  2. Run any native tool (e.g. Read) with the matcher swapped to Read to confirm the hook itself is sound.

Ask

  1. Is PostToolUse (and PreToolUse?) intended to fire for MCP tool calls?
  2. If yes — this is a bug report for 2.1.198 (repro above); does deferred tool loading affect hook matching?
  3. If no — could the hooks docs state the exclusion explicitly? The mcp__* matcher examples currently read as a promise, and the failure mode is completely silent: a hook that looks correctly wired simply never runs, which is the worst kind of integration surprise to debug.

View original on GitHub ↗