PreToolUse hook allow decision not reliably honored under concurrent Bash tool call dispatch (race condition)

Open 💬 0 comments Opened Jul 13, 2026 by jahammo2

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

  1. Launch 4 subagents (Agent/Task tool, general-purpose type, foreground, i.e. not run_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
``

  1. All 4 commands are structurally identical: same command shape, same approved host, same hook.
  2. 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 correct allow decision every time, in isolation.
  • Ruled out stale config: the hook had been wired into settings.json for 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_background as the variable: neither foreground nor background single-subagent dispatch reproduced the issue.
  • Reproduced only when 4 Agent tool calls were sent in one message (true concurrent foreground dispatch). Temporarily instrumented the hook script with a side-effect-only debug log (append tool_name/command to 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 their allow decision 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.

View original on GitHub ↗