[BUG] PermissionRequest Hook Race Condition - Dialog Shows Despite Hook Returning "allow"
[BUG] PermissionRequest Hook Race Condition - Dialog Shows Despite Hook Returning "allow"
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (not multiple bugs)
- [x] I am using the latest version of Claude Code
Summary
PermissionRequest hooks execute asynchronously in parallel with permission dialogs being displayed. When a hook returns {"behavior": "allow"}, the permission dialog is still shown to the user if the hook takes longer than ~1-2 seconds to complete. This creates a race condition where the hook's decision is ignored.
What's Wrong?
The permission system adds dialogs to UI state before awaiting hook results, creating a race between:
- Hook execution completing and removing the dialog from state
- UI rendering the dialog to the user
Result: If the hook is "slow" (>1-2 seconds), the user sees the permission dialog even though the hook already approved the action.
Evidence from Debug Logs
First Command: aws route53 list-hosted-zones (Hook won, no dialog shown)
2025-11-23T04:41:10.042Z [DEBUG] Hooks: Processing prompt hook...
2025-11-23T04:41:11.744Z [DEBUG] Hooks: Model response: {
"ok": true,
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow"
}
}
}
2025-11-23T04:41:11.745Z [DEBUG] Hooks: Prompt hook condition was met
2025-11-23T04:41:12.217Z [DEBUG] No hook environment files found
✅ Hook completed in 1.7s → Dialog never shown → Command executed
Second Command: aws route53 list-hosted-zones --profile test (Hook lost, dialog shown)
2025-11-23T04:41:32.382Z [DEBUG] Hooks: Processing prompt hook...
2025-11-23T04:41:34.281Z [DEBUG] Hooks: Model response: {
"ok": true,
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow"
}
}
}
2025-11-23T04:41:34.281Z [DEBUG] Hooks: Prompt hook condition was met
2025-11-23T04:41:38.380Z [DEBUG] Getting matching hook commands for Notification with query: permission_prompt
⚠️ Hook completed in 1.9s → Dialog still shown at 04:41:38.380Z (4 seconds after hook approved!) → User sees dialog despite hook approval
Root Cause Analysis
Found in cli.js around lines 2689-2723 (minified code):
case "ask": {
let H = !1;
// 1. Dialog IMMEDIATELY added to UI state
A((U) => [...U, {
assistantMessage: I,
tool: B,
description: C,
input: G,
toolUseContext: Z,
toolUseID: Y,
permissionResult: K,
// ... handlers...
}]); // <-- Dialog created HERE
let E = await Z.getAppState();
// 2. Hook executes ASYNCHRONOUSLY in parallel
(async () => {
for await (let U of HIA([NZ0(B.name, Y, G, Z, E.toolPermissionContext.mode, Z.abortController.signal)])) {
if (H) return;
if (U.permissionRequestResult && U.permissionRequestResult.behavior === "allow") {
H = !0;
// 3. Hook "allow" tries to REMOVE dialog from state
A((q) => q.filter((w) => w.toolUseID !== Y));
W({behavior: "allow", updatedInput: q, userModified: !1,
decisionReason: {type: "hook", hookName: "PermissionRequest"}});
return;
}
}
})();
return; // <-- Returns immediately without waiting!
}
The Problem:
- Line 2689: Dialog pushed to UI state array
- Line 2693: Async IIFE starts hook execution
- Line 2723: Function returns immediately without awaiting
- Line 2699: Hook eventually completes and tries to filter dialog from state
- BUT: If UI rendered the dialog already, user sees it
Expected Behavior
When a PermissionRequest hook returns {"behavior": "allow"}, the permission dialog should never be shown to the user, regardless of hook execution time.
Actual Behavior
- If hook completes in <1.5s: Dialog usually not shown (race won)
- If hook completes in >2s: Dialog shown despite approval (race lost)
- Behavior is non-deterministic and depends on system performance
Steps to Reproduce
- Configure a PermissionRequest hook in
~/.claude/settings.json:
``json``
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate if Claude should be allowed to execute the requested Bash command in $ARGUMENTS. Auto-approve ONLY safe read-only operations including AWS CLI read commands (list-, describe-, get-, show). If safe to auto-approve, return: {\"ok\": true, \"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}. Otherwise return {\"ok\": true} to show user prompt. NEVER auto-deny."
}
]
}
]
}
}
- Run with debug logging:
claude --debug
- Execute a command not in auto-approved list (e.g.,
aws route53 list-hosted-zones)
- Repeat the same command multiple times
- Observe: Sometimes dialog shows, sometimes it doesn't, even though hook always returns "allow"
- Check debug logs:
grep -A 5 "Hooks: Model response" ~/.claude/debug/latest
- Notice: Hook always approves, but dialog appearance is inconsistent
Related Issues
- #9575: Notification hook only fires ~25% for permission prompts (related symptom)
- This is the root cause explanation for why Notification hooks are unreliable
Impact
- Hook reliability: PermissionRequest hooks cannot be trusted for automated approvals
- User experience: Inconsistent behavior confuses users
- Automation broken: CI/CD workflows with hooks fail unpredictably
- Workaround required: Must add everything to
permissions.allowrules instead of using hooks
Proposed Fix
Before adding dialog to state, await the hook result:
case "ask": {
let E = await Z.getAppState();
// Execute hook FIRST
for await (let U of HIA([NZ0(B.name, Y, G, Z, E.toolPermissionContext.mode, Z.abortController.signal)])) {
if (U.permissionRequestResult && U.permissionRequestResult.behavior === "allow") {
// Hook approved - execute without dialog
W({behavior: "allow", updatedInput: U.permissionRequestResult.updatedInput || G, userModified: !1,
decisionReason: {type: "hook", hookName: "PermissionRequest"}});
return;
}
}
// Hook didn't approve - NOW show dialog
A((U) => [...U, {
assistantMessage: I,
tool: B,
// ... rest of dialog config
}]);
return;
}
Environment
- Claude Code Version: 2.0.50
- Platform: Anthropic API
- Operating System: macOS (Darwin 24.3.0)
- Model: claude-sonnet-4-5-20250929
Additional Information
Hook Configuration:
- Hooks in
~/.claude/settings.json - Using prompt-based PermissionRequest hook
- Hook executes successfully (status logged in debug)
- Hook always returns correct decision
Why This Matters:
The PermissionRequest hook is designed for security automation (e.g., auto-approving safe commands, blocking dangerous ones). The race condition makes it unreliable for this purpose, forcing users to either:
- Manually approve everything (defeats automation)
- Add broad wildcard rules (reduces security)
Neither is acceptable for enterprise/CI environments.
4 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Additional Evidence: Workaround Confirms Root Cause
What We Did
Replaced the slow LLM-based prompt hook with a fast bash script (~10ms execution):
Bash Script Hook:
Hook Configuration:
Result
✅ Works perfectly - auto-approves
aws route53 list-hosted-zonesinstantly with NO dialog shownWhat This Proves
Why This Matters
The bash script workaround only works for simple pattern matching. The whole point of prompt-based PermissionRequest hooks is to use LLM reasoning for complex security decisions. The race condition makes LLM-based hooks unreliable, forcing users to choose between:
None of these are acceptable for production security automation.
Test Results
Bash hook (instant execution):
LLM prompt hook (1-2s execution):
This definitively proves the race condition exists and is timing-dependent.
If you want to make a blocking permission decision, you should use the
PreToolUsehook instead.We asynchronously execute the
PermissionRequesthook and display the UI to the user while the hook runs. This allows, for example, the hook to send a Slack notification and wait for the user to approve the permission via either the CLI or a Slack response.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.