Skip to content

Expose a privacy-minimal output-only human-input lifecycle from codex-core #39426

Open

Description

@gennadi-kuzmin

What variant of Codex are you using?

Codex App, IDE Extension, and CLI.

What feature would you like to see?

Please expose an optional, output-only lifecycle for the moments when Codex is
actually waiting for a human permission decision or supported user input.

The lifecycle should be produced once in codex-core, so App, IDE Extension,
and CLI clients share the same semantics. It should emit only:

  • permission pending and a matching resolved;
  • question pending and a matching resolved.

Pending should be emitted only after policy, cached approvals, hooks,
automated review, and other automatic routing have finished and the request
will actually be shown to a human.

Resolved should be emitted exactly once, without the decision or answer,
when the owning client receives a response or the request is cleared by
decline, cancellation, replacement, interruption, turn completion, abort, or
session shutdown.

This should cover permission requests as well as blocking
request_user_input and MCP elicitation questions.

The observer must remain strictly notification-only. It must have no protocol
or code path to approve, reject, select an option, or submit text. The user
continues to respond only in the Codex client that owns the request.

Privacy and authority boundary

The output should contain only a generic kind and an internally generated
opaque request ID. It should never contain request content, filesystem paths,
approval reasons, question text, options, tool input, model output, user
answers, or approval outcomes. Lifecycle data should not be written to
telemetry or long-lived logs.

An optional local transport should be output-only from Codex. Observer input
must never be read and must not be able to influence Codex behavior.

Deterministic cleanup

The lifecycle needs bounded source identity, monotonic sequencing, a heartbeat,
and an authoritative reconnect snapshot containing only requests that are
still active.

EOF, an unknown version or event, a sequence gap, a collision, process loss,
or restart should clear pending state by default. Reconnect must not replay
historical notifications. A still-active request should retain its opaque
request ID across a fresh transport source so a receiver can restore visual
state without replaying sound for the same unresolved request.

Why existing surfaces are not sufficient

App Server exposes typed requests and matching resolution on the owning client
transport, but a separate read-only observer cannot attach to every existing
App, IDE, and CLI task.

PermissionRequest hooks run before the final human-routing decision, so a
hook invocation does not prove that Codex is waiting for a person. They also
do not provide the matching generic resolution needed to remove a notification
deterministically.

A one-shot notification event cannot reliably clear stale pending state when
the Codex process or observer disappears.

Additional information

This is related to #28833, which requests a post-routing approval signal for
passive notifications. This request additionally covers paired no-outcome
resolution, supported blocking questions, shared core ownership, reconnect,
and deterministic source-loss cleanup.

A software-only reference prototype has validated the feasibility of the
core-owned lifecycle and a strict downstream receiver, including permission
and question round trips, privacy boundaries, duplicate and replay handling,
reconnect, restart, observer loss, collision handling, and stale cleanup.
Technical design and test details can be provided privately if useful.

The feature can be disabled by default and should not change existing approval
flows or require UI scripting, keyboard automation, or accessibility
automation.

Activity

  1. added
    enhancementNew feature or request
    mcpIssues related to the use of model context protocol (MCP) servers
    sandboxIssues related to permissions or sandboxing
    on Aug 19, 2026
  2. owieschon commented on Sep 6, 2026

    @owieschon

    Reproduced a downstream consequence of this gap: Superset reports a Codex terminal as working while its native approval dialog is waiting for a response.

    The live control used Superset 1.26.0 and the app-bundled Codex 0.153.4 executable on macOS arm64. Codex ran through Superset's existing wrapper with --ask-for-approval on-request --sandbox read-only. Project-local command hooks recorded lifecycle events and returned {} without deciding permission or updating Superset state.

    The reproduction was a single harmless command:

    Call exec_command with sandbox_permissions=require_escalated to execute:
    python3 -c "print(1234567)"
    Wait for the permission decision.
    

    The native dialog was visibly pending when the host state was sampled. Accepting once executed the command and completed the same conversation. These are observations from one trace; the prolonged wait included an operator interruption and is not a latency benchmark.

    Observation UTC Result
    Native PermissionRequest 18:54:29.662 Approval dialog presented
    Host state sampled while dialog remained pending 19:03:54.862 Start
    TUI submitted ExecApproval 19:04:44.969 Accepted once
    Native PostToolUse 19:04:55.021 Command output 1234567
    Native Stop 19:05:14.230 Same conversation; host reached Stop

    The process-scoped TUI log recorded the acceptance with an execution ID, but contained no corresponding approval-prompt record. The live run used the app-bundled binary; a separate npm installation reporting 0.153.4 was version-checked, not exercised in this trace.

    Registering PermissionRequest and clearing it with PostToolUse would not preserve the consumer's semantics. The request hook runs before automatic or user resolution. The post-tool hook observes successful, completed execution. It cannot distinguish actual human attention or clear immediately on every resolution path. These additional paths were source-reviewed, not live-tested here.

    Current main already has the required semantics in app-server. At ad931a45, approval requests follow automatic routing; the existing serverRequest/resolved notification provides matching resolution. The TUI receives these through handle_app_server_event, which bypasses the opt-in session recorder.

    Would maintainers support a fixed, redacted projection of that existing request/resolved pair through CODEX_TUI_RECORD_SESSION, or is another terminal-observer surface preferred? The projection could retain method and thread/request correlation while excluding request content and response decisions. Approval authority would stay with the owning client.

    Focused acceptance cases for that exposure:

    • Each actual approval request and its resolution retain matching identity.
    • Automatic decisions create no false user-attention event.
    • Accept, decline, cancellation, and teardown preserve the existing resolution boundary; accepted work clears before tool completion.
    • Concurrent requests retain distinct identities.
    • Redaction excludes command, directory, reason, input, answer, decision, and error payloads.
    • Disabled or failed recording leaves native approval and execution behavior unchanged.

    The existing session-log tests provide a narrow regression surface. This builds on the app-server lifecycle and relates to #33020 without introducing another approval protocol.

    The control's final response was 1234567, rather than the requested completion marker. Command execution and lifecycle completion are verified; exact-marker compliance is not. Diagnostic excerpts have cost metadata and unrelated account/configuration information removed.

  3. hippoley commented on Sep 28, 2026

    @hippoley

    The output-only observer boundary in this proposal looks right to me. One thing I would preserve in the lifecycle shape: "resolved" should not collapse human completion and machine cleanup into the same semantic outcome.

    I hit the same ambiguity while building a small external human-boundary control plane. The failure mode was subtle:

    pending human question
    → timeout / cancellation / policy action
    → generic "resolved"
    → downstream observer concludes "human handled it"
    

    That is enough to create false audit state even if the observer never receives the answer itself.

    A privacy-minimal lifecycle can still distinguish provenance without exposing the decision:

    request/pending
      id=opaque-123
      kind=permission
    
    request/cleared
      id=opaque-123
      cleared_by=human | system | policy | client
      reason=answered | declined | expired | cancelled | replaced | teardown
    

    No command/question text, answer, or approval outcome is required.

    In the prototype I ended up enforcing the same rule at the canonical state layer:

    human decision   -> resolved
    system timeout   -> expired
    runtime cancel   -> cancelled
    policy action    -> policy/system provenance
    

    and a human-only boundary rejects non-human provenance on the normal resolution path.

    Implementation + regression tests, if a concrete shape is useful:
    hippoley/HumanQueue#17

    Separately, for native-host evidence I also found that PermissionRequest alone is not proof of human attention and PostToolUse is too late to represent resolution. That matches the distinction already made in the comment above: the best source is the core-owned request/resolved lifecycle itself.

    So I think the minimal observer contract is strongest if it exposes request identity + lifecycle provenance, while remaining completely output-only and content-free.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmcpIssues related to the use of model context protocol (MCP) serverssandboxIssues related to permissions or sandboxing

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions