Codex Desktop remote proxy respawns unmanaged app-server and blocks daemon bootstrap

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

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-control should 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 proxy or SSH notty session 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 proxy behavior can make connection failures difficult to diagnose.

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 3 months ago

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

  • #23699

Powered by Codex Action

airsplay · 2 months ago

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 --listen processes for the same displayed Unix socket path.

Environment:

  • Local client: macOS Codex Desktop.
  • Remote host: Linux over SSH.
  • Desktop bundled Codex version: 0.137.0-alpha.4.
  • Remote Codex CLI: 0.137.0.
  • Remote app-server path shape: per-host CODEX_HOME, with socket at .../app-server-control/desktop-ssh-websocket-v0.sock.

Observed remote process state:

old app-server group:
  node wrapper pid 2564, native app-server pid 2577
  started: Mon Jun 8 11:25:02 2026 PDT
  command: codex app-server --listen unix://.../desktop-ssh-websocket-v0.sock

new app-server group:
  node wrapper pid 34731, native app-server pid 34744
  started: Mon Jun 8 12:54:24 2026 PDT
  command: codex app-server --listen unix://.../desktop-ssh-websocket-v0.sock

current proxy group:
  shell/node/native proxy pids 34853/34868/34880
  started: 12:54:24-12:54:25 PDT
  command: codex app-server proxy --sock .../desktop-ssh-websocket-v0.sock

Desktop log timeline, sanitized:

2026-06-08T18:25:02.593Z ssh_websocket_v0.remote_login_shell_command_completed
  operation=app_server_bootstrap code=0 sshAlias=<remote-host>

2026-06-08T18:25:26.987Z ssh_websocket_v0.remote_login_shell_command_completed
  operation=app_server_bootstrap code=0 sshAlias=<remote-host>

2026-06-08T18:25:28.373Z remote_connections.connection_state_changed
  hostId=remote-ssh-discovered:<remote-host> state=connected

2026-06-08T19:34:15.199Z app_server_connection.transport_closed
  code=1006 transportKind=websocket

2026-06-08T19:54:24.572Z ssh_websocket_v0.remote_login_shell_command_completed
  operation=app_server_bootstrap code=0 sshAlias=<remote-host>

2026-06-08T19:54:24.573Z ssh_websocket_v0.proxy_command_starting
  operation=app_server_proxy sshAlias=<remote-host>

2026-06-08T19:54:25.817Z AppServerConnection
  Current reported app-server version: currentVersion=0.137.0

2026-06-08T19:54:25.872Z remote_connections.connection_state_changed
  hostId=remote-ssh-discovered:<remote-host> state=connected

The socket directory/file mtime matched the second bootstrap (12:54), but ss -lxnp on the remote host showed both native app-server PIDs listening on the same displayed socket path:

... desktop-ssh-websocket-v0.sock ... users:(("codex",pid=2577,fd=27))
... desktop-ssh-websocket-v0.sock ... users:(("codex",pid=34744,fd=27))

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:

  • Two app-server processes wrote to the same remote Codex state/log storage.
  • Some thread/log rows were associated with the old process and others with the new process.
  • A remote thread later appeared in the Desktop UI as stopped immediately after local Desktop disconnect/reopen, even though I found no evidence of a user-issued turn/interrupt in the remote session/log records.

Possible invariant/fix direction:

  • Before ssh_websocket_v0 runs app_server_bootstrap, validate that no live codex app-server --listen process already owns the target socket path or per-host CODEX_HOME.
  • If a live old server exists, either adopt/connect to it or terminate it before recreating the socket.
  • Avoid unlinking/recreating desktop-ssh-websocket-v0.sock while a live app-server still owns a listener for the same logical endpoint.
  • A pid/lock file tied to the listening process, plus a live-process check, would make this easier to enforce.
dzshzx · 2 months ago

Additional evidence: pid-update-loop deletes PID file during failed restart, permanently breaking "managed" status

Environment:

  • Remote host: WSL2 (Linux 6.18.33.1-microsoft-standard-WSL2 x86_64)
  • Codex CLI: 0.139.0
  • Client: Windows Codex Desktop (remote via SSH tunnel 127.0.0.1:2222) + mobile app (via Tailscale SSH)
  • Daemon backend: pid (after codex app-server daemon bootstrap --remote-control)

Root cause chain (confirmed with process tracing):

  1. codex app-server daemon bootstrap --remote-control succeeds, writes ~/.codex/app-server-daemon/app-server.pid with {"pid":<N>,"processStartTime":"..."}, and starts both app-server --remote-control and pid-update-loop.
  1. ~5 minutes later, pid-update-loop triggers an auto-update check cycle. During this cycle it attempts to restart the app-server:
  • Stop phase: deletes app-server.pid and kills the running app-server process.
  • Start phase: fails because the Desktop-spawned unmanaged app-server (from CODEX_REMOTE_PAYLOAD via SSH notty) is already bound to desktop-ssh-websocket-v0.sock.
  1. The start failure is not rolled back — app-server.pid remains 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."
  1. Any subsequent codex remote-control start or codex app-server daemon start hits 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 start refuses to restart it.

Evidence: PID file lifecycle

T+0:00  bootstrap → app-server.pid written (pid=922102)
T+5:00  pid-update-loop update check → app-server.pid DELETED
        (restart start phase failed; Desktop proxy holds desktop-ssh-websocket-v0.sock)
T+5:00+ app-server process still alive, but:
        - app-server.pid: missing
        - daemon.lock: FREE
        - app-server.pid.lock: FREE
        → "running but not managed" state

Suggested fix:

The pid-update-loop restart logic should be atomic — if the start phase fails after stop, it should either:

  • Restore the previous PID file (rollback), or
  • Detect the Desktop-spawned unmanaged app-server and skip the restart, or
  • At minimum, leave the existing managed app-server process untouched when restart cannot complete.

Workaround:

Kill all app-server processes and re-run bootstrap:

pkill -f 'codex app-server'
sleep 2
codex app-server daemon bootstrap --remote-control

This needs to be repeated every time pid-update-loop triggers (roughly every 5–10 minutes) unless the pid-update-loop process is also killed after bootstrap.

dzshzx · 23 days ago

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

  • Remote host: WSL2 Ubuntu, standalone managed install (~/.codex/packages/standalone/current)
  • Codex CLI and app-server: 0.146.0
  • Client: remote-control over SSH

What changed since 0.139.0

The pid-update-loop chain 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:

sshd: <user>@notty
  └─ /bin/sh -c '… codex app-server proxy'
       └─ codex -c features.code_mode_host=true app-server --listen unix://

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 start

$ codex app-server daemon restart
Error: app server is running but is not managed by codex app-server daemon

$ codex app-server daemon stop
Error: app server is running but is not managed by codex app-server daemon

$ codex remote-control stop
Stopping remote control...
Error: app server is running but is not managed by codex app-server daemon

codex app-server daemon version still answers over the control socket and reports "status":"running", so the process is perfectly reachable — only the management layer refuses.

bootstrap loses a socket race against the client

Killing the unmanaged app-server and then running codex app-server daemon bootstrap --remote-control does 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:

Error: app-server control socket is already in use

bootstrap itself 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.toml from 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 -f with a codex or app-server pattern matches the invoking shell's own command line and can kill the script mid-run. Matching on the process name avoids it:

for p in $(pgrep -x codex); do
  tr '\0' ' ' < /proc/$p/cmdline | grep -q -- '--listen' && echo "$p"
done

Suggested direction

Either let codex app-server proxy attach 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.

nickdx7 · 20 days ago

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:

  • Windows Codex desktop app
  • Remote Ubuntu 24.04 Linux host connected through the app’s SSH connection
  • Remote Codex CLI/app-server version: 0.147.0
  • Standalone Codex installation
  • One shared CODEX_HOME
  • Managed app-server originally running with Remote Control enabled
  • Custom lifecycle/recovery timer explicitly disabled before testing

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 doctor reported:

  • running (persistent mode)
  • settings file present
  • PID file present
  • updater PID file present
  • 18 OK, 0 warnings, 0 failures

A Windows SSH proxy was also connected:

codex app-server proxy

Controlled reproduction:

  1. The Windows Codex desktop SSH connection was left running.
  2. The phone Remote Control client was left available.
  3. The custom recovery timer was confirmed disabled and inactive.
  4. A monitoring script continuously recorded:
  • app-server commands and PIDs
  • Windows proxy PIDs
  • Unix control-socket ownership
  • managed PID-file contents
  • Codex TCP connections
  • app-server stderr
  1. Only the verified managed app-server PID 2515160 was sent SIGKILL.
  2. No restart, bootstrap, repair or replacement command was issued by the test.

Observed timeline:

T+0.0 seconds:

  • Managed app-server was gone.
  • Control socket had no listener.
  • Existing Windows proxy was still temporarily present.
  • Managed PID file still referenced dead PID 2515160.

T+2.3 seconds:

  • Existing Windows proxy disappeared.
  • No app-server was running.
  • No control-socket listener existed.

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:

  • A new Windows codex app-server proxy process connected.
  • Later, a second Windows proxy process accumulated.
  • The Windows UI visibly continued retrying/reconnecting.
  • The unmanaged app-server remained PID 2563830 for the remainder of the 180-second test.
  • The custom recovery timer remained disabled.
  • No repair command was issued.
  • The app-server stderr log remained empty.

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 doctor reported:

  • app-server running (persistent mode)
  • status running
  • mode persistent
  • 18 OK, 0 warnings, 0 failures

codex app-server daemon version also 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 doctor can be told the app-server is managed and persistent when an unmanaged Desktop-created listener has actually taken over the shared socket.

Impact:

  • Managed Remote Control ownership is lost.
  • Phone Remote Control can disconnect because the replacement lacks --remote-control.
  • An active agent turn can be interrupted.
  • In-memory turn state, approvals, MCP connections and background-terminal tracking may be lost or reset.
  • Child processes may terminate or become orphaned.
  • Windows proxy processes can accumulate during reconnect attempts.
  • Managed start may treat the occupied socket as already running.
  • Managed restart or bootstrap can refuse because the socket is owned by an unmanaged process.
  • Users can receive contradictory reports about whether the server is managed.

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:

  1. Desktop requests recovery through the managed daemon lifecycle.
  2. Desktop waits for the managed daemon to recover.
  3. Desktop launches a replacement that remains daemon-managed and retains Remote Control.
  4. Desktop fails clearly without claiming the managed control socket.

Additionally, codex doctor and codex app-server daemon version should 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.

nos1609 · 3 hours ago

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:

codex -c features.code_mode_host=true app-server --listen unix://

under nohup, then connects through:

codex app-server proxy

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 version reports status: running whenever 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:

  1. Keep competing custom systemd/TCP/shared app-server units disabled.
  2. Let Windows Desktop remain the single owner of the SSH app-server lifecycle.
  3. Persist the same-user MCP adapter configuration under ~/.codex.
  4. After a remote reboot, reconnect from Windows Desktop. Its normal SSH bootstrap recreates the Unix socket and reloads the global configuration.

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/start for an idle thread and turn/steer for 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_HOME would 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:

  • attach to or adopt one supported managed app-server owner; or
  • pass through its native task coordination tools (list, read, and approval-gated send) to Remote SSH tasks.

Ownership should remain single and machine-readable across reconnects and reboots. daemon version and diagnostics should also distinguish “the socket answers” from “a managed daemon owns the listener.”