claude-code - 💡(How to fix) Fix PreToolUse hook deny reason not surfaced to agent's tool_result (v2.1.143)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When a PreToolUse hook blocks a tool call, neither of the two documented channels for delivering a human-readable reason to the agent (hookSpecificOutput.permissionDecisionReason via JSON+exit-0, or stderr+exit-2) reaches the model. The agent only receives the generic string Hook PreToolUse:Bash denied this tool with no explanation, making it impossible for the agent to understand why it was blocked and adjust its behavior.

Docs at https://code.claude.com/docs/en/hooks state that permissionDecisionReason is surfaced to Claude, and that exit code 2 causes "stderr text [to be] fed back to Claude as an error message". Both appear non-functional for PreToolUse-deny in 2.1.143.

Error Message

Docs at https://code.claude.com/docs/en/hooks state that permissionDecisionReason is surfaced to Claude, and that exit code 2 causes "stderr text [to be] fed back to Claude as an error message". Both appear non-functional for PreToolUse-deny in 2.1.143.

Exit 2 means a blocking error. Claude Code ignores stdout and any JSON in it. Instead, stderr text is fed back to Claude as an error message.

Root Cause

Real-world consequence: a plugin like hookify (https://github.com/anthropics/claude-plugins-official/tree/main/plugins/hookify) is designed to deliver project-specific guidance ("you must run skill X before running bd create") via PreToolUse-deny. Because the reason never reaches the agent, the agent retries the same blocked command, gets the same generic deny, and either gives up or guesses — defeating the plugin's purpose.

Fix Action

Fix / Workaround

  • Claude Code: 2.1.143
  • Platform: macOS (Darwin 25.4.0)
  • Shell: zsh
  • Tested with both interactive CLI and via subagent dispatch

Workaround currently in use

None. We've patched hookify locally to emit permissionDecisionReason correctly (it was missing), and verified the hook output is correct. The patch is dead-on-arrival until the harness side propagates the field.

Code Example

#!/usr/bin/env python3
import json, sys
input_data = json.load(sys.stdin)
if input_data.get('tool_name') == 'Bash' and 'echo-hookbug' in input_data.get('tool_input', {}).get('command', ''):
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "MARKER-REASON-12345: this string should reach the agent"
        }
    }))
sys.exit(0)

---

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "python3 /tmp/deny-hook.py"}]
    }]
  }
}

---

echo-hookbug-trigger

---

Hook PreToolUse:Bash denied this tool

---

#!/usr/bin/env python3
import json, sys
input_data = json.load(sys.stdin)
if input_data.get('tool_name') == 'Bash' and 'echo-hookbug' in input_data.get('tool_input', {}).get('command', ''):
    print("MARKER-REASON-67890: this stderr text should be fed back to Claude", file=sys.stderr)
    sys.exit(2)
sys.exit(0)
RAW_BUFFERClick to expand / collapse

PreToolUse hook deny reason not surfaced to agent's tool_result (v2.1.143)

Summary

When a PreToolUse hook blocks a tool call, neither of the two documented channels for delivering a human-readable reason to the agent (hookSpecificOutput.permissionDecisionReason via JSON+exit-0, or stderr+exit-2) reaches the model. The agent only receives the generic string Hook PreToolUse:Bash denied this tool with no explanation, making it impossible for the agent to understand why it was blocked and adjust its behavior.

Docs at https://code.claude.com/docs/en/hooks state that permissionDecisionReason is surfaced to Claude, and that exit code 2 causes "stderr text [to be] fed back to Claude as an error message". Both appear non-functional for PreToolUse-deny in 2.1.143.

Environment

  • Claude Code: 2.1.143
  • Platform: macOS (Darwin 25.4.0)
  • Shell: zsh
  • Tested with both interactive CLI and via subagent dispatch

Repro A — JSON permissionDecisionReason + exit 0 (documented channel #1)

Minimal hook script (/tmp/deny-hook.py):

#!/usr/bin/env python3
import json, sys
input_data = json.load(sys.stdin)
if input_data.get('tool_name') == 'Bash' and 'echo-hookbug' in input_data.get('tool_input', {}).get('command', ''):
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "MARKER-REASON-12345: this string should reach the agent"
        }
    }))
sys.exit(0)

Settings (~/.claude/settings.json or project .claude/settings.json):

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "python3 /tmp/deny-hook.py"}]
    }]
  }
}

Repro step: In a Claude Code session, ask the agent to run:

echo-hookbug-trigger

Expected (per docs): Agent's tool_result includes MARKER-REASON-12345, so the agent can read why it was blocked and adapt.

Actual: Agent's tool_result is exactly:

Hook PreToolUse:Bash denied this tool

MARKER-REASON-12345 is nowhere in the tool_result. Manual python3 /tmp/deny-hook.py < input.json confirms the hook emits the JSON correctly with exit 0.

Repro B — stderr + exit 2 (documented channel #2)

Same setup, but the hook script writes to stderr and exits 2:

#!/usr/bin/env python3
import json, sys
input_data = json.load(sys.stdin)
if input_data.get('tool_name') == 'Bash' and 'echo-hookbug' in input_data.get('tool_input', {}).get('command', ''):
    print("MARKER-REASON-67890: this stderr text should be fed back to Claude", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

Docs quote (https://code.claude.com/docs/en/hooks, "Exit code output"):

Exit 2 means a blocking error. Claude Code ignores stdout and any JSON in it. Instead, stderr text is fed back to Claude as an error message.

Expected: Agent's tool_result contains MARKER-REASON-67890.

Actual: Same as A — only Hook PreToolUse:Bash denied this tool. The stderr content is not propagated.

Impact

Real-world consequence: a plugin like hookify (https://github.com/anthropics/claude-plugins-official/tree/main/plugins/hookify) is designed to deliver project-specific guidance ("you must run skill X before running bd create") via PreToolUse-deny. Because the reason never reaches the agent, the agent retries the same blocked command, gets the same generic deny, and either gives up or guesses — defeating the plugin's purpose.

The user must currently manually intervene and tell the agent what the rule was — exactly the friction hookify-style plugins exist to eliminate.

What I'd expect

Either:

  1. permissionDecisionReason is included in the tool_result string the agent sees on PreToolUse-deny (e.g. Tool denied by PreToolUse hook: <reason>), OR
  2. stderr-on-exit-2 is appended to the tool_result, as the docs state.

Either would let agents understand and adapt to hook denials without user intervention.

Related issues

  • #33106 — PreToolUse permissionDecision: "deny" not enforced for MCP server tools
  • #37210 — PreToolUse permissionDecision: "deny" ignored for Edit tool
  • #52822 — PreToolUse permissionDecision: "allow" doesn't suppress permission prompt (v2.1.119)

These show PreToolUse-deny handling has multiple known bugs across 2.1.x; this report adds the reason-propagation case.

Workaround currently in use

None. We've patched hookify locally to emit permissionDecisionReason correctly (it was missing), and verified the hook output is correct. The patch is dead-on-arrival until the harness side propagates the field.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix PreToolUse hook deny reason not surfaced to agent's tool_result (v2.1.143)