Codex Desktop Remote SSH reconnect loop leaks thousands of orphaned app-server processes when hosts share an NFS home
What version of the Codex App are you using (From “About Codex” dialog)?
26.721.31836 (build 5828), Remote Codex versions observed in the leaked processes: codex-cli 0.144.4 and 0.145.0.
What subscription do you have?
ChatGPT Pro
What platform is your computer?
Darwin 25.5.0 arm64 arm
What issue are you seeing?
Codex Desktop's Remote SSH feature repeatedly spawned new remote app-server
processes after transport/socket failures, while the previous app-server
processes remained alive after their SSH sessions had ended.
The leaked process tree was:
node /home/<redacted-user>/.nvm/versions/node/v24.13.0/bin/codex \
-c features.code_mode_host=true app-server --listen unix://
└── .../vendor/x86_64-unknown-linux-musl/bin/codex \
-c features.code_mode_host=true app-server --listen unix://
The wrapper processes had PPID=1, no TTY, stdin attached to /dev/null, and
separate XDG_SESSION_ID values. Their corresponding systemd-logind sessions
were Remote=yes and State=closing. In other words, the SSH sessions had
ended, but the app-server processes had not.
Across 28 SSH targets, a cluster-wide snapshot found:
- 20 affected hosts
- 6,626 Codex-related processes
- 3,279 app-server instances, of which 3,277 had orphaned wrappers
- approximately 370 GiB of summed RSS (this may double-count shared pages)
On two 64-core nodes, the leaked Codex processes consumed approximately 55 and
46 CPU cores respectively. Both nodes were CPU-saturated, with load averages
above 400. The affected user's cgroup and per-PID CPU counters independently
attributed approximately 85% and 72% of total server CPU to these Codex
processes.
Local Codex Desktop logs showed hundreds of repeated cycles per host:
Starting app-server connection
-> ensure_remote_app_server
-> app_server_proxy
-> failed to connect to socket:
/home/<redacted-user>/.codex/app-server-control/app-server-control.sock
-> No such file or directory (os error 2)
-> ECONNRESET / socket hang up
-> reconnect_scheduled
-> bootstrap and start another app-server
The reconnect counts and timestamps matched the creation times of the orphaned
processes on the remote hosts. No user cron job, user systemd unit, or shell
startup command was found that launched these app servers.
What steps can reproduce the bug?
I have not rerun a destructive controlled reproduction after cleanup because
the incident affected a shared compute cluster. The following is the observed
reproduction pattern:
- On macOS, configure Codex Desktop with several Remote SSH hosts.
- Have those Linux hosts share the same NFS-backed
$HOME, leaving
CODEX_HOME=$HOME/.codex on every host.
- Connect to projects on several hosts. Codex Desktop starts:
``text``
codex -c features.code_mode_host=true app-server --listen unix://
- Cause or encounter a transport interruption, such as a network change,
sleep/wake, an SSH disconnect, or failure to connect to:
``text``
$HOME/.codex/app-server-control/app-server-control.sock
- Allow Codex Desktop to reconnect automatically.
- On the remote host, inspect the process tree:
``bash``
ps -u "$USER" -o pid,ppid,%cpu,%mem,etime,cmd --sort=-%cpu
- Observe that a new wrapper/native app-server pair is launched while the
previous pair can remain alive. The old wrapper is eventually reparented to
PID 1.
- Repeat reconnects or leave the app running through repeated failures. In the
observed incident, a new pair accumulated every few minutes for roughly two
days.
This was not associated with a single chat/session ID, token limit, or context
window. It occurred in the Remote SSH app-server connection lifecycle.
What is the expected behavior?
At most one app-server generation should remain active for a given Codex
Desktop Remote SSH connection.
When the SSH transport, proxy, or owning Desktop connection exits:
- the associated remote app-server and all of its children should terminate;
- a reconnect should reuse a healthy instance or cleanly replace the old one;
- reconnect attempts should be single-flight and bounded so failures cannot
create an unbounded number of processes;
- runtime files such as the control socket should be scoped by stable remote
connection/host identity when $HOME is shared across machines.
A failed reconnect must not leave a live PPID=1 app-server consuming remote
CPU and memory.
Additional information
All affected hosts shared:
CODEX_HOME=/home/<redacted-user>/.codex
Only SQLite state was host-scoped:
CODEX_SQLITE_HOME=/home/<redacted-user>/.codex/sqlite/<hostname>
Therefore, all hosts referenced the same apparent control-socket and log paths:
$HOME/.codex/app-server-control/app-server-control.sock
$HOME/.codex/log/app-server.log
At inspection time the socket pathname was missing, even though hundreds of
app-server listeners referring to that pathname were still alive on several
hosts. Multiple hosts also wrote to the same NFS-backed app-server log. The
shared log contained 751 model-refresh failures and 782 HTTP 429 responses.
The local Desktop log counts on the five most affected hosts were:
| Redacted host | Ensure app-server | Bootstrap completed | Reconnect started | Transport failed | Socket missing |
|---|---:|---:|---:|---:|---:|
| A | 397 | 265 | 396 | 395 | 152 |
| B | 401 | 218 | 400 | 400 | 149 |
| C | 613 | 503 | 612 | 612 | 496 |
| D | 328 | 57 | 327 | 327 | 26 |
| E | 522 | 379 | 521 | 522 | 343 |
My strongest root-cause hypothesis is:
shared NFS CODEX_HOME and control-socket pathname
-> socket/transport failure
-> Desktop reconnect starts another remote app-server
-> previous SSH session ends without terminating its app-server
-> wrapper is reparented to PID 1
-> repeated reconnects cause unbounded accumulation
This is a hypothesis about the trigger; the orphaning and process accumulation
itself were directly observed.
For recovery, I first disabled Codex Desktop's SSH connections. I then selected
only orphaned wrappers matching all of the following:
- owned by the affected user;
PPID=1;- command was
node/MainThread; - command line contained both
/bin/codexand
features.code_mode_host=true app-server --listen unix://.
Immediately before cleanup there were 3,145 matching wrappers and 3,146
descendants, for 6,291 target PIDs. After terminating only those trees, a fresh
SSH audit of all 28 hosts found zero matching app-server processes and zero
orphaned wrappers.
Related issues that appear adjacent but do not cover this exact combination of
Remote SSH reconnect, shared NFS home, and cluster-scale process leakage:
- https://github.com/openai/codex/issues/11090 — orphaned app-server processes
from the local macOS app lifecycle
- https://github.com/openai/codex/issues/20128 — fragile Remote SSH reconnects
and stale remote app-server state
- https://github.com/openai/codex/issues/20399 — cross-host state mixing when
Remote SSH hosts share $HOME
I can provide additional redacted process snapshots, CPU/cgroup measurements,
and Desktop log excerpts if useful.
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Thanks — I reviewed #34142. There is significant overlap: both involve the macOS Desktop SSH bootstrap, the same
app-server --listen unix://process, and old brokers surviving reconnects.However, I do not think this is an exact duplicate based on the evidence currently available:
futex_do_wait, and requires SIGKILL.$HOMEand the sameCODEX_HOME/control-socket pathname, which appears relevant to the repeated socket failures and reconnect loop.During cleanup I used SIGTERM followed by SIGKILL after a grace period, but the controlling SSH sessions were interrupted, so I could not reliably determine how many processes required SIGKILL. Therefore, this report does not independently reproduce the specific SIGTERM deadlock from #34142.
The two issues may form a compounding chain:
Remote SSH reconnect leaves the previous broker alive → brokers accumulate and degrade → some degraded brokers may then hit the #34142 SIGTERM deadlock.
I am leaving this issue open to track the Remote SSH reconnect/process-lifecycle and shared-home aspects, but I am happy to consolidate it if maintainers confirm that they share the same root cause.
Analysis (community)
Residual analysis for #35217 (Desktop Remote SSH reconnect loop leaks thousands of orphaned app-server processes when hosts share an NFS home) under docs/contributing.md — invitation-only. No unsolicited PR.
Root-cause hypothesis
The unbounded respawn loop may still be closed Desktop (
ensure_remote_app_server), but OSS amplifies the multi-host NFS-home failure with globally shared control-socket/lock names, cross-host “stale” unlink, no last-client shutdown for UnixSocket, and unguarded path removal on drop — matching the orphaned-wrapper / missing shared socket pattern reported here.Verified at HEAD
4c43465133(distinct from the closed Windows WMI/taskkillstorm):1. Fixed names under
CODEX_HOME— no host/generation scope.codex-rs/app-server-transport/src/transport/mod.rs: bareunix://listen resolves to fixed paths underCODEX_HOME/app-server-control/:app-server-control.sockapp-server-startup.lockNo hostname, SSH target, connection ID, or generation enters either path. Shared NFS
CODEX_HOME⇒ every remote host contends on the same directory entries.2. Startup “stale socket” removal is host-local unsafe on NFS.
transport/unix_socket.rs(prepare path): after a failed connect, a socket-typed path is treated as stale and removed. A Unix domain socket is host-local even when its directory entry is visible through NFS — so host A cannot safely decide that host B’s live listener is “stale.” Cross-host unlink of a visible path is a direct multi-host failure mode.3. Startup lock is the same global path.
The lock serializes file operations on that shared pathname (
unix_socket.rslock region) but cannot make one host’s Unix listener reachable from another. It orders racy unlinks; it does not provide cross-host ownership.4. No last-client shutdown for UnixSocket.
App-server enables
shutdown_when_no_connectionsfor Stdio, not for UnixSocket (codex-rs/app-server/src/lib.rslisten-mode branch). Losing the owning SSH/WebSocket connection therefore does not terminate a Unix-socket app-server. Acceptor + detached client tasks keep running (unix_socket.rsacceptor loop).5. Unconditional guard unlink on drop.
ControlSocketFileGuard::dropremoves the pathname without inode/owner/generation verification. One host/generation exiting can erase the visible path for another still-running listener — again amplified under shared NFS homes.6. Issue fit.
#35217’s shared NFS home, missing shared socket, reconnect/creation correlation, and orphan scale match this amplifier chain. Treat Desktop reconnect policy as closed; the OSS residual is socket/lock lifecycle under shared home.
Ranked fix outline (all OSS):
CODEX_HOME.CODEX_HOMEwith a clear error when control socket dir is on NFS.Fail-first tests: (1) two logical hosts with same
CODEX_HOMEget distinct sock paths; (2) failed connect does not unlink another generation’s live sock; (3) UnixSocket with zero clients eventually shuts down under the chosen lease policy.Happy to help draft the path-scoping patch. No unsolicited PR.
Corroborating this from a different site, with a few observations that may help narrow it down. Still present with remote codex-cli 0.148.0 (Codex/ChatGPT Desktop version
26.814.41407 (6720)onmacOS 26.5.2 (25F84)↔ Linux 6.8 HPC login node over SSH, NFS-shared$HOME, same~/.codex/app-server-control/app-server-control.sockpath).Same mechanism at smaller scale — but it bites much earlier when the remote has a per-user thread quota. The login node caps each user's systemd slice at
TasksMax=512(pids.maxcounts threads). On 2026-08-20 my slice hit 512/512:fork: Resource temporarily unavailablefor every process, codex's own TUI panicking in thenucleothreadpool, unrelated Node programs aborting onuv_thread_create— on a day I never actively used Desktop. Background reconnects alone did it.What the slice contained (cgroup walk, exact numbers):
State=closing, all from my own client IP.codex app-server proxypair (node wrapper + native child; ~22–24 threads combined once they had carried traffic, ~3 threads if they never did). So on Linux the proxy leaks too, not only theapp-server --listen unix://daemon — the proxy does not exit when its sshd parent / transport dies.-c features.code_mode_host=true app-server --listen unix://wrapper + native child + code-mode child, ~33 threads); one was 8+ h old with no clients.State=closingwhile the processes remained.Two structural details that may be useful for a fix:
/proc/<pid>/cgroup→session-N.scope) — that is what pins those sessions inState=closingindefinitely.ss -xlHelists every listener still bound to…/app-server-control.sockwith its vfsino:, and only one listener'sino:equalsstatof the path. Every other listener is a daemon that lost the name and can never receive another client, yet keeps running. On the same node, other users show the same thing:/proc/net/unixhas 29 such leaked listeners across 5 users; one user has 25 distinct listener sockets on that single path (≥ 24 permanently unreachable daemons).Related: #29809 (same accumulation → fork/exec failures), #24542 (proxy respawning unmanaged daemons), #30791 (Windows proxies accumulating).
Expected behaviour, from this angle: the proxy should treat transport EOF as terminal (or set
PR_SET_PDEATHSIG); the daemon should exit after a grace period with no clients, or as soon as it is superseded on the socket path; and a node-local socket directory ($XDG_RUNTIME_DIR) would avoid the NFS pathname aliasing this issue already points at.Analysis (community)
@ishiihiro — thanks for the 2026-08-20 HPC corroboration on #35217 (remote CLI
0.148.0, Desktop26.814.41407, NFS$HOME,TasksMax=512). The new evidence I want to bind is not the NFS pathname aliasing I posted in July. It is the proxy that does not die with sshd, plus the Unix-socket daemon that does not exit when it has zero clients. Both are still inmain.Root-cause hypothesis (re-checked 2026-08-21 at
main536f86e5c): a Remote SSH session leak is two OSS processes, not one.codex app-server proxyisstdio-to-uds(cli/src/main.rs:1346-1354). That helpertry_join!s stdin→UDS and UDS→stdout (stdio-to-uds/src/lib.rs:18-43). stdin EOF only half-closes the write side (:33). The process stays alive until the Unix-socket peer closes. The daemon on--listen unix://is not single-client:single_client_modeis Stdio-only (app-server/src/lib.rs:728), andConnectionClosedonlybreaks the server loop for Stdio (:1036-1038). UnixSocket keeps the acceptor running (unix_socket.rs:53-76). So: sshd dies → proxy stdin hits EOF → proxy hangs incopy_socket_to_stdout→ daemon keeps the UDS open → both stay in the dead session cgroup. That is the “proxy pair” you counted, and whyPR_SET_PDEATHSIGwould have helped:stdio-to-udshas zeroPR_SET_PDEATHSIGuses (the only hits in-tree are linux-sandbox / pty / sleep-inhibitor).Still live amplifiers from the July residual, now at this HEAD:
unix://is still$CODEX_HOME/app-server-control/app-server-control.sock. Yourss/ino:split is exactly “many listeners, one pathname.”prepare_control_socket_pathstill treatsConnectionRefusedas stale andremove_files the path (unix_socket.rs:108-131). A Unix socket is host-local even when the directory entry is on NFS, so host A can still knock out host B’s name.ControlSocketFileGuard::dropstillremove_files unconditionally (unix_socket.rs:178-189).setsid. App-server spawn stillsetsid()(app-server-daemon/src/backend/pid.rs:173-181) with stdin/stdout/dev/null(:166-167). That is why a leaked daemon can reparent to PID 1 and remain insession-N.scope: it is not in sshd’s process group, so SIGHUP on SSH close does not reap it. Combined with (UnixSocket ≠ last-client shutdown), it idles forever.Your two structural details map onto this: (1) PID-1 + dead-session cgroup =
setsiddaemon + no last-client exit; (2) extraino:listeners on one path = NFS-visible name + unguarded unlink/bind. Related already on the issue: #29809, #24542.No unsolicited PR. Ranked OSS levers if maintainers want them: (a)
stdio-to-udsshould treat stdin EOF as process-terminal (orPR_SET_PDEATHSIG), not wait on the UDS peer; (b) UnixSocket should get an explicit last-client / superseded-inode lease so a daemon that lost the name exits; (c) socket+lock paths should be host-scoped ($XDG_RUNTIME_DIR/ machine-id), not a single NFS-visibleCODEX_HOMEname. I have not reproduced the HPCTasksMax=512box here; this is a code read against your cgroup/ssnumbers.I reproduced the same class of leak on a single Linux host with a local ext4 home, so a shared NFS
CODEX_HOMEis not required.Environment
codex-cli 0.150.1(re-verified on 2026-08-28)ext4(/dev/vda4), not NFSwith the native Codex child process beneath it.
Observed impact
After repeated Desktop SSH disconnect/reconnect cycles, stale app-server generations remained reparented to PID 1 inside old systemd session scopes.
At the incident peak:
The 177 Codex threads consisted of:
The oldest orphan had survived for 3 days 17 hours 15 minutes. Nine duplicate app-server scopes had survived for approximately 10h49m–10h57m; another 14-thread generation self-exited after approximately 10h28m. Some associated one-thread workers had lived for approximately 1h50m–10h46m.
The stale session scopes reported
State=closing/SubState=abandoned, and the oldest app-server wasPPID=1with no active TCP connection. However, a currently used Desktop-created app-server could also appear asabandoned, so killing every abandoned scope is not a safe workaround.Recovery evidence
I terminated only the precisely identified stale scopes:
SIGTERM, so that exact stale scope requiredSIGKILLProduction containers remained healthy throughout the targeted cleanup.
Why this appears related
The directly observed lifecycle was:
This matches the core behavior reported here, but occurred on one host with a local filesystem and current
0.150.1.Expected behavior
codex doctorshould expose the owning connection and provide a supported cleanup action.No hostnames, IP addresses, usernames, account identifiers, tokens, credentials, environment variables, or raw session transcripts are included in this comment. I can provide additional targeted and redacted process/systemd snapshots if maintainers specify what is useful.