Repository navigation
PermissionRequest hook is awaited before the local dialog for background subagents; the main session runs them in parallel #82150
Description
Activity
Still reproduces on 2.1.235. Since this has sat for three weeks, here is the path in the shipped bundle so the behaviour can be confirmed without a repro. Identifiers are minified and will change between builds; the shape is the point.
The flag. A backgrounded/async agent's permission context is built with
awaitAutomatedChecksBeforeDialog: true:let Ut = i !== void 0 ? !i : Ke === "bubble" || Qe ? !1 : o; if (Ut) vr = { ...vr, shouldAvoidPermissionPrompts: !0 }; if (o && !Ut) vr = { ...vr, awaitAutomatedChecksBeforeDialog: !0 };
The gate. In the
askbranch of the permission coordinator, that flag makes the automated checks a precondition of the dialog rather than a racer (reflowed for readability):case "ask": { if (CIc.awaitAutomatedChecksBeforeDialog) { let T_A = await BWn({ ctx: fFe, updatedInput: t6.updatedInput, suggestions: t6.suggestions, permissionMode: CIc.mode }); if (T_A) { bst(T_A); return } // decided → the dialog is never constructed } if (fFe.resolveIfAborted(bst)) return; ... return Iqn({ ctx: fFe, description: nwg, result: t6, awaitAutomatedChecksBeforeDialog: CIc.awaitAutomatedChecksBeforeDialog, bridgeCallbacks: A_A.replBridgePermissionCallbacks, channelCallbacks: A_A.channelPermissionCallbacks }, bst) }
What is being awaited.
BWnis the hook run and nothing else:async function BWn(e) { let { ctx: t, updatedInput: r, suggestions: n, permissionMode: o } = e; let s = await t.runHooks(o, n, r); if (s && !("reprompted" in s)) return s; return null }
So for a backgrounded subagent the
PermissionRequesthook runs to completion first, and the dialog is built only if the hook declines to decide. On the main session the flag is falsy, control goes straight toIqn(...), and the dialog,bridgeCallbacksandchannelCallbacksall resolve through oneclaim()— first answer wins.Why this is worth changing rather than documenting. A hook that asks a human somewhere else (a phone, a chat, another machine) can only be given a long timeout if it accepts removing the terminal as an answering surface for that entire window. On the main session that trade-off does not exist: the hook may hold as long as it likes and the person at the keyboard is never locked out, because whoever answers first wins. External approval tools want exactly the main-session semantics. Today, for backgrounded subagents, they must either keep the window short — and usually lose the race, making the feature pointless — or accept that a subagent's prompt cannot be answered at the keyboard while the hook waits.
Either shape would resolve it:
- run the hook concurrently with the dialog for backgrounded subagents, the way the main session already does; or
- keep the current ordering but add an explicit "not deciding now, show the dialog, I may still answer" hook result, with the loser notified that it lost so it can withdraw its own UI.
Family context. #82418 was updated on 2026-08-18 with a re-verification on 2.1.234. Taken together, the same hook-originated
asknow behaves three different ways depending only on how the agent was spawned:spawn kind PermissionRequestdispatched?local dialog while the hook holds? in-process subagent (#79177) yes yes — reported working on 2.1.220 backgrounded / async subagent (this issue) yes no — awaited first agent-teams teammate (#82418) never forwarded to the team lead's TUI; observable through the lead's worker_permission_promptNotification, but not answerable by any hookThree spawn kinds, three answers, one
ask. Even without a fix here, knowing which of the three is the intended model would tell integrators what to build against.Would be really nice if this is fixed. This hampers the workflow when you want the permission decision to come from either the hook or from the claude-code ui manually.
Summary
For the main session, a
PermissionRequestcommand 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 returnedhookSpecificOutput.decisionofallowis honored. The only asymmetry is the ordering.Environment
PermissionRequestcommand hook registered from a plugin'shooks.json,matcher: "*",timeout: 86400Notificationof typepermission_prompt.Measurement 1 — main session: dialog is present while the hook holds
permission_promptNotification fired 6.0s after the request in 5 of 5 holds (spread 5.992s–6.008s).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:
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:
So
allowis 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
awaitahead 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:
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
PermissionRequesthook concurrently with dialog construction for these agents too, with first-answer-wins — the same arrangement the main-session path already uses.Related
allowwas ignored. Not reproducible here on 2.1.220 when the decision uses the documentedhookSpecificOutputenvelope; see Measurement 3.