[BUG] PermissionRequest hook 'allow' for ExitPlanMode is ignored since v2.1.199 — plan-approval chooser still blocks
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Since v2.1.199, a PermissionRequest hook that returns decision.behavior: "allow" for ExitPlanMode is executed (its stdin payload is delivered and its stdout response is read) but the decision is discarded. The native plan-approval chooser ("Claude has written up a plan and is ready to execute. Would you like to proceed?") is still displayed and blocks the session until answered manually.
This breaks tools that implement out-of-band plan approval via hooks (e.g. browser-based plan review UIs): the human approves in the external UI, but the terminal still hangs on the interactive chooser.
Variants tested on 2.1.201 — all ignored:
PermissionRequestallow withupdatedPermissions({"type": "setMode", "mode": "acceptEdits", "destination": "session"})PermissionRequestbare allow (noupdatedPermissions)PreToolUsehook withpermissionDecision: "allow"
Notably, the deny path still works on 2.1.201: decision.behavior: "deny" with a message round-trips correctly — the model receives the feedback, revises the plan, and calls ExitPlanMode again. Only the allow path is broken, which narrows the regression to how an allow decision is applied to the plan-approval dialog.
What Should Happen?
v2.1.198 behavior: when the hook returns allow, the plan-approval chooser is skipped, any updatedPermissions (e.g. setMode: acceptEdits) are applied, and the session proceeds directly into implementation. This matches the documented "Auto-approve specific permission prompts" flow in the hooks guide, and @anthropic-ai/claude-agent-sdk 0.3.201 still exports the same PermissionRequestHookSpecificOutput contract, so the current behavior appears to be an unintentional regression. Nothing in the 2.1.199–2.1.201 changelog mentions a behavior change for plan approval via hooks.
Error Messages/Logs
# Hook stdin payload captured on 2.1.201 (hook demonstrably fires; shape identical to 2.1.198):
{
"session_id": "7942a346-27fc-4d06-b627-84708db96ae9",
"cwd": "/tmp/repro",
"permission_mode": "plan",
"hook_event_name": "PermissionRequest",
"tool_name": "ExitPlanMode",
"tool_input": {
"plan": "# Context\n\nUser requested a simple file be created...",
"planFilePath": "~/.claude/plans/write-a-file-named-noble-crescent.md"
}
}
# Hook stdout response (read by Claude Code, then ignored):
{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","updatedPermissions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}]}}}
Steps to Reproduce
- Create
approve-hook.sh(chmod +x):
``bash``
#!/bin/bash
cat > /dev/null
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","updatedPermissions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}]}}}'
- Create
settings.json:
``json``
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{ "type": "command", "command": "/path/to/approve-hook.sh", "timeout": 600 }
]
}
]
}
}
- In an empty directory, run
claude --permission-mode plan --settings settings.json - Prompt:
Write a file named hello.txt containing exactly: hi - Claude writes the plan and calls
ExitPlanMode
Expected (observed on 2.1.198): chooser is skipped, session switches to accept-edits, hello.txt is created.
Actual (observed on 2.1.199 and 2.1.201): the chooser is displayed and blocks; hello.txt is never created. Capturing the hook's stdin confirms the hook ran and answered allow.
Version bracketing (same repro, driven programmatically against the TUI):
| Version | Result |
|---|---|
| 2.1.198 | allow honored — chooser skipped, plan implemented |
| 2.1.199 | allow ignored — chooser blocks |
| 2.1.201 | allow ignored — chooser blocks |
Claude Model
Other: reproduced with Haiku 4.5; the behavior is model-independent (also observed with Opus-class models).
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.198
Claude Code Version
2.1.201 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other: reproduced both in the desktop app and headlessly (expect/xterm-256color driving the TUI), so terminal-independent.
Additional Information
Related but distinct existing issues: #71061 (showClearContextOnPlanAccept not honored on hook approval — the hook-allow bypass itself still worked there), #50660 (PreToolUse deny ignored on ExitPlanMode, older). This report is specifically about the allow path regressing between 2.1.198 → 2.1.199 while deny continues to work.
3 Comments
Root cause and a verified workaround, from comparing notes with another affected tool.
Plannotator hit the same regression and traced it in the bundled CLI (backnotprop/plannotator#995): the PermissionRequest result handling now has a guard that throws away an
allowfor any non-MCP tool whererequiresUserInteraction()is true, unless the decision includesupdatedInput. It returns early as if the hook gave no decision at all, soupdatedPermissionsget dropped too. Thedenybranch isn't touched, which matches the allow/deny split I reported above.The workaround is to echo the event's
tool_inputback verbatim asdecision.updatedInput. I re-ran the repro from this issue on 2.1.201 with that envelope: the approval dialog is skipped, thesetMode: acceptEditspermission update applies, and implementation proceeds exactly as it did on 2.1.198. The same envelope is a no-op on 2.1.198, so hook authors can ship it unconditionally:One correction to the Plannotator report: it says >=2.1.200, but they upgraded straight from 2.1.198 and never ran 2.1.199. My version bisection shows 2.1.199 is the first affected release.
Since the workaround leans on guard internals, a real fix would still be welcome. If the new behavior is intentional, documenting that an ExitPlanMode
allowneedsupdatedInputwould also close this out. Happy to re-verify against a fix.One regression test that may pin this down cleanly is to assert the native plan-gate outcome, not only the hook result.
For ExitPlanMode, I would make the allow path prove three joins:
Then keep a paired negative case: allow without a digest-bound updatedInput on the affected path should be visible as native gate still pending, not as a successful approval. That mirrors the useful deny-path behavior isolated here and gives hook authors a stable contract for external approval UIs.
Reproduced on v2.1.233 (macOS), with the exact steps from the report.
approve-hook.shthat reads stdin and prints{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","updatedPermissions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}]}}}settings.jsonwith aPermissionRequesthook, matcherExitPlanMode, pointing at that scriptclaude --permission-mode plan --settings settings.json --model haikuWrite a file named hello.txt containing exactly: hiObserved: the hook runs (its stdin shows
"hook_event_name":"PermissionRequest","tool_name":"ExitPlanMode") and answers allow, but the plan chooser still renders and blocks:hello.txtis never created. TheupdatedPermissions(set mode to accept-edits) is dropped along with the allow.Expected (per the report): chooser skipped, mode switches to accept-edits,
hello.txtwritten.The workaround from the comments still works on 2.1.233: echoing the event's
tool_inputback asdecision.updatedInputskips the chooser ("User approved Claude's plan" then "Allowed by PermissionRequest hook"), applies accept-edits, andhello.txtis written.Assessment: This is intended behavior as of v2.1.199, but the expectation is reasonable and the docs don't fully cover it.
ExitPlanModeandAskUserQuestionare tools whose approval card is the user interaction, so a hook can only skip the card if it also supplies the answer viaupdatedInput(for ExitPlanMode, echoing the tool's input back is enough). The hooks docs say this in the PreToolUse section (https://code.claude.com/docs/en/hooks#pretooluse-decision-control: "Returning\"allow\"alone is not sufficient for these tools"), but the PermissionRequest section (https://code.claude.com/docs/en/hooks#permissionrequest-decision-control) never mentions it, and a bare allow is silently discarded with no warning to the hook author. We should make this clearer by documenting theupdatedInputrequirement in the PermissionRequest section and surfacing a debug/warning when a hook allow for one of these tools is ignored (or letting an accompanyingsetModecount as the approval for ExitPlanMode).🤖 Generated with Claude Code