Mobile Remote Control cannot connect to Linux SSH remote project on Codex 0.133+/0.134, works after downgrading remote host to 0.130.0

Open 💬 3 comments Opened May 29, 2026 by ArtichoKaleyard

What version of the Codex App are you using?

Codex App for macOS: 26.519.81530 (build 3178)

Desktop bundled CLI:

codex-cli 0.133.0

Desktop app-server:

appServerVersion: 0.135.0
managedCodexVersion: 0.135.0
cliVersion: 0.133.0

What subscription do you have?

ChatGPT Pro

What platform is your computer?

Desktop host:

Darwin 25.5.0 arm64 arm

Linux SSH remote host that fails on newer Codex versions:

Linux 6.17.0-29-generic x86_64 x86_64

WSL SSH remote host that works:

Linux 6.6.87.2-microsoft-standard-WSL2 x86_64 x86_64

What issue are you seeing?

ChatGPT mobile Remote Control can connect to my paired macOS Codex Desktop host, and it can control SSH remote projects on WSL. However, it cannot connect to an SSH remote project on a Linux physical host when that remote host runs Codex CLI/app-server 0.133.0 or 0.134.0.

On mobile, selecting the Linux SSH remote project shows "Unable to connect".

The same Linux remote project works from Codex Desktop. SSH connectivity is healthy, the remote project path exists, and the remote app-server/proxy processes are running.

Downgrading only the remote Linux host to @openai/codex@0.130.0 immediately restores mobile Remote Control for the same host, same SSH route, same macOS Desktop host, same mobile pairing, and same remote project path.

This looks like a regression in the SSH remote app-server/proxy path used by mobile Remote Control after 0.130.0.

What steps can reproduce the bug?

  1. Pair ChatGPT mobile with Codex App for macOS.
  2. Add two SSH remote hosts/projects to the macOS Codex App:
  • WSL2 remote host running @openai/codex@0.130.0
  • Linux physical remote host running @openai/codex@0.133.0 or 0.134.0
  1. Confirm both SSH remote projects work from Codex Desktop.
  2. Confirm the Linux host has Remote Control enabled:

``text
remote_control = true
``

  1. Confirm the Linux remote app-server/proxy processes are running.
  2. Open ChatGPT mobile -> Codex Remote Control.
  3. Select the paired macOS Codex Desktop host.
  4. Select the Linux SSH remote project.

Observed:

Unable to connect
  1. Downgrade only the Linux remote host:

``bash
npm install -g @openai/codex@0.130.0
pkill -TERM -f 'codex.*app-server'
``

  1. Let Codex Desktop/mobile reconnect to the same Linux remote project.

Observed after downgrade:

Mobile Remote Control works again.

What is the expected behavior?

Mobile Remote Control should be able to connect to SSH remote projects through the paired macOS Codex Desktop host when the same remote project already works in Codex Desktop.

The mobile connection should not fail only because the remote Linux host is running 0.133.0 or 0.134.0, especially when the same setup works after downgrading the remote host to 0.130.0.

Additional information

Diagnostics already checked:

  • Mobile pairing is not globally broken: WSL SSH remote projects still work from mobile.
  • The macOS Codex Desktop host is online and controllable from mobile.
  • The Linux remote host is reachable over SSH.
  • The Linux remote project works from Codex Desktop.
  • The Linux remote project path exists.
  • Recreating the Linux remote project did not fix the issue.
  • Switching the Linux remote project path to an ASCII-only path did not fix the issue.
  • Restarting the macOS app-server did not fix the issue.
  • Downgrading the Linux remote host from 0.133.0 / 0.134.0 to 0.130.0 did fix the issue.

Version evidence:

Failing:
Linux remote host @openai/codex@0.134.0 -> mobile unable to connect
Linux remote host @openai/codex@0.133.0 -> mobile unable to connect

Working:
Linux remote host @openai/codex@0.130.0 -> mobile works

Current working Linux remote state:

codex-cli 0.130.0
@openai/codex@0.130.0
remote_control = true
remote project path: /home/atk/codex-workspace

Related issues:

  • #23527
  • #23062
  • #23403

Those issues appear related to mobile Remote Control with SSH remote projects, but this report is specifically about a version regression on the remote Linux app-server/proxy side: 0.130.0 works while 0.133.0 and 0.134.0 fail for the same Linux SSH remote project.

View original on GitHub ↗

3 Comments

mephistiks · 2 months ago

Same on 0.137.0, ubuntu.

bbingz · 1 month ago

Analysis (community)

Thanks for the careful multi-version bisect and the dual-remote control (WSL @ 0.130 works vs Linux physical @ 0.133+ fails). That isolation is unusually helpful.

  • Environment (from the report): macOS Desktop 26.519.81530 / bundled CLI 0.133; Linux remote 0.133/0.134 fail for mobile; Linux remote 0.130 restores mobile; WSL @ 0.130 works; Desktop can still use the Linux remote; config includes remote_control = true. A later comment reports the same on 0.137.0 (Ubuntu).
  • Repro status: not re-run end-to-end on my side, but the version isolation, Desktop-vs-mobile split, and “downgrade only the remote host” recovery are strong. Confidence in the mechanism below is high from reading the open-source enablement path; confidence that this is exactly the process shape on the reporter’s host is high but not proven until process argv / remoteControl/status dumps land.
  • Observed vs expected: Desktop SSH remote project works (proxy / control-plane healthy), but ChatGPT mobile shows Unable to connect when selecting that Linux remote project. Expected: if Desktop can use the remote project and remote control is intended to be on, mobile should reach the same remote enrollment/transport (or get a specific status reason, not an opaque failure).

Related: #23699 describes the same process-shape failure more explicitly — manual codex remote-control start yields app-server --remote-control --listen unix:// and mobile works; a Desktop restart yields plain app-server --listen unix:// without --remote-control even when settings still show remoteControlEnabled: true and the features list shows remote_control removed.

Root-cause hypothesis

This looks like a regression in how the remote Linux app-server enables the ChatGPT Remote Control transport, not SSH reachability or macOS mobile pairing.

Enablement cutover (0.130 → 0.131+):

  1. 0.130: app-server gated remote control on the config feature flag — effectively config.features.enabled(Feature::RemoteControl) when [features] remote_control = true (or equivalent). That matches the recovery when only the remote host is pinned to 0.130.0.
  2. 0.131+: Feature::RemoteControl is Stage::Removed in codex-rs/features (key: "remote_control") — always off / ignored as a feature. Enablement moves to runtime options: CLI --remote-control, daemon settings (DaemonSettings.remote_control_enabled / JSON remoteControlEnabled), and later RemoteControlStartupMode (EnabledEphemeral | DisabledEphemeral | ResolvePersisted) via start_remote_control.
  3. Daemon launch shape (codex-rs/app-server-daemon pid backend command_args / command_env):
  • remote_control_enabled: true → app-server --remote-control --listen unix://
  • remote_control_enabled: false → app-server --listen unix:// and CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1
  1. CLI entry (codex-rs/app-server main): hidden --remote-control sets EnabledEphemeral; the disabled env var forces DisabledEphemeral; otherwise ResolvePersisted. RPCs remoteControl/enable|disable|status/read (see app-server README + remote_control_processor) are the explicit control plane.

Why Desktop can work while mobile cannot: Desktop’s SSH remote path can keep a healthy unix-socket app-server / proxy for project control even when the remote-control websocket / enrollment path is disabled. Mobile needs that RC transport (or a clear status) — so a silent capability downgrade to plain --listen unix:// fits “Desktop OK, mobile Unable to connect.”

Why 0.130 restores: downgrading reintroduces config-feature auto-enable from remote_control = true, so the live process regains RC without needing daemon remoteControlEnabled / --remote-control / remoteControl/enable.

I may be wrong on the exact Desktop restart command line (daemon vs bare app-server); the invariant that matters is: settings / UI “available from signed-in devices” must not be paired with a live process that has RC disabled.

High-level fix outline (not a PR)

Small, focused, capability-preserving fix — happy to be corrected if Desktop already has a different intended contract:

  1. Every SSH / Desktop remote app-server start and restart path that intends signed-in-device access should go through the daemon contract (daemon start / restart / enable-remote-control / codex remote-control start) or equivalent args so the live process includes --remote-control whenever remoteControlEnabled (or the UI equivalent) is on. Never replace an RC-capable process with plain --listen unix:// (and avoid stamping CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1 in that case).
  2. Hardening: assert daemon start honors settings; if Desktop ever bypasses the daemon, treat silent RC downgrades as a bug (fail loudly or re-apply the flag), not as success.
  3. Optional migration: treat legacy [features] remote_control = true as a one-time signal to set daemon remoteControlEnabled=true / call remoteControl/enable. Docs should stop implying the removed feature alone enables RC on 0.131+.
  4. Status surface: prefer remoteControl/status/read fields (disabled vs connecting/connected/errored, environmentId) over opaque mobile Unable to connect when the remote process is RC-disabled.

Risks / non-goals

  • Risks: dual app-server processes (Desktop plain vs RC daemon) racing the same unix socket; auth/enrollment failures if the remote has no ChatGPT login; Windows daemon gaps (daemon is Unix-oriented today).
  • Alternatives considered: always-on RC for all SSH remotes (privacy/scope concern); leave manual codex remote-control start as the only workaround (works, but brittle across Desktop restarts — see #23699).
  • Do not change: mobile pairing for the macOS host itself, generic SSH auth, app-server proxy byte-forwarding for Desktop project use, or re-enable Feature::RemoteControl as a full feature flag without a deliberate migration plan.

I am not opening a PR unless the team invites one — sharing analysis only for alignment.

Test ideas

Failing-first ideas if the team (or an invitee) implements:

  1. Daemon unit: DaemonSettings { remote_control_enabled: true } → command_args include --remote-control and do not set the disabled env; false → opposite (pid backend already has nearby unit coverage for the arg shape).
  2. App-server integration: start with --listen unix:// only → remoteControl/status/read is disabled / environmentId null; start with --remote-control --listen unix:// → status moves onto connecting/connected path (mock enroll WS as needed).
  3. Regression / capability preservation (Desktop-equivalent restart): simulate a restart that would omit --remote-control while settings still say remoteControlEnabled: true → must fail or re-apply the flag (#23699 invariant).
  4. Migration (if implemented): config with [features] remote_control = true on a post-0.130 binary results in an RC-enabled process after bootstrap/start.
  5. Manual E2E checklist: macOS Desktop + Linux SSH remote @ current CLI; mobile selects remote project; remote ps shows app-server --remote-control --listen unix://; mobile connects; repeat after Desktop “restart Codex on remote.”

Questions for maintainers

  1. On the reporter’s host, what is the exact Desktop SSH start/restart command line — daemon lifecycle vs bare codex app-server, and is CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED set?
  2. On a failing 0.133+ remote: output of daemon/version JSON, remoteControl/status/read, and process argv — does it match plain --listen unix:// without --remote-control?
  3. Is mobile connecting to a separate remote environmentId on Linux, or only through the macOS host’s project list? (#23699 suggests the Android client can hit the remote app-server directly.)
  4. Is WSL@0.130 vs Linux@0.133+ purely version, or also install path (npm -g vs standalone ~/.codex/packages / daemon bootstrap)?
  5. Did Desktop ever call enable-remote-control successfully on the Linux host (settings true, process without flag)?
  6. Any protocol/enrollment field (installation_id / server_name) break on some Linux hosts independent of enablement?

Happy to refine this analysis with any process dumps or correct the approach if the intended contract differs. If the team later wants an invited PR along these lines, I would be glad to help — no pressure either way.

bbingz · 1 month ago

Building on the root-cause analysis above with the exact introducing PR my behavioral "0.130 -> 0.131+ cutover" maps to, plus a unit test that locks the config-ignore as intended. Traced against main as of 5dd992a (2026-07-24).

Regression introducer. The cutover is PR #22578 (4e368aa2e9, merged 2026-05-14, "enable/disable remote control at runtime, not via features"). Its body states it "reapplies #22386 which was previously reverted", "Marks Feature::RemoteControl as removed and ignores [features].remote_control", and "Updates managed daemon startup to use codex app-server --remote-control --listen unix://". #22578 touches codex-rs/features/src/lib.rs (+5) + features/src/tests.rs + app-server-transport/.../remote_control/mod.rs + remote_control_processor.rs + app-server-daemon/.../pid.rs - including the Stage::Removed + config-ignore change (plus enable/disable surfaces). This is the commit that made [features] remote_control = true config-inert. Split for precision: #22578 introduced enable/disable + the --remote-control flag + Stage::Removed; remoteControl/status/read and the extension of RemoteControlStatusChangedNotification (e.g. server_name) landed in #22877 (merged 2026-05-15, "update remote control APIs for better UX") - the notification itself predates #22877.

Full archaeology (for the timeline).

  • #22386 (2026-05-13): first marked Feature::RemoteControl Stage::Removed.
  • #22520 (2026-05-13, same day): reverted it.
  • #22578 (2026-05-14): re-applied #22386 (Stage::Removed + config-ignore) and added remoteControl/enable|disable + the --remote-control flag.
  • #22877 (2026-05-15): added remoteControl/status/read and extended the existing RemoteControlStatusChangedNotification (e.g. server_name).
  • #27445 (2026-06-12): added persistence of RC desired state (RemoteControlStartupMode::ResolvePersisted).

Not fixed as of 5dd992a. Stage::Removed + the config-ignore continue. DaemonSettings (codex-rs/app-server-daemon/src/settings.rs:9, #[derive(Default)]) yields remote_control_enabled: false by default, and I found no code path from legacy [features] remote_control = true into DaemonSettings or the enable RPC - i.e. no migration.

Two distinct disabled-path mechanisms (do not conflate).

  • (A) Managed daemon with default/missing settings: the pid backend (app-server-daemon/src/backend/pid.rs:413-430) starts app-server --listen unix:// (no --remote-control) with CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1 env (command_env() returns (REMOTE_CONTROL_DISABLED_ENV_VAR, "1")). This is the daemon-disabled path.
  • (B) Plain --listen with ResolvePersisted and no prior enable: desired_state_from_persisted_enrollment (app-server-transport/src/transport/remote_control/desired_state.rs) resolves a user with no persisted enrollment to Disabled.

Which path the OP hit depends on whether remote was started via the managed daemon vs a bare app-server (the first comment left this open). If via the managed daemon with default settings, the disabled env is set - but asserting that for the OP specifically needs host evidence, so I'd hold the reporter's exact process shape as inferred, not proven.

Unit test that locks config-ignore as intended. codex-rs/features/src/tests.rs:165-174 remote_control_config_is_ignored asserts features.enabled(Feature::RemoteControl) == false after apply_map({remote_control: true}). Any migration fix must update or delete this test - a concrete anchor for maintainers.

Source-of-truth pointer. REMOTE_CONTROL_DISABLED_ENV_VAR = "CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED" is defined at codex-rs/app-server-transport/src/transport/remote_control/mod.rs:86-87.