PreToolUse hook permissionDecision 'deny' not enforced for MCP server tool calls
Bug Description
PreToolUse hooks that return permissionDecision: "deny" are not enforced when the target tool is an MCP server tool. The hook fires correctly and returns a valid deny response, but the MCP tool call proceeds anyway.
Reproduction Steps
- Register a PreToolUse hook in
~/.claude/settings.jsontargeting an MCP tool:
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "/path/to/hook.sh",
"type": "command"
}
],
"matcher": "mcp__my_server__my_tool"
}
]
}
}
- The hook script reads stdin and returns a deny decision:
#!/bin/bash
INPUT=$(cat)
echo "BLOCKED: reason here"
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"reason here"}}'
exit 0
- When Claude calls the MCP tool, the hook fires and returns deny, but the MCP tool call is executed anyway.
Expected Behavior
The MCP tool call should be blocked, and the model should see the deny reason — identical to how PreToolUse deny works for built-in tools (Bash, Read, Write, etc.).
Actual Behavior
The hook fires, returns permissionDecision: "deny", but the MCP tool call proceeds and succeeds. The deny decision is effectively ignored.
Context
This was discovered while running multiple Claude Code instances in tmux panes with an identity enforcement hook. The hook correctly detects when an agent tries to send a message as a different identity via an MCP-based messaging server, returns deny, but the message is sent anyway.
Verified:
- Hook script works correctly when tested manually (
echo '...' | hook.sh) - Hook IS registered with the correct matcher pattern
- Built-in tool PreToolUse deny IS enforced (e.g., Bash hooks work)
- MCP tool PreToolUse deny is NOT enforced
Environment
- Claude Code CLI
- Linux
- MCP server type: HTTP (
"type": "http")
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗