Hooks: let PermissionRequest answer concurrently with the native approval prompt (first answer wins)

Open 💬 0 comments Opened Aug 19, 2026 by y49

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:485timeout_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:

  1. 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.

  1. 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.

View original on GitHub ↗