Interactive CLI/TUI: opt-in local ingress for trusted local controllers (avoid PTY input emulation)

Open 💬 11 comments Opened Mar 20, 2026 by sidkandan
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What feature would you like to see?

Expose an opt-in local ingress for interactive Codex CLI/TUI sessions, so another trusted local process can safely deliver input to the active session using real session semantics rather than PTY/keyboard emulation.

This is intentionally narrower than broad “remote control” and broader than “support one MCP notification”. The need is:

I have an already-running interactive Codex CLI/TUI session on my machine. I want a trusted local controller to hand it structured work or a follow-up message without pretending to be a keyboard.

Why this matters

Today, local orchestration around a running Codex TUI session often falls back to terminal injection:

  • send-keys
  • paste-buffer
  • pipe-pane -I
  • direct PTY writes

These all have the same class of problems:

  • text can land in the draft without actually submitting
  • injected text can race with a human actively typing
  • attribution, dedupe, and replay are ad hoc
  • wrappers are forced to emulate terminal input instead of using real session/thread semantics

In practice this makes local automation brittle. It also discourages safer local integrations because the only widely available option is “pretend to be the terminal”.

This matters for:

  • local orchestrators coordinating multiple Codex/CLI workers
  • tmux- or terminal-based multi-agent workflows
  • editor/terminal wrappers
  • accessibility tooling
  • local remote-control shells
  • background workers that need to hand work back to a human-visible Codex session

Why existing features are close, but not enough

Codex already has the right conceptual model in the programmable surfaces:

  • thread/start
  • thread/resume
  • thread/fork
  • turn/start
  • turn/steer
  • turn/interrupt

Those are exactly the kinds of semantics local controllers want.

The gap is that there does not appear to be a supported way to apply equivalent semantics to an already-running interactive Codex CLI/TUI session on the same machine.

Current adjacent features do not fully solve this:

  • App Server / SDK: strong control model, but they do not obviously provide a documented “attach this existing interactive TUI session as a local controllable endpoint” workflow.
  • MCP: great tool plane, but not a documented receive-side transport into the active TUI thread.
  • Event hooks / notify: useful for observation and orchestration, but outbound-only.
  • Remote control / mobile proposals: related, but they target remote supervision/control rather than the local trusted-controller case.

What I expected

At least one supported local-first path such as:

  1. An opt-in local IPC listener for interactive sessions
  • for example: codex --listen uds:///path/to/session.sock
  • local-only by default
  • receives real turn/draft/steer operations instead of raw keystrokes
  1. A local CLI control command for live interactive sessions
  • for example: codex tui send --session <id> --text "..." [--submit]
  • optionally --append-draft, --steer, or --interrupt
  1. A documented way to bind an interactive TUI session to App Server semantics
  • so a local controller can attach to the same session/thread and send real turns

What I observed

For the stock interactive CLI/TUI, the practical fallback today is still terminal input emulation.

That leads to concrete failure modes:

  • draft text inserted but not submitted
  • active human typing gets mixed with injected content
  • no built-in sender identity or audit trail at the ingress point
  • orchestrators need terminal-state heuristics instead of stable session APIs

Why this is worth solving

This would unlock a cleaner class of local integrations without forcing people into cloud- or network-oriented remote-control flows.

Benefits:

  • safer local orchestration for long-running interactive sessions
  • less pressure to build wrappers around PTY injection
  • easier composition with local tools and terminal multiplexers
  • better accessibility and automation ergonomics
  • stronger alignment between the interactive TUI and the documented thread/turn model already present in App Server / SDK

Suggested direction

My strong preference would be:

  • keep it opt-in
  • keep it local-only by default
  • reuse existing thread/turn semantics where possible
  • avoid introducing a second unrelated message model just for the TUI

If full ingress is too large, a useful partial step would be richer documented local event coverage for orchestration wrappers, for example:

  • turn-started
  • turn-interrupted
  • approval-requested
  • approval-resolved
  • idle
  • draft-presence-changed

That would at least let local controllers make safer delivery decisions without scraping terminal output.

Related issues

This feels related to, but distinct from:

  • #15299 inbound MCP notifications into an active CLI session
  • #14722 sync CLI and app-server sessions
  • #15320 app-server TUI not fully reflecting external live turns on shared thread
  • #14693 no-interrupt typing / queued-draft safety while Codex is running
  • #9224 remote control from phone/web
  • #2109 event hooks

The difference here is the focus on trusted local ingress for an already-running interactive session, without relying on PTY injection and without requiring a separate remote/mobile product surface.

Additional information

If useful, I can provide a more explicit repro matrix showing:

  • PTY injection behavior today
  • the specific failure modes in interactive terminal workflows
  • the minimal event/ingress API surface needed by a local orchestrator

View original on GitHub ↗

11 Comments

sidkandan · 4 months ago

Small concrete repro matrix to make the integration gap more testable.

What local wrappers/orchestrators do today

When they need to hand work to an already-running interactive Codex CLI/TUI session, the practical options are usually:

  • send-keys
  • paste-buffer
  • pipe-pane -I
  • direct PTY writes

Those differ in mechanics, but from the session’s perspective they are all “pretend to be terminal input”.

Failure modes with current PTY-style approaches

1. send-keys

Observed/expected class of problems:

  • text may be inserted character-by-character into the draft/composer
  • submission depends on UI state and exact key sequence
  • races badly with a human actively typing
  • poor fit for structured or multi-line payloads

2. paste-buffer

Observed/expected class of problems:

  • safer than character-by-character injection, but still terminal emulation
  • pasted text can land in the draft without becoming a real submitted turn
  • still races with local editing / focus state
  • no built-in sender identity or session-level audit semantics

3. pipe-pane -I

Observed/expected class of problems:

  • writes to the pane “as if typed”, so it is still not a real session API
  • bypasses some wrapper assumptions because it is lower-level than explicit key macros
  • still depends on current TUI/input state
  • still has draft-collision / not-submitted ambiguity

Desired behavior instead

What local orchestrators actually need is a supported ingress with real session semantics, for example:

  • append draft without pretending to type
  • start turn with explicit submission
  • steer active turn as a first-class operation
  • interrupt active turn as a first-class operation

And ideally:

  • local-only / opt-in
  • explicit session identity
  • clear sender attribution or source metadata
  • deterministic success/failure semantics
  • no dependence on terminal focus, cursor position, or whether a human is mid-typing

That is why this request is less about any one transport and more about exposing a proper local control surface for interactive sessions.

github-actions[bot] contributor · 4 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #15299

Powered by Codex Action

sidkandan · 4 months ago

Thanks — I reviewed #15299.

I think it is related, but not a true duplicate.

My read of the distinction:

  • #15299 is asking for a specific inbound MCP-notification path into an active Codex CLI session.
  • #15355 is asking for a broader opt-in local ingress for an already-running interactive CLI/TUI session, ideally reusing thread/turn semantics and not depending on any one transport.

In other words, #15299 is one possible solution shape under the larger problem, but not the whole problem.

Some examples that would satisfy #15355 without necessarily solving it via MCP notifications:

  • a local IPC listener for an interactive session
  • a codex tui send ... command for a live session
  • a documented way to bind an interactive TUI session to App Server-style thread/turn control

I opened this separately because the core integration gap exists even outside MCP-specific channel/inbox use cases.

That said, if maintainers would prefer to consolidate discussion into #15299, I’m happy to adapt and close this one.

sidkandan · 4 months ago

I think the smallest useful first step here is a local-only, opt-in control surface for live interactive sessions, with a phased rollout instead of treating full ingress as all-or-nothing.

If a full local ingress endpoint is too large initially, a phased shape like this seems aligned with Codex’s existing thread/turn direction:

  1. a documented local event/status surface
  • idle
  • busy
  • turn-started
  • turn-completed
  • approval-requested
  • approval-resolved
  1. a supported turn.interrupt / turn.steer path into a live session
  2. then turn.start / submit semantics once the interactive session model is settled

The reason I think this shape matters is that local wrappers/orchestrators are currently forced into PTY input emulation (send-keys, paste-buffer, pipe-pane -I, direct PTY writes). Those are all the same class of primitive from the session’s perspective: “pretend to be terminal input.” That is where the brittle failure modes come from:

  • text lands in the draft but does not become a real submitted turn
  • injected content collides with a human actively typing
  • sender/session semantics and attribution are ad hoc

If there eventually is a full ingress endpoint, the cleanest model seems like a session-scoped local endpoint that maps into Codex’s own turn semantics rather than raw stdin. But even the smaller first step of status/events plus interrupt / steer would already make local wrappers and sidecars much safer.

If maintainers prefer, I’m happy to narrow the request further to just the event surface plus interrupt / steer as a first phase.

isCopyman · 3 months ago

app server?

divyekant · 2 months ago

I have a small implementation branch for this shape here:

Scope of the branch:

  • Adds codex --experimental-inbound-channel <SOCKET> for Unix TUI sessions.
  • Starts an opt-in local Unix socket with 0600 permissions.
  • Accepts JSONL user text messages like {"text":"Please review the current diff","source":"local-client"}.
  • Routes accepted messages through the normal TUI user-turn path.
  • Does not expose remote networking, app-server RPCs, tool-call injection, or shell-command injection. Inbound text beginning with ! is submitted as user text, not treated as the TUI shell shortcut.
  • Refuses to replace non-socket paths and logs only source/text_len metadata.

Validation run locally after rebasing onto current main:

just fmt
cargo test -p codex-tui inbound_channel --lib
cargo test -p codex-tui external_user_message --lib
cargo test -p codex-cli interactive_inbound_channel_flag_parses
cargo test -p codex-tui
cargo test -p codex-cli
just fix -p codex-tui
just fix -p codex-cli
codespell codex-rs/cli/src/main.rs codex-rs/tui/src/app.rs codex-rs/tui/src/app/event_dispatch.rs codex-rs/tui/src/app_event.rs codex-rs/tui/src/chatwidget.rs codex-rs/tui/src/chatwidget/tests/composer_submission.rs codex-rs/tui/src/cli.rs codex-rs/tui/src/inbound_channel.rs codex-rs/tui/src/lib.rs codex-rs/tui/src/session_log.rs docs/tui-inbound-channel.md
git diff --cached --check

I saw the contributing policy says external contributions are invitation-only, and the API currently rejects PR creation from this account. If this direction fits the maintainers' intended design, I am happy to open/retarget a PR in whatever form you prefer.

Snailflyer · 2 months ago

This distinction matters for remote/mobile sidecars too. Without a semantic local ingress, every wrapper eventually falls back to PTY-shaped input, and then the controller cannot tell whether text was accepted as a turn, left in draft, raced with human typing, or blocked behind an approval gate.

A useful minimal surface would be both state and admission control, not only delivery:

  • idle / busy
  • awaiting_input
  • blocked_on_permission { prompt_id, requested_capability }
  • input_accepted { turn_id, source }
  • input_rejected { reason }
  • interrupt_accepted / interrupt_rejected

The input_rejected side is important. For same-session phone/desktop control, silent queueing is often worse than a hard NACK, because it creates a false liveness signal while the real TUI process is blocked somewhere else.

hendryyeh · 2 months ago

+1 to @Snailflyer on remote/mobile sidecars. There's a small cluster of community wrappers that have shipped on exactly the PTY-injection model the original issue describes — tmux send-keys / paste-buffer / direct PTY writes — and we hit the exact failure modes you list (text-in-draft-not-submitted, races with human typing, approval-gate state scraped from the terminal buffer instead of read from a session API).

A few examples, all PTY-based today: CodeVibe (iOS/Android wrapper for codex/claude/gemini), codex-pocket (codex-specific mobile), pikiclaw (web UI for claude). Same class of brittle.

The admission-control primitives Snailflyer sketched (input_accepted {turn_id, source}, input_rejected {reason}) would let every one of these drop their pane-scraping heuristics and behave deterministically. Happy to share concrete failure-mode repro logs if it'd help shape the API.

prassanna-ravishankar · 1 month ago

strong +1, and the failure matrix @sidkandan posted is exactly right. sharing from the other side of it: i maintain repowire (a cross-runtime agent mesh) and the codex transport currently falls back to tmux injection precisely because this ingress doesnt exist.

the thing that bit me and pushed me toward fail-loud: you cant tell from a send-keys or paste-buffer write whether the text was accepted as a turn, left in the draft, raced with the human typing, or swallowed behind an approval prompt. i ended up only marking a delivery as injected when a separate hook acks it back, which is just papering over the missing semantics youre asking for here.

a real local ingress with observable accept/reject would let wrappers like this drop the PTY hack entirely. id move repowires codex path onto it day one.

alexfrmn · 29 days ago

Related: the MCP notification path is a narrower opt-in local ingress primitive for trusted controllers, without PTY input emulation.

Patch: https://github.com/openai/codex/compare/main...alexfrmn:upstream-pr-mcp-surface-notifications?expand=1

safal207 · 26 days ago

This proposal maps closely to LS work on governed agent ingress, durable event identity, and replayable coordination.

The central distinction is:

text appeared in terminal
≠ message was accepted by the session
≠ turn was started
≠ requested action was authorized

PTY injection collapses all of these stages into an unreliable stream of keystrokes.

A structured local ingress could instead accept a typed request:

{
"schema_version": "local-session-ingress/v0.1",
"request_id": "local-controller:1842",
"sender": {
"controller_id": "local-orchestrator",
"process_identity": "pid:4412",
"trust_scope": "same-user-local"
},
"target": {
"session_id": "codex-thread-123",
"trajectory_id": "project-alpha"
},
"operation": "START_TURN",
"payload": {
"content_type": "text/plain",
"text": "Review the latest integration-test result"
},
"delivery": {
"deduplication_key": "local-controller:1842",
"expires_at": "2026-06-24T22:00:00Z"
},
"authority": {
"may_deliver_input": true,
"may_interrupt_turn": false,
"may_approve_actions": false
}
}

A safe processing path could be:

local IPC request
→ authenticate controller
→ validate target session
→ check operation capability
→ deduplicate
→ inspect current TUI state
→ accept / queue / reject
→ create a real thread item or turn
→ emit receipt

The key invariant would be:

«Permission to submit text to a session must not imply permission to approve tool execution, change sandbox policy, or interrupt an active turn.»

Capabilities should therefore remain separate:

  • "APPEND_DRAFT"
  • "SUBMIT_MESSAGE"
  • "START_TURN"
  • "STEER_TURN"
  • "INTERRUPT_TURN"
  • "READ_STATUS"
  • "RESPOND_TO_APPROVAL"

A local controller could be allowed to submit follow-up work while being explicitly forbidden from resolving approvals.

A delivery receipt might look like:

{
"request_id": "local-controller:1842",
"session_id": "codex-thread-123",
"status": "QUEUED",
"reason": "active human draft present",
"thread_item_id": null,
"turn_id": null
}

This makes important states observable:

RECEIVED
→ VALIDATED
→ QUEUED
→ DELIVERED
→ SUBMITTED
→ TURN_STARTED
→ PROCESSED

instead of treating successful socket or PTY writing as successful task delivery.

Human-input collision also needs an explicit rule. For example:

human draft present

  • controller requests START_TURN

→ HOLD or QUEUE

not silently merge both text streams.

A deterministic conformance fixture could test:

  1. ingress is disabled by default;
  2. only the owning local user can access the socket;
  3. wrong session ID is rejected;
  4. duplicate request creates only one thread item;
  5. active human draft prevents accidental text merging;
  6. "APPEND_DRAFT" does not automatically submit;
  7. controller without interrupt capability cannot stop an active turn;
  8. input delivery does not grant approval authority;
  9. session restart invalidates or safely rebinds the IPC endpoint;
  10. every accepted request appears in a durable audit trail with sender attribution.

Related LS work:

  • causal audit:

https://github.com/safal207/LS/issues/594

  • authorization and evidence gates:

https://github.com/safal207/LS/issues/595

  • deterministic replay:

https://github.com/safal207/LS/issues/597

  • continuation identity:

https://github.com/safal207/LS/pull/651

Would a small vendor-neutral "LocalSessionIngressRequest" and "IngressDeliveryReceipt" fixture help test controller authentication, capability separation, draft collisions, deduplication, and real turn semantics without PTY emulation?