PreToolUse hook allow decision not reliably honored under concurrent Bash tool call dispatch (race condition)
Summary
A PreToolUse hook (matcher: Bash) that returns hookSpecificOutput.permissionDecision: "allow" is not reliably honored when several Bash tool calls are dispatched concurrently (e.g. via parallel subagents launched in the same message). Under concurrent dispatch, most calls correctly receive the hook's allow decision and skip the permission prompt, but occasionally one call — with an identical command shape to the others — falls through to the manual confirmation prompt instead.
Environment
- Claude Code CLI
- macOS (Darwin 25.5.0)
Hook config
~/.claude/settings.json:
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "python3 ~/.claude/hooks/auto-approve-read-only-gets.py" }
]
}
]
}
The hook parses tool_input.command and, for a curl GET to an approved host piped only through read-only utilities (jq/head/etc., no body flags, no writes outside /tmp), prints:
{"decision":"approve","hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"..."}}
Repro
- Launch 4 subagents (Agent/Task tool,
general-purposetype, foreground, i.e. notrun_in_background) in a single message so they run concurrently. Each subagent runs exactly one Bash command, identical in shape apart from a unique query-string marker:
````
curl -s "https://api.planningcenteronline.com/calendar/v2/documentation?hookdispatchtest=<unique-marker-A|B|C|D>" | head -c 200
- All 4 commands are structurally identical: same command shape, same approved host, same hook.
- Observed: 3 of 4 subagents' Bash calls were silently approved by the hook (command executed immediately, no prompt, real API output returned). The 4th hit the standard interactive "Permission rule
Bash(curl *)requires confirmation for this command" prompt, requiring the user to manually click through.
Isolation performed before filing
- Confirmed the hook script's logic is correct: manually piping the exact blocked command's JSON payload to the hook (
echo '...' | python3 hook.py) returns the correctallowdecision every time, in isolation. - Ruled out stale config: the hook had been wired into
settings.jsonfor 5+ days before the session where this was observed; the session itself started well after the config was last touched. - Ruled out "subagents never consult hooks": a single subagent (foreground) and a single subagent (background) both had the identical hook-approved command succeed silently with no prompt — so subagents can have hook decisions honored.
- Ruled out
run_in_backgroundas the variable: neither foreground nor background single-subagent dispatch reproduced the issue. - Reproduced only when 4
Agenttool calls were sent in one message (true concurrent foreground dispatch). Temporarily instrumented the hook script with a side-effect-only debug log (appendtool_name/commandto a log file, no change to decision logic) and confirmed via the log that the hook process was invoked for every one of the 4 concurrent calls — but only some of those invocations had theirallowdecision actually honored by the permission engine. The debug instrumentation was reverted immediately after (confirmed byte-identical to the pre-instrumentation backup).
Expected
A PreToolUse hook's allow decision should be reliably honored for every tool call it's invoked for, regardless of how many other tool calls (from concurrently-running subagents) are in flight at the same time.
Actual
Under concurrent dispatch of ~4 simultaneous Bash tool calls from parallel subagents, one call's hook-approved decision is occasionally dropped, and the user is shown a manual confirmation prompt instead — despite the hook executing correctly and returning the correct decision for that exact call.
Impact
Any workflow that fans out to several subagents which each independently make hook-approved Bash calls (e.g. a skill that spawns N parallel review/research subagents, each of which may curl a documentation API) can hit an unexpected, seemingly random permission prompt for a command shape that's supposed to be auto-approved — breaking the "run N subagents unattended" expectation those hooks exist to support.