Skip to content

bug(codex): trust dialog clicks are refused invisibly — keystroke never reaches the agent #711

Open

Description

@Juliusolsson05

Expected behavior

Clicking "trust directory" on the Codex trust modal writes the accept keystroke to the Codex PTY, the dialog resolves, and trust persists. If the write cannot be delivered, the user sees why.

Actual behavior

Clicking (even spamming) the button does nothing visible. The dialog stays up, the pane never becomes usable, and because the modal is application-global (#713) the whole app is unusable while it shows.

Root cause (two stacked defects)

  1. The refusal is invisible. sendConditionKey (src/renderer/src/workspace/tile-tree/TileLeaf.tsx:399-418) correctly surfaces failures — !runtime.conditions → "That prompt is no longer live.", sendInput === false → "That keystroke did not reach the agent…" — but as pane toasts, which render underneath the Radix app-modal overlay the trust dialog itself creates. Every refusal is therefore silent in exactly the situation where it matters most.
  2. The write is refused. SessionManager.write() (src/main/sessionManager.ts:3293+) returns false before recordInputWrite when no live registry entry exists or a delivery holds the reservation. In the recorded incident (app run 2026-08-31T00-48-19-592Z-main-63968-59a3af, codex session 9d9fd575… in bringdown), the user's clicks produced zero input.write journal rows — the writes were refused at main (or fenced at !runtime.conditions on a stale frozen snapshot; TileLeaf's own quarantine comment documents that a fenced pane's "last condition snapshot stays frozen on screen while main's entry under that id may belong to someone else"). The session was meanwhile cycling through the bug(session): wake readiness timeout kills a live codex blocked on its trust dialog — 30s kill loop #712 kill loop, so for most of the spamming window there was no live backend to write to.

Note the keystroke contract itself is NOT the bug this time: vendored codex source (vendor/codex-src/codex-rs/tui/src/onboarding/trust_directory.rs:122) still maps Char('1') | Char('y') → trust, matching CODEX_TRUST_DIALOG_ACCEPT_KEYS = '1' (codex-cli 0.151.0 in use).

Impact

An untrusted folder makes a Codex pane — and with #712, the whole app — unusable, with zero feedback. Same severity class as the Claude trust regression (#705), different mechanism.

Fix direction

Environment

agent-code build 3378d171 (dirty), codex-cli 0.151.0, macOS. Related: #705 (Claude sibling), #712 (kill loop), #713 (app-global modal).

Activity

  1. Juliusolsson05 commented on Sep 20, 2026

    @Juliusolsson05
    OwnerAuthor

    Audit note (2026-09-20): the trigger is largely gone; both stated fix directions are still open, and one of them is now inconsistent with code that shipped today.

    Shipped. ed9e852f (Fixes #714) calls ensureCodexProjectTrust(this.cwd) early in CodexSession.start (codexSession.ts:317-330), pre-writing trust_level = "trusted" before the PTY spawns, so app-spawned Codex sessions in untrusted folders never paint the dialog. Ordering pinned by codexSession.launchOrdering.test.ts:186. The "spamming window" half is gone too — see #712, now closed.

    Not shipped, item 1 — the refusal is still invisible above a modal. sendConditionKey (TileLeaf.tsx:413-432) still reports both refusals through workspace.showPaneToast, which renders as PaneToast — an inline flex child of the pane, with no z-index, sitting under the Radix overlay at z-[1100] (dialog.tsx:29) and inside the subtree Radix marks aria-hidden.

    This is now inconsistent within the same feature: #1070 (merged today as #1099) moved condition refusal messages onto GlobalToast at z-[1200] for exactly this reason, and GlobalToast's own header records the trap. The pty arm that sendConditionKey owns did not move. So a refused structured answer is now visible and a refused keystroke answer is still not — and the keystroke arm is the one a trust dialog uses.

    Not shipped, item 2 — a click on a session with no live backend is still a silent no-op. sendConditionKey does not call ensureSessionLive, and SessionManager.write() (sessionManager.ts:3514-3575) returns false before recordInputWrite when a delivery holds the reservation or there is no registry entry. The frozen-snapshot half (clear or refresh when main has no entry) is untouched; the quarantine reasoning is still in the comment at TileLeaf.tsx:400-412.

    Item 1 is now a small, well-defined change: route sendConditionKey's two refusals through the same global toast the refusal reporter uses.

  2. Juliusolsson05 commented on Sep 25, 2026

    @Juliusolsson05
    OwnerAuthor

    Reopened: #1110 closed this through its "Closes #711 (item 1)" trailer, but only item 1 shipped.

    • Item 1 — done in fix(conditions): put a refused keystroke where a modal cannot cover it #1110. Both keystroke refusals now go to the global toast above the modal, at the same 6 s as the structured refusal reporter.
    • Item 2 — still open. A condition click on a session with no live backend is still a silent no-op: sendConditionKey doesn't call ensureSessionLive, and SessionManager.write() returns false before recordInputWrite when there is no registry entry. The user now at least sees the refusal (item 1), but the pane doesn't wake. The frozen-snapshot half (clear or refresh the condition when main has no entry) is also untouched.
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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions