PostToolUse hooks never fire for MCP tool calls — matcher verified correct, native tools fine (v2.1.198; is this intended?)
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 asmcp__plugin_<plugin>_<server>__<tool> - Hook:
PostToolUsecommand hook registered in the plugin'shooks/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 }]
}
]
- The matcher is provably correct. A unit test parses
hooks.jsonand assertsnew RegExp(matcher).test('mcp__plugin_sterling_sterling__knowledge_create')— passes. - 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. - Live MCP tool calls produce none of the hook's observable effects. Two
knowledge_createcalls 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). - Native-tool PostToolUse hooks fire in the same configuration. Several other PostToolUse hooks in the same
hooks.json(matchersRead,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:
.mcp.jsonwith any stdio MCP server exposing one tool..claude/settings.json:
``json``
{
"hooks": {
"PostToolUse": [
{ "matcher": "mcp__.*", "hooks": [{ "type": "command", "command": "touch /tmp/posttooluse-fired" }] }
]
}
}
- Call the MCP tool from a session; check for
/tmp/posttooluse-fired. - Run any native tool (e.g.
Read) with the matcher swapped toReadto confirm the hook itself is sound.
Ask
- Is PostToolUse (and PreToolUse?) intended to fire for MCP tool calls?
- If yes — this is a bug report for 2.1.198 (repro above); does deferred tool loading affect hook matching?
- 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.