Hooks: let PermissionRequest answer concurrently with the native approval prompt (first answer wins)
What variant of Codex are you using?
Codex CLI (codex-cli 0.147.0) with codex app-server.
What feature would you like to see?
A way for a PermissionRequest hook to answer alongside the native approval prompt rather than before it — that is, a hook can say "show the native prompt now; I may still answer; whichever answer arrives first wins."
Today the event is serial by construction:
//! This event runs in the approval path, before guardian or user approval UI is
//! shown.
// codex-rs/hooks/src/events/permission_request.rs
run_permission_request_hooks(...) is awaited before the approval flow continues (core/src/tools/orchestrator.rs:434), and a handler's timeout has a floor but no ceiling (hooks/src/engine/discovery.rs:485 — timeout_sec.unwrap_or(600).max(1)). So a hook that waits on a human somewhere else holds the entire approval: the person sitting at the terminal sees a pending hook row and has nothing they can answer until the hook returns.
That leaves an external approval tool with only bad options:
- short hook timeout — the remote answerer usually loses, so the feature barely works;
- long hook timeout — the local user is locked out of their own approval for as long as the hook waits.
Either shape would solve it:
- A hook output that keeps the request open:
{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "defer_and_watch"}}}
Codex shows the native prompt immediately, the handler stays alive, and the first decision to arrive — hook or human — wins.
- Or a declaration flag in
hooks.json, e.g."concurrent": true, with the same semantics.
The half that matters most either way is cancellation: when the local user answers first, the handler needs to learn that (a signal, or a line on stdin) so an external approval card can be withdrawn instead of dangling until it times out.
Why this matters
The app-server already has exactly this property, which is why external tooling gravitates there: a pending approval ServerRequest reaches subscribed connections and is replayed to new ones (app-server/src/outgoing_message.rs:352, replay_requests_to_connection_for_thread), so another client can answer while the TUI shows the same prompt, first answer wins.
But the app-server is an internal, unversioned surface, and it carries only what it carries. Hooks are the declarative, documented, cross-tool surface — and PermissionRequest there already covers strictly more in a single event: apply_patch, shell, unified_exec, sandbox escalation (core/src/tools/runtimes/shell/unix_escalation.rs), MCP tool calls (core/src/mcp_tool_call.rs), and network policy (core/src/tools/network_approval.rs).
For a tool that puts approvals on a phone while the terminal stays usable, the only reason not to build on hooks today is that they cannot be concurrent.
Related, but different
- #23465 — expose the effective approval reviewer, or an explicit defer, so an external UI does not steal approvals Codex would have handled itself.
- #28833 — tell a passive notifier whether a human is actually required.
Both concern the routing and observability of a request that is still answered serially. This issue is about the concurrency of the answer: even when the reviewer really is user and a human really is required, a hook can only answer instead of the terminal, never with it.