claude-code - 💡(How to fix) Fix Hook bug: PreToolUse permissionDecision:deny is silently ignored on ExitPlanMode [2 comments, 3 participants]

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…
GitHub stats
anthropics/claude-code#50660Fetched 2026-04-19 15:14:05
View on GitHub
Comments
2
Participants
3
Timeline
7
Reactions
0
Author
Timeline (top)
labeled ×5commented ×2

A PreToolUse hook matching ExitPlanMode that emits {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"..."}} has no effect — Claude Code proceeds to the "Accept this plan?" approval UI as if no hook existed. The hook script runs (confirmed via side effects like state-file writes), but the harness does not honor the deny.

The same output shape on a Write|Edit PreToolUse hook does enforce correctly and surfaces the reason as a permission prompt. The bug is scoped to the ExitPlanMode tool specifically.

Error Message

Actual: tool call proceeds, normal "Accept this plan?" UI appears, TEST DENY never shown to the user, model's tool-use result contains no deny/error. The tool call should be blocked by the harness before ExitPlanMode fires. The permissionDecisionReason string should surface to the user as a permission prompt (allow/deny dialog), matching (a) the documented behavior in the hooks output schema, and (b) the actual observed behavior when the same hook output is emitted against other PreToolUse matchers like Write|Edit. The model's subsequent turn should receive the deny as a tool-use error with the reason string attached.

Error Messages/Logs

None — the failure is silent. No error surfaces in the UI, no error appears in the model's tool-use result, nothing in the session transcript indicates the hook's deny was rejected. The only observable side effect is that the hook script runs to completion (verified via state-file writes performed by the script). The script's stdout JSON is apparently discarded by the harness when the matcher is ExitPlanMode. By contrast, emitting the identical JSON from a Write|Edit PreToolUse hook produces the expected permission prompt. Actual: tool call proceeds, normal "Accept this plan?" UI appears, TEST DENY never shown, no tool-use error delivered to the model.

Root Cause

A PreToolUse hook matching ExitPlanMode that emits {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"..."}} has no effect — Claude Code proceeds to the "Accept this plan?" approval UI as if no hook existed. The hook script runs (confirmed via side effects like state-file writes), but the harness does not honor the deny.

The same output shape on a Write|Edit PreToolUse hook does enforce correctly and surfaces the reason as a permission prompt. The bug is scoped to the ExitPlanMode tool specifically.

Fix Action

Fix / Workaround

Hook-based gating of ExitPlanMode (plan auditors, pre-approval compliance checks) is not enforceable via PreToolUse. Workaround: post-hoc enforcement via a Stop hook using decision:"block" — different harness code path, reliably enforced — but this fires after the approval UI, not before.

Code Example

#!/usr/bin/env bash
set -u
cat >/dev/null
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"TEST DENY"}}\n'
exit 0

---

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "ExitPlanMode",
        "hooks": [
          { "type": "command", "command": "/absolute/path/to/minimal-repro.sh", "timeout": 5 }
        ]
      }
    ]
  }
}

---

None — the failure is silent. No error surfaces in the UI, no error appears in the model's tool-use result, nothing in the session transcript indicates the hook's deny was rejected. The only observable side effect is that the hook script runs to completion (verified via state-file writes performed by the script). The script's stdout JSON is apparently discarded by the harness when the matcher is ExitPlanMode. By contrast, emitting the identical JSON from a Write|Edit PreToolUse hook produces the expected permission prompt.
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

Summary

A PreToolUse hook matching ExitPlanMode that emits {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"..."}} has no effect — Claude Code proceeds to the "Accept this plan?" approval UI as if no hook existed. The hook script runs (confirmed via side effects like state-file writes), but the harness does not honor the deny.

The same output shape on a Write|Edit PreToolUse hook does enforce correctly and surfaces the reason as a permission prompt. The bug is scoped to the ExitPlanMode tool specifically.

Minimal reproduction

  1. Create minimal-repro.sh:
#!/usr/bin/env bash
set -u
cat >/dev/null
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"TEST DENY"}}\n'
exit 0

chmod +x minimal-repro.sh.

  1. Wire in a plugin's hooks.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "ExitPlanMode",
        "hooks": [
          { "type": "command", "command": "/absolute/path/to/minimal-repro.sh", "timeout": 5 }
        ]
      }
    ]
  }
}
  1. Restart Claude Code (hooks load at session start).
  2. Enter plan mode (Shift+Tab), write any trivial plan, call ExitPlanMode.

Expected: tool call blocked, "TEST DENY" surfaced as a permission prompt. Actual: tool call proceeds, normal "Accept this plan?" UI appears, TEST DENY never shown to the user, model's tool-use result contains no deny/error.

Control: same shape works on Write|Edit

Change the matcher from ExitPlanMode to Write|Edit in the same hooks.json. Restart. Attempt to edit any non-trivial file. The same script blocks the edit and surfaces "TEST DENY" as expected. This rules out:

  • JSON shape issues (identical shape enforces on Write|Edit)
  • Missing hookEventName field (already present)
  • General hook infrastructure problems

Isolation experiment

Three progressively minimal hooks, all on PreToolUse ExitPlanMode:

#HookOutput shapeObserved
1Full production hook with transcript parsing, state file, bypass logic{"hookSpecificOutput":{"permissionDecision":"deny",...}} (no hookEventName)ignored
28-line minimal, same shape as #1sameignored
38-line minimal with "hookEventName":"PreToolUse" added (see script above)identical to known-working Write|Edit hookstill ignored

Narrowed to: bug is in the ExitPlanMode-specific harness integration, not output validation.

Environment

  • Platform: macOS (Darwin 25.3.0)
  • Claude Code: VS Code native extension
  • Plugin system: sampo-marketplace plugins (user-authored), hooks loaded via hooks.json
  • Date observed: 2026-04-19 (bug first surfaced 2026-04-18)

Impact

Hook-based gating of ExitPlanMode (plan auditors, pre-approval compliance checks) is not enforceable via PreToolUse. Workaround: post-hoc enforcement via a Stop hook using decision:"block" — different harness code path, reliably enforced — but this fires after the approval UI, not before.

What Should Happen?

The tool call should be blocked by the harness before ExitPlanMode fires. The permissionDecisionReason string should surface to the user as a permission prompt (allow/deny dialog), matching (a) the documented behavior in the hooks output schema, and (b) the actual observed behavior when the same hook output is emitted against other PreToolUse matchers like Write|Edit. The model's subsequent turn should receive the deny as a tool-use error with the reason string attached.

Error Messages/Logs

None — the failure is silent. No error surfaces in the UI, no error appears in the model's tool-use result, nothing in the session transcript indicates the hook's deny was rejected. The only observable side effect is that the hook script runs to completion (verified via state-file writes performed by the script). The script's stdout JSON is apparently discarded by the harness when the matcher is ExitPlanMode. By contrast, emitting the identical JSON from a Write|Edit PreToolUse hook produces the expected permission prompt.

Steps to Reproduce

Create minimal-repro.sh:

#!/usr/bin/env bash set -u cat >/dev/null printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"TEST DENY"}}\n' exit 0 chmod +x minimal-repro.sh.

Wire it in a plugin's hooks.json:

{ "hooks": { "PreToolUse": [ { "matcher": "ExitPlanMode", "hooks": [ { "type": "command", "command": "/absolute/path/to/minimal-repro.sh", "timeout": 5 } ] } ] } } Restart Claude Code (hooks load at session start).

Enter plan mode (Shift+Tab), write any trivial plan, call ExitPlanMode.

Expected: tool call blocked, TEST DENY surfaced as permission prompt. Actual: tool call proceeds, normal "Accept this plan?" UI appears, TEST DENY never shown, no tool-use error delivered to the model.

Control: change the matcher from ExitPlanMode to Write|Edit, restart, attempt to edit any non-trivial file — same script enforces correctly. Confirms the bug is scoped to the ExitPlanMode matcher specifically, not output shape or general hook infrastructure.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

No response

Claude Code Version

2.1.114 (VS Code native extension, anthropic.claude-code-2.1.114-darwin-arm64)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

No response

extent analysis

TL;DR

The issue can be worked around by using a Stop hook with decision:"block" instead of a PreToolUse hook for enforcing permissions on ExitPlanMode.

Guidance

  • The bug appears to be specific to the ExitPlanMode harness integration, not output validation, as the same hook output works for Write|Edit matchers.
  • To verify the issue, create a minimal reproduction script and hooks configuration as described in the issue body.
  • To mitigate the issue, use a Stop hook with decision:"block" to enforce permissions after the approval UI, although this is not the desired pre-approval behavior.
  • Investigate the ExitPlanMode-specific harness code to identify the root cause of the issue.

Example

No code example is provided as the issue is related to the harness integration and not a specific code snippet.

Notes

The issue is scoped to the ExitPlanMode matcher and does not affect other PreToolUse matchers like Write|Edit. The workaround using a Stop hook may not provide the exact desired behavior but can help enforce permissions.

Recommendation

Apply the workaround using a Stop hook with decision:"block" until the root cause of the issue is identified and fixed. This will provide some level of permission enforcement, although not at the desired point in the workflow.

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