Hooks spawned by the app-server daemon cannot identify the client terminal, breaking terminal-targeting hooks
Summary
Since the interactive TUI became a client of the app-server daemon, lifecycle hooks are spawned by that detached daemon rather than by the process that owns the terminal. The hook payload contains no terminal or client identity, so a hook has no supported way to tell which terminal (tab/window) the session it fired for belongs to.
This breaks any hook that acts on the user's terminal: per-tab progress/status indicators, tab titles, bells, notifications targeted at the originating window. It affects the interactive TUI only. codex exec is unaffected, because it still runs inside the terminal's own process tree.
Environment
- codex-cli 0.147.0 (standalone), x86_64-unknown-linux-musl
- Linux (WSL2), Windows Terminal
features listreportstui_app_serveras stageremoved, effectivetrue
What changed
Previously a hook could find its terminal by walking up its own process ancestry to the codex process and using that process's pty. That pty was the tab, which made the result correctly per-tab.
Now the TUI process is a thin client connected to the daemon over app-server-control.sock, and hooks are children of the daemon. Observed on a live session:
hook process -> ppid = <app-server daemon pid>
fd0 -> /dev/null
fd1 -> /dev/null
fd2 -> ~/.codex/app-server-daemon/app-server.stderr.log
-> <init>
There is no terminal anywhere in that ancestry, so the walk cannot succeed by design.
Reproduction
- Register any user hook that writes to the terminal, for example a
PreToolUsecommand hook that emits anOSC 9;4progress sequence to the tty it resolves. - Run
codex exec "..."in a terminal. The hook resolves the terminal and the write lands. - Run the interactive TUI in a terminal and issue a prompt. The hook fires, but no terminal is resolvable.
In a 30 second TUI session I observed 10 hook invocations, all of which failed to resolve a terminal. The same hook succeeded under codex exec in the same terminal.
Hook payload has no client identity
A captured PreToolUse payload (stdin):
{"session_id":"...","turn_id":"...","transcript_path":"...","cwd":"...",
"hook_event_name":"PreToolUse","model":"...","permission_mode":"default",
"tool_name":"Bash","tool_input":{...},"tool_use_id":"..."}
No tty, no client id, no window/tab identifier. The hook environment is also stripped of the terminal variables that would otherwise help (TERM_PROGRAM, WT_SESSION, and similar are absent).
There is also no way to recover the mapping indirectly:
- The TUI process holds no session state. The daemon holds every rollout file descriptor; the TUI holds only sqlite handles.
state_5.sqlitethreadshas no column identifying the client that owns a thread.
Impact
Any hook whose purpose is to affect the user's terminal cannot target it correctly. With several tabs open there is no supported way to distinguish them, so such hooks either do nothing or risk acting on the wrong window.
This is silent. Hooks continue to fire and exit successfully, so nothing surfaces in the UI to indicate the capability was lost.
Request
Include the originating client's terminal identity in the hook payload, for example a tty field carrying the client's terminal device, or a stable client identifier that can be correlated with the connected client. Exposing it in the hook environment would work equally well.
If exposing the tty directly is not desirable, a stable per-client id in both the payload and a queryable client list would be enough to build the mapping.
Note on workarounds
-c features.tui_app_server=false is accepted but ignored (the feature is stage removed), so there is no way to opt back into the in-process TUI.
It is possible to approximate the mapping outside Codex, by matching codex resume <session-id> in a client's argv, following parent_thread_id for subagent sessions, and comparing session creation time against client process start time. That works, but it depends on argv shape and rollout file naming, neither of which is a stable interface.