Codex Desktop remote proxy respawns unmanaged app-server and blocks daemon bootstrap
Summary
When a Mac Codex Desktop remote connection is active, it appears to keep a non-interactive SSH session open on the remote host and run codex app-server proxy. During migration to standalone managed daemon mode, killing codex app-server --listen unix:// is ineffective because the proxy immediately respawns an unmanaged app-server.
This causes:
codex app-server daemon bootstrap --remote-control
Error: app server is running but is not managed by codex app-server daemon
Environment
- Remote host: Linux over SSH
- Codex CLI: 0.133.0
- Client: Mac Codex Desktop remote connection
- Managed path after successful bootstrap:
~/.codex/packages/standalone/current/codex
Context
I ran into this while trying to make the same remote server available from a phone. The remote server was not showing up as expected from the phone, so I tried to start or repair remote control on the SSH host.
That led to an error around the existing app-server state. I then tried to migrate the host to the standalone managed daemon flow, but even after disabling the old codex-remote-control.service and killing codex app-server --listen unix://, an unmanaged app-server kept coming back and continued to block codex app-server daemon bootstrap --remote-control.
The surprising part was that the respawn source was not the old systemd service. It was a Codex Desktop-created SSH notty session that was still running codex app-server proxy.
Observed process tree
sshd: user@notty
`- /bin/sh -c ... codex app-server proxy
`- codex app-server proxy
codex app-server --listen unix://
The unmanaged app-server kept owning:
~/.codex/app-server-control/app-server-control.sock
Killing only codex app-server --listen unix:// caused it to reappear.
Workaround
Terminate the stale SSH notty session that owns the proxy, then run:
codex app-server daemon bootstrap --remote-control
After that, the expected managed processes are:
~/.codex/packages/standalone/current/codex app-server --remote-control --listen unix://
~/.codex/packages/standalone/current/codex app-server daemon pid-update-loop
Expected behavior
One or more of:
codex app-server daemon bootstrap --remote-controlshould report the PID and parent process that is keeping the unmanaged app-server alive.- The Desktop remote proxy should detect an existing managed daemon and attach to it instead of respawning an unmanaged app-server.
- There should be a clear command to disconnect or stop Desktop-created remote proxies on the remote host.
- The error message should mention that an active
codex app-server proxyor SSHnottysession may be the respawn source.
Actual behavior
The bootstrap command only reports:
Error: app server is running but is not managed by codex app-server daemon
The unmanaged server may be immediately recreated by the Desktop proxy, making normal kill or disable steps appear ineffective.
Possibly related
- #23102: SSH bootstrap/proxy flow is fragile and hardcoded.
- #23317: Desktop-managed SSH remotes are exposed as remote devices.
- #23572:
codex app-server proxybehavior can make connection failures difficult to diagnose.
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Additional evidence: reconnect can leave two live app-servers for the same Remote SSH socket
I hit what looks like the same underlying bug, with an additional failure mode: after a Codex Desktop Remote SSH reconnect, the remote host had two live
codex app-server --listenprocesses for the same displayed Unix socket path.Environment:
0.137.0-alpha.4.0.137.0.CODEX_HOME, with socket at.../app-server-control/desktop-ssh-websocket-v0.sock.Observed remote process state:
Desktop log timeline, sanitized:
The socket directory/file mtime matched the second bootstrap (
12:54), butss -lxnpon the remote host showed both native app-server PIDs listening on the same displayed socket path:That looks consistent with the Unix socket pathname being unlinked/recreated while the old server process still held the original listener fd. The reconnect path then started a new unmanaged app-server instead of adopting or terminating the old one.
Impact:
turn/interruptin the remote session/log records.Possible invariant/fix direction:
ssh_websocket_v0runsapp_server_bootstrap, validate that no livecodex app-server --listenprocess already owns the target socket path or per-hostCODEX_HOME.desktop-ssh-websocket-v0.sockwhile a live app-server still owns a listener for the same logical endpoint.Additional evidence:
pid-update-loopdeletes PID file during failed restart, permanently breaking "managed" statusEnvironment:
Linux 6.18.33.1-microsoft-standard-WSL2 x86_64)0.139.0127.0.0.1:2222) + mobile app (via Tailscale SSH)pid(aftercodex app-server daemon bootstrap --remote-control)Root cause chain (confirmed with process tracing):
codex app-server daemon bootstrap --remote-controlsucceeds, writes~/.codex/app-server-daemon/app-server.pidwith{"pid":<N>,"processStartTime":"..."}, and starts bothapp-server --remote-controlandpid-update-loop.pid-update-looptriggers an auto-update check cycle. During this cycle it attempts to restart the app-server:app-server.pidand kills the running app-server process.CODEX_REMOTE_PAYLOADvia SSHnotty) is already bound todesktop-ssh-websocket-v0.sock.app-server.pidremains deleted. If the original app-server process survived (or a new one started), it is now running but has no PID file, making it permanently "unmanaged."codex remote-control startorcodex app-server daemon starthits the same error reported in this issue:``
``Error: app server is running but is not managed by codex app-server daemon
Key observation: The app-server process and remote-control functionality may actually still be working at this point — the mobile app can still connect. But the daemon management layer is broken, so the user cannot restart or manage it via CLI commands. The user only discovers the problem hours later when the app-server process eventually dies (cause unknown — possibly OOM, WSL memory pressure, or relay-side TTL) and
remote-control startrefuses to restart it.Evidence: PID file lifecycle
Suggested fix:
The
pid-update-looprestart logic should be atomic — if the start phase fails after stop, it should either:Workaround:
Kill all app-server processes and re-run bootstrap:
This needs to be repeated every time
pid-update-looptriggers (roughly every 5–10 minutes) unless thepid-update-loopprocess is also killed after bootstrap.Still reproduces on 0.146.0 (2026-08-05) — and the failure surface is wider than originally reported
Re-verified today on the same setup as my June comment, two months and several releases later.
Environment
~/.codex/packages/standalone/current)0.146.0What changed since 0.139.0
The
pid-update-loopchain I described in my earlier comment no longer appears to be the trigger. On 0.14x the updater runs healthy and I could not reproduce the PID-file deletion, so that secondary mechanism looks fixed.What did not change
The behaviour in this issue's title still reproduces exactly. The client self-spawns an app-server over SSH:
and that process is structurally non-managed for its whole lifetime — not a broken state that can be repaired on the host.
Wider than originally reported: all three lifecycle verbs refuse, not just
startcodex app-server daemon versionstill answers over the control socket and reports"status":"running", so the process is perfectly reachable — only the management layer refuses.bootstraploses a socket race against the clientKilling the unmanaged app-server and then running
codex app-server daemon bootstrap --remote-controldoes not recover managed state: the client reconnects and respawns its own app-server within seconds. Whichever side loses the race leaves this in~/.codex/app-server-control/app-server.log:bootstrapitself still prints{"status":"bootstrapped","backend":"pid",…}, which is misleading, because the resulting state is only half-registered:| file | state after a lost race |
| --- | --- |
|
~/.codex/app-server-daemon/app-server-updater.pid| present ||
~/.codex/app-server-daemon/app-server.pid| absent |and the CLI keeps reporting "not managed". Deleting the stale updater PID file is needed to get back to a clean baseline.
Practical workaround on 0.146.0
Since the client owns the process lifecycle, "restarting the daemon" reduces to terminating that one PID and letting the client respawn it. The new process re-reads
~/.codex/config.tomlfrom disk, which is the only reliable way I have found to make startup-only config take effect on a long-lived daemon.One footgun worth flagging for anyone scripting this:
pgrep -f/pkill -fwith acodexorapp-serverpattern matches the invoking shell's own command line and can kill the script mid-run. Matching on the process name avoids it:Suggested direction
Either let
codex app-server proxyattach to an already-running managed daemon instead of spawning its own, or let the supervisor verbs adopt a client-spawned app-server instead of refusing outright. As of 0.146.0 there is no supported path to a managed daemon on a host that a remote client is attached to, which also means no supported way to restart it.AI drafting disclosure: This report was drafted by an AI agent after an extended troubleshooting session with the user. The technical observations below are based on a controlled test run by the user, captured in timestamped server output. The results are not based solely on agent inference.
Title: Windows Desktop SSH reconnect replaces managed Remote Control app-server with an unmanaged listener and causes misleading persistent status
Environment:
Baseline state:
The server began with one correctly managed app-server:
PID: 2515160
Command:
/root/.codex/packages/standalone/current/codex app-server --remote-control --listen unix://
The managed PID file correctly referenced PID 2515160. The control socket was owned by that process.
codex doctorreported:A Windows SSH proxy was also connected:
codex app-server proxy
Controlled reproduction:
Observed timeline:
T+0.0 seconds:
T+2.3 seconds:
T+6.9 seconds:
A new app-server appeared as PID 2563830:
codex -c features.code_mode_host=true app-server --listen unix://
This was not the managed command. It did not use the managed standalone path and did not include
--remote-control.The new unmanaged process subsequently claimed the standard control socket:
/root/.codex/app-server-control/app-server-control.sock
The managed PID file still referenced the original dead PID 2515160.
Subsequent behaviour:
codex app-server proxyprocess connected.Final diagnostic inconsistency:
At the end of the test, the actual running listener remained:
codex -c features.code_mode_host=true app-server --listen unix://
The managed PID file was missing or stale, and the running process was not daemon-owned.
Despite this,
codex doctorreported:codex app-server daemon versionalso reported a running server because something answered on the expected control socket, even though the process was not the managed daemon-owned process.This creates a significant diagnostic false positive: agents or users relying on
codex doctorcan be told the app-server is managed and persistent when an unmanaged Desktop-created listener has actually taken over the shared socket.Impact:
--remote-control.startmay treat the occupied socket as already running.restartorbootstrapcan refuse because the socket is owned by an unmanaged process.Expected behaviour:
The Windows Desktop SSH connection should detect and attach to the existing managed app-server.
If the managed app-server dies, Desktop should not replace it with a plain unmanaged listener that claims the same control socket.
One of the following should occur instead:
Additionally,
codex doctorandcodex app-server daemon versionshould verify actual PID ownership and launch mode before reporting persistent/managed status.Related open GitHub reports:
This controlled reproduction demonstrates that the problem remains present on Codex 0.147.0 on a Windows-to-Linux SSH connection.
AI drafting disclosure: this comment was drafted by an AI agent from a live, user-verified inspection. Process ownership and the commands below were checked on the remote host; private host and user details are omitted.
Windows Desktop SSH behavior and an on-demand workaround on 0.150.1
I inspected this with the Windows Codex Desktop app using its standard SSH connection to a Linux host (remote CLI/app-server 0.150.1).
The Desktop SSH bootstrap launches an unmanaged listener:
under
nohup, then connects through:The listener owns
~/.codex/app-server-control/app-server-control.sock, but the process is Desktop-launched in the user session, not owned by a systemd service. Despite that,codex app-server daemon versionreportsstatus: runningwhenever the socket answers. This confirms that the ownership/status ambiguity is still present on 0.150.1.For a Desktop-only workflow, the least-bad workaround is:
~/.codex.I verified that a same-user stdio MCP adapter can connect to the 0600 Unix control socket and expose thread list/read plus an approval-gated send route. The send route uses
turn/startfor an idle thread andturn/steerfor an active thread. The files and configuration survive reboot; the runtime returns when Windows Desktop reconnects.This is only an on-demand workaround for standard Desktop SSH. It is not a managed or headless solution, and it does not solve phone/Remote Control ownership. Starting a second app-server against the same
CODEX_HOMEwould also create competing owners and risk task/event conflicts.This should not be necessary. The Windows app already exists and already owns the SSH bootstrap, proxy, app-server connection, task UI, and native app-tools surface. Users should not have to build a custom MCP adapter, then manually ensure that no second app-server starts. That is a poor product boundary.
Windows Desktop should either:
Ownership should remain single and machine-readable across reconnects and reboots.
daemon versionand diagnostics should also distinguish “the socket answers” from “a managed daemon owns the listener.”