PermissionRequest hook is awaited before the local dialog for background subagents; the main session runs them in parallel

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Jul 29, 2026

Summary

For the main session, a PermissionRequest command hook runs concurrently with the local permission dialog — both surfaces are live and the first answer wins. For background subagents, the hook is awaited before the dialog is constructed, so a hook that holds (for example to route the approval to a phone or a chat channel) removes the local prompt for the entire hold.

Both halves of the subagent path work as documented: the hook fires and carries agent_id, and a returned hookSpecificOutput.decision of allow is honored. The only asymmetry is the ordering.

Environment

  • Claude Code 2.1.220, Linux x86_64, interactive TTY
  • PermissionRequest command hook registered from a plugin's hooks.json, matcher: "*", timeout: 86400
  • The hook forwards the event to a local daemon over a unix socket and does not write stdout until an answer arrives from elsewhere. "Holding" below means exactly that: the hook process is alive and has produced no output yet.
  • All timings are from daemon-side logs that record each hook dispatch and each Notification of type permission_prompt.

Measurement 1 — main session: dialog is present while the hook holds

  • The permission_prompt Notification fired 6.0s after the request in 5 of 5 holds (spread 5.992s–6.008s).
  • 4 requests were answered at the keyboard while the hook was still holding; the hook's later answer was discarded, which is the expected outcome of a first-answer-wins race.
  • Two holds shorter than 6s produced no Notification at all, which is consistent with that notification having its own delay rather than with the dialog being absent.

So for the main session both surfaces coexist, and either one can settle the request.

Measurement 2 — background subagent: no dialog until the hook answers

Same hook, same session, request originating from a backgrounded subagent:

T+0.000s    PermissionRequest hook invoked      (agent_id present, tool_name Read)
            ... hook holds; configured window = 120s
            zero permission_prompt Notifications for the entire hold
T+120.0s    hook returns {}                     (window expired, no decision)
T+126.079s  permission_prompt Notification      → the dialog is constructed only now

The +6.079s offset after the pass-through matches the notification delay measured above, so the dialog appeared essentially the moment the hook answered — not before.

Measurement 3 — the decision path itself is fine

Same setup, but answered remotely instead of timing out:

T+0.000s    PermissionRequest hook invoked      (agent_id a3a3…, tool_name Read)
T+26.000s   hook returns hookSpecificOutput.decision = { "behavior": "allow" }
T+26.080s   PostToolUse for the same agent_id + tool_name  → the tool executed
            zero permission_prompt Notifications at any point

So allow is honored for subagents, and it correctly suppresses the prompt. Nothing is broken here; the report is only about when the dialog is built.

For what it is worth, the behavior looks like it comes from a flag set on a spawned agent's permission context when that agent is async, which turns the hook into an await ahead of dialog construction. The main-session path instead starts the hook as a background task that races the dialog, with a shared claim latch deciding the winner.

Why it matters

Any integration that routes approvals to another surface has to choose, for subagents only, between being able to approve remotely and a local prompt existing at all:

  • Hold the request, and someone sitting at the keyboard cannot answer it — there is nothing on screen for the duration of the window.
  • Pass it through, and the request becomes unanswerable from anywhere except the keyboard, because the hook invocation is over.

The main session gets both surfaces for free. Since 2.1.198 subagents run in the background by default, so this applies to most subagent tool calls.

Request

Run the PermissionRequest hook concurrently with dialog construction for these agents too, with first-answer-wins — the same arrangement the main-session path already uses.

Related

  • #23983 — hook never fires for Agent Teams teammates. A different failure: absence, not ordering.
  • #79177 — reports (its Mode A) that a subagent's returned allow was ignored. Not reproducible here on 2.1.220 when the decision uses the documented hookSpecificOutput envelope; see Measurement 3.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗