[BUG] PreToolUse hook permissionDecision "allow" does not suppress native permission prompt in interactive mode (v2.1.119)
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?
A PreToolUse hook (type: "command") that exits with code 0 and emits a well-formed hookSpecificOutput with permissionDecision: "allow" on stdout does not suppress the native Do you want to X? permission prompt in interactive mode.
The hook executes correctly, stdout JSON is parsed successfully by Claude Code (confirmed in debug log with "Successfully parsed and validated hook JSON output"), but the native UI prompt is still shown to the user, requiring manual Yes/No confirmation.
The documented behavior per hooks reference is that permissionDecision: "allow" bypasses the permission system. This appears to be a regression relative to #28812, which was confirmed resolved on v2.1.59.
Impact: programmatic approval workflows (e.g. remote approval UIs, audit-gated tool execution, compliance logging) cannot rely on hooks to gate tool calls without a redundant native confirmation from the user. Hook exit 2 (deny) correctly blocks the tool — only the allow suppression path is broken.
What Should Happen?
When a PreToolUse hook returns hookSpecificOutput with permissionDecision: "allow" and exits 0, Claude Code should execute the tool without displaying the native permission prompt.
This matches:
- The documented behavior in the hooks reference: "
allow: Bypass permission system and allow" - The resolved behavior in #28812, confirmed working on v2.1.59
- The stated SDK semantics: "bypassPermissions: Auto-approves all tool uses without prompts. Hooks still execute and can block operations if needed."
Error Messages/Logs
Hook stdout (captured via hexdump, confirmed valid JSON):
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"<reason>"}}
Debug log excerpt from ~/.claude/debug/<session>.txt:
[DEBUG] Hooks: Checking first line for async: {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"..."}}
[DEBUG] Hooks: Parsed initial response: {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"..."}}
[DEBUG] Successfully parsed and validated hook JSON output
[INFO] [Stall] tool_dispatch_start tool=Write toolUseId=toolu_... permissionDecisionMs=<N>
The "Successfully parsed and validated" line confirms JSON acceptance. However, permissionDecisionMs shows the delay corresponds to the native prompt wait (>60s until user presses Yes), not the hook response time (which was <1s).
When PermissionRequest event is added as a second hook target with identical JSON structure:
Hook PermissionRequest:Write (PermissionRequest) error:
Failed to run: Hook returned incorrect event name: expected 'PermissionRequest' but got 'PreToolUse'
After patching the hook to emit hookEventName dynamically from stdin's hook_event_name field, PermissionRequest parses successfully — but the native prompt still appears.
Steps to Reproduce
- Create a minimal "always allow" hook script:
cat > /tmp/fake-allow-hook.sh <<'EOF'
#!/bin/bash
# Discard stdin (hook event payload)
cat > /dev/null
# Emit well-formed allow decision per hooks reference
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"reproduction test"}}'
exit 0
EOF
chmod +x /tmp/fake-allow-hook.sh
- Configure Claude Code user settings:
mkdir -p ~/.claude
cat > ~/.claude/settings.json <<'EOF'
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|Bash|MultiEdit",
"hooks": [
{ "type": "command", "command": "/tmp/fake-allow-hook.sh" }
]
}
]
}
}
EOF
- Start Claude Code with hooks debug:
claude --debug hooks
- In the Claude Code prompt, request a Write operation:
Create a file at /tmp/repro.txt with the content "hello". Use the Write tool.
- Observe: the native
Do you want to create repro.txt? 1. Yes / 2. Yes, allow all / 3. Noprompt appears even though the hook has already returnedallowand Claude Code has loggedSuccessfully parsed and validated hook JSON output.
- Expected: no native prompt, Write executes directly.
Actual: native prompt shown, execution stalls until manual confirmation.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.59
Claude Code Version
2.1.119 (Claude Code)
Platform
Anthropic API
Operating System
Other Linux
Terminal/Shell
Other
Additional Information
Environment details
- Terminal: Konsole (KDE Plasma 6) on NixOS 25.05 — dropdown did not include Konsole as an option
- Install method:
nix profile install github:sadjow/claude-code-nix(self-contained Bun binary, ~180MB, auto-updated hourly from Anthropic releases) - Subscription: Claude Max (via organization)
- Shell: bash (via nix-shell)
Additional testing performed
1. PermissionRequest event test:
Added PermissionRequest as a second hook target with identical JSON structure. Claude Code rejected the payload with:
Hook returned incorrect event name: expected 'PermissionRequest' but got 'PreToolUse'
Patched the hook to emit hookEventName dynamically based on hook_event_name field from stdin. PermissionRequest then parsed successfully (confirmed via "Successfully parsed and validated hook JSON output" in debug log), but the native prompt still appeared.
2. --permission-mode bypassPermissions CLI flag:
When this flag was set, the hook was not invoked at all — no hook execution visible in debug log, no stdin consumed by the hook script. This contradicts the documented SDK behavior: "bypassPermissions: Auto-approves all tool uses without prompts. Hooks still execute and can block operations if needed." (SDK permissions docs).
Not filing this separately since it may be the same root cause — the hook system and the permission prompt suppression appear to be decoupled in 2.1.119.
3. Fresh state reproduction:
Reproduced on a clean ~/.claude/ (single settings.json, no prior hooks, no plugins, no MCP servers loaded). Bug is not state-dependent.
Related issues
- #28812 — confirmed working on v2.1.59; possible regression introduced between 2.1.59 and 2.1.119
- #41615 — similar symptom scoped to
.claude/sensitive paths; this issue is broader (all Write/Edit/Bash/MultiEdit operations) - #15897 —
updatedInputfield ignored on v2.0.76 (separate but related hook output parsing issue) - #13339 — VS Code extension ignores
permissionDecision: "ask"(different decision value, similar UI suppression gap)
Expected troubleshooting if helpful
Happy to provide:
- Full
~/.claude/debug/<session>.txtlog (redacted for paths) - Additional test matrix across hook event types (PreToolUse, PermissionRequest)
- Test with various
matcherpatterns (empty, explicit, regex) - Sample hook in other languages (Python, Rust) confirming same behavior
6 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Thank you for the duplicate suggestions. After reviewing each, I believe this issue is distinct — none of the three match the exact scenario here:
#51798 — Specific to
BashwithdangerouslyDisableSandbox: true. My reproduction uses theWritetool with no sandbox flags at all.#36059 — Concerns
permissionDecisionno longer overridingaskrules. My settings.json has noaskrules configured;permissions.allowonly lists read-only operations (Read,Glob,Grep,WebFetchfor docs domains).Write/Edit/Bashare not in any allow or ask list.#41615 — Specific to
~/.claude/sensitive-file paths. My target path is/tmp/argos-pas5-sandbox/repro.txt— a non-sensitive, non-home location.The distinguishing characteristic of this issue is that
permissionDecision: "allow"is not suppressing the native prompt for plainWriteon ordinary paths, under vanilla settings, with no sandbox flags, noask/allowrules covering the tool, and no sensitive-path logic involved. The hook is invoked, the JSON is parsed and validated (confirmed in debug log), yet the prompt still appears.Also worth noting: #28812 explicitly confirmed that
permissionDecision: "allow"correctly suppressed the native prompt as of v2.1.59. Between 2.1.59 and 2.1.119 (the version I'm reproducing on), this behavior regressed for the simple case described here.Happy to provide additional debug logs, try additional configurations, or narrow down the minimum-regression version if that helps triage.
Data point from 2.1.139 that might narrow this bug.
permissionDecision: "allow"is honored on Bash, Edit/Write/MultiEdit, MCP calls, skill invocations, Agent and WebFetch. All auto approve via the standardhookSpecificOutputshape with no dialog.The one tool where the dialog persists is
ExitPlanMode. Its tool class in the webview bundle has a custompermissionRequestthat renders the dialog directly, and the upstream categorizer pushes "plan-mode" rather than "write-code". That category doesn't seem to consult hook output.So the title may overstate the scope. If it's actually
ExitPlanMode-only the report could be sharpened (and links cleanly to #46517). Happy to share repro notes.Workaround for the regular-tools side, if useful for anyone: https://github.com/PettHa/claude-auto-plan-mode
Confirmed fixed on 2.1.140. Write auto-approved via hook with no native prompt, matching @PettHa's findings on 2.1.139.
The regression was between 2.1.59 and 2.1.119, now resolved. I'll test ExitPlanMode separately — if that's the only remaining case, the title should be narrowed to reflect that per @PettHa's suggestion.
Thanks everyone.
This issue was fixed as of version 2.1.140.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.