Codex mobile does not show SSH remote projects from connected Mac host
Summary
Codex mobile can connect to a macOS Codex App host, and that macOS host can connect to an SSH remote project successfully. However, SSH remote projects that are visible and usable in the Mac Codex App do not appear in the ChatGPT mobile Codex project selector for the same connected Mac host.
This seems inconsistent with the Remote connections docs, which describe the remote development workflow as: the Codex App host connects to the SSH/devbox environment first, while the phone still connects to the Codex App host.
Docs reference: https://developers.openai.com/codex/remote-connections
Environment
- Codex App for macOS:
26.513.31313 - Codex App bundled CLI:
codex-cli 0.131.0-alpha.9 - Local platform:
Darwin 24.2.0 arm64 - Remote SSH host: Linux arm64
- Remote Codex CLI:
codex-cli 0.130.0 - ChatGPT mobile app: iOS, latest App Store build available at time of testing
- Same ChatGPT account and same workspace are used on both Mac and iPhone
What Works
- iPhone can see and connect to the Mac Codex App host.
- Mac Codex App can see the SSH remote project in the sidebar.
- Mac Codex App can connect to the SSH host.
- Remote host has Codex installed and available in the remote login shell.
- A Codex SSH app-server/proxy process is running from the Mac host to the SSH host.
Remote verification:
ssh <ssh-alias> 'pwd; hostname; whoami; command -v codex; codex --version'
/home/<remote-user>
<remote-hostname>
<remote-user>
/usr/local/bin/codex
codex-cli 0.130.0
Local Codex global state contains the remote project:
{
"remote-projects": [
{
"id": "<project-id>",
"hostId": "remote-ssh-discovered:<ssh-alias>",
"remotePath": "/workspace",
"label": "oci"
}
],
"project-order": [
"<local-project-path>",
"<project-id>"
]
}
Mobile remote-control enrollment exists for the Mac host:
remote_control_enrollments count: 1
server_name: <mac-hostname>.local
environment_id: env_e_<redacted>
Actual Behavior
On iPhone:
- Open ChatGPT mobile app.
- Open Codex.
- Select the connected Mac Codex App host.
- Open the project selector.
Only the local Mac project appears. The SSH remote project does not appear, even though it appears in the Mac Codex App sidebar and local Codex state.
Rescanning the Codex mobile QR code does not fix the issue. After scanning, ChatGPT mobile goes directly to the Connections page and treats the Mac host as already connected rather than refreshing the host project list.
Killing/reopening the iOS ChatGPT app does not fix the issue.
Deleting and reinstalling the iOS ChatGPT app also does not fix the issue. After reinstalling, the phone can still connect to the Mac Codex App host, but the SSH remote project still does not appear in the project selector.
Expected Behavior
After a Mac Codex App host adds an SSH remote project and the phone is connected to that Mac host, the SSH remote project should appear in the mobile Codex project selector for that host.
The user should not need to delete/reinstall the ChatGPT mobile app or redo mobile setup every time a new SSH remote project is added to the Mac host.
Steps to Reproduce
- On macOS, sign in to Codex App.
- Set up Codex mobile and connect an iPhone running ChatGPT mobile.
- Confirm the Mac host appears in Codex mobile.
- On macOS, add an SSH host in
~/.ssh/config. - Confirm
ssh <ssh-alias>works from the Mac. - Install/authenticate Codex on the SSH host and confirm
codex --versionworks in the remote login shell. - In Mac Codex App, add/enable the SSH host under
Settings > Connections. - Add a remote project folder such as
/workspace. - Confirm the project appears in the Mac Codex App sidebar.
- On iPhone, select the same Mac host and open the project selector.
Observed: the SSH remote project does not appear.
Troubleshooting Already Attempted
- Confirmed same ChatGPT account and workspace on Mac and phone.
- Confirmed Mac host is online and awake.
- Confirmed remote-control/mobile enrollment exists for the Mac host.
- Confirmed remote SSH works from the Mac.
- Confirmed remote Codex CLI exists and works.
- Confirmed a Codex SSH app-server/proxy process is running for the remote host.
- Reset local mobile remote-control enrollment on the Mac and rescanned the QR code.
- Killed and reopened the iOS ChatGPT app.
- Deleted and reinstalled the iOS ChatGPT app.
Additional Observation
Direct SSH from the iPhone to the same remote host can be made to work as a workaround. That suggests the remote host itself and remote Codex runtime are valid. The missing behavior appears specific to the mobile-to-Mac-host path not surfacing the Mac host's remote-ssh-discovered:* projects in the mobile project selector.
Impact
The documented mobile-to-Mac-to-SSH workflow is not usable from mobile when SSH remote projects added on the Mac do not sync to the mobile project selector.
10 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
🔧 Proposed Fix: Forward
remote-projectsThrough App-Server ProtocolRoot Cause
The remote-control protocol forwards local projects from the Mac host's
project-orderconfig, but does NOT forward SSH remote projects from the Mac'sremote-projectsconfig. The app-server proxy process knows about these projects (it creates the SSH connections) but doesn't include them in the project list sent to mobile clients.Fix
1. Forward remote-projects in app-server project discovery:
2. Mobile client handles
ssh-remoteproject type — render SSH projects in project selector with proxy connection through Mac host.3. App-server proxy forwards SSH project connections — when mobile selects SSH remote, Mac app-server establishes/uses existing SSH connection.
Why This Works
global-state.json)ssh-remotetypeCompetitive Edge
iOS,app,app-server,remote)---
<details>
<summary>📋 Full Technical Analysis</summary>
Solution: openai/codex #23527 — SSH Remote Projects Not Visible on Mobile
Issue Summary
Codex mobile connects to macOS Codex App host successfully, and macOS can see SSH remote projects. However, SSH remote projects visible in the Mac Codex App do NOT appear in the ChatGPT mobile project selector for the same connected Mac host.
Root Cause Analysis
The Data Flow Gap
remote-projectsconfig in~/.codex/global-state.jsonWhy This Happens
The remote-control protocol likely forwards:
project-orderremote-projectsconfigThe
remote-projectsarray in global-state.json is app-local configuration. The app-server proxy process knows about these projects (it creates the SSH connections), but doesn't include them in the project list it sends to the mobile client.Proposed Fix
Fix 1: Forward remote-projects through app-server protocol
Fix 2: Mobile client handles ssh-remote project type
Fix 3: App-server proxy forwards SSH project connections
When mobile selects an SSH remote project:
connect(projectId)to Mac app-serverremote-projectsconfigKey Insight
The SSH remote projects ARE discoverable from the Mac host (they're in global-state.json). The app-server proxy just needs to include them in the project list it sends to mobile clients, and the mobile client needs to handle the
ssh-remoteproject type.Estimated Impact
Competitive Analysis
iOS,app,app-server,remote— cross-component bug</details>
I have a workaround for users like @jameBoy who may be testing experimental or prerelease builds. It can be applied directly on a Linux remote host today, without rebuilding the Desktop app, patching the Desktop app-server source, or waiting for mobile/client parity to catch up.
Here’s the exact shim I use for standalone app-server binaries on my remote hosts, without including my personal tooling.
Install this minimal Codex remote shim
Check first:
Create the shim:
Make sure
~/.local/binis on PATH:For the current shell:
Verify:
Expected:
Verify the real app-server exists:
If the app-server binary lives somewhere else, either edit this line in the shim:
or set an environment variable:
What this does
When Codex Desktop asks the SSH host to run:
the shim launches:
That makes the remote Linux host enroll as its own mobile-visible remote-control device instead of relying on the old:
config path.
The simple shim above does not require anything else. For this specific mobile/remote-control workaround, the only important part is that the SSH host has a standalone
codex-app-serverbinary and that thecodexshim turns:into:
That bypasses the stale
[features].remote_control = truepath and makes the SSH host enroll directly as a remote-control device or ssh remote connection with the latest version of the Codex Desktop app. Both options become possible, with it also being accessible from the ChatGPT mobile app. Adjust my shim as needed to suit your own requirements.https://github.com/openai/codex/issues/23527#issuecomment-4493602069
The key difference I found is between
0.130.0and0.131.0.With
0.130.0,[features].remote_control = truestill works as the old live control path. I was able to get a remote SSH host visible from mobile and toggle remote control successfully.With
0.131.0,remote_controlunder[features]was intentionally moved to removed/inert compatibility behavior. Config APIs can still read/write it, but that write no longer controls remote-control process enrollment. That maps directly to the Desktop toggle flashing back off / doing nothing on a plain0.131.0remote host.When I traced the Desktop switch, I saw this on enable:
And this on disable:
So the Settings switch is still writing
features.remote_control = true/false.That worked in the
0.130.0world, but in0.131.0app-server startup now needs the runtime/process path instead:Related changes:
remoteControl/enable,remoteControl/disable, andremoteControl/status/readlanded in the runtime API work: #22578 / #22877
codex remote-controlmoved toward daemon-managed startup: #22218 / #225620.131.0release notes mention daemon-managedcodex remote-control,runtime enable/disable APIs, and status reads.
On a normal standalone install like:
Desktop starts the remote process as:
not:
My workaround was to use a small remote
codexshim. The shim implements only the surface the Desktop SSH flow needs:and when Desktop asks for:
the shim actually launches:
That makes the Linux SSH host enroll as its own mobile-visible remote-control device. It does not rely on the Mac app forwarding a nested SSH project, and it does not rely on the deprecated/inert
[features].remote_controlpath.The Desktop UI may still show "Allow other devices" as disabled or flash the toggle back off, because that UI path is not fully aligned with the
0.131.0remote-control runtime model yet. But the remote host can still show up on mobile because the app-server process was started with--remote-control.So I think there are two separate lanes here:
One other compatibility issue I discovered during my analysis was version reporting. The Codex Desktop SSH connections panel appears to care about the exact
codex --versiontext returned by the remote host, not just whether the app-server behavior is actually present.For example, one of my earlier remote shims reported:
and Codex Desktop surfaced it as:
That was a useful clue: even though
0.130.0-alpha.5looks close to0.130.0, Desktop treated the prerelease string as below the stable compatibility floor.The same class of problem can happen with local/custom builds off the latest commit in main-branch.
Depending on how the build metadata is stamped, my locally built Codex artifacts first reported odd versions like:
and the built artifact prerelease provided on github included strings like:
Those version strings can still trip the compatibility/status logic in the SSH remote connections panel, even if the app-server binary itself has the feature behavior you need because that's how the desktop app is developed currently.
That is why the shim’s version response is not just cosmetic. In my shim I make it report a stable-looking version:
This does not mean the remote has the full stock CLI installed. It is a Desktop/mobile compatibility response for the SSH handshake. The actual app-server is still the standalone
codex-app-serverbinary behind the shim.For anyone testing this on a normal remote host, I would recommend getting off alpha/dev builds first:
Expected:
That avoids prerelease/build-version weirdness before debugging the separate remote-control behavior.
If someone wants the least weird path today and does not need the latest version, staying on remote
codex-cli 0.130.0is the easiest option. In0.130.0, the oldfeatures.remote_control=trueflag still works and remains in parity with the Desktop app switch toggle.If you want to test
0.131.0or later, the shim/runtime-flag path is more reliable than expectingfeatures.remote_control=trueto keep working.Yes, you can use the daemon path with the full CLI instead. But in practice, you can also run the standalone Desktop-launched/nohup-style detached app-server process instead of using the
codex app-server daemon/codex remote-control startdaemon path.It is already daemon-like because it is backgrounded with
nohupand survives the SSH launcher, but it is not daemon-managed. The shim injects--remote-controlwhen Desktop starts:so the real process becomes:
with a live Unix control socket.
You do not need the daemon management commands for this workaround. You only need listener mode plus the app-server's internal remote-control protocol support, which lets the host behave like a persistent remote-control device.
One note: the public help text for the standalone app-server binary does not currently advertise
--remote-control:but the app-server accepts the flag when the shim launches it with
--remote-control.https://github.com/openai/codex/issues/23527#issuecomment-4493613001
I use this shim as part of a separate setup that is a bit ahead of the current out-of-the-box Codex flow. In that setup, I run a patched standalone executor on an internal VM:
with a helper binary beside it:
That lets my main "bastion" remote host control internal VMs as execution environments. Instead of the remote connection falling back to one-off SSH commands into those VMs, the bastion can expose normal Codex tooling while execution happens inside any of my internal VMs.
In my setup, the internal VMs sit behind the bastion on a private bridge, and the bastion can provide routing/egress when needed. The result is one remote Codex control plane on the bastion, with multiple internal execution environments behind it, without losing the native Codex tooling. The reason this matters is that each Codex thread is normally tied to a single remote connection. When working across multiple VMs, that creates a context gap: either you open separate remote connections and split the work across threads, or you stay on the bastion and use plain SSH commands, losing the native Codex tool surface inside the VM.
My patched route tries to avoid that. The Desktop app connects to the bastion, but the bastion app-server can route tool execution into a configured internal VM environment. That keeps the work controllable from one thread while still letting the execution happen inside the VM.
The basic goal was to avoid installing the full Codex CLI or a full app-server on every internal VM. The internal nodes only need the executor side: a standalone
codex-exec-serverbinary, thecodex-linux-sandboxhelper beside it so filesystem and patch operations could work correctly, SSH access from the bastion, and an environment entry on the bastion that launches the executor over SSH.To make that work, I created a standalone
codex-exec-serverbinary wrapper. Because upstream mostly treats exec-server as part of the larger Codex stack, I had to patch the MUSL build dependency shape to make the executor standalone. So after generating a tiny standalonecodex-exec-serverbinary wrapper in a temporary overlay, the upstream source tree stays read-only/unmodified; the wrapper is added only in the overlay. That wrapper supports the executor listen path I need:To process all this, I built a standalone virtual machine MUSL build pipeline. The flow fetches an immutable upstream
openai/codexsource archive, verifies it, transfers it to the virtual machine, extracts it into a versioned source tree, and builds inside a containerized MUSL lane instead of installing build dependencies directly on the VM host. Output artifacts go into a dedicated build-output directory, with hashes,filemetadata, and provenance sidecars recorded.The build flow fetches a pinned upstream Codex source archive, verify it, transfer/build in the internal VM build environment, produce MUSL artifacts, hash them, and record provenance so I knew exactly which upstream commit and patchset produced the binaries. That mattered because I was not just installing a normal CLI release; I was producing standalone app-server/exec-server artifacts with controlled patches.
The standalone executor build also needed dependency work. The first standalone build pulled OpenSSL through
reqwestdefault features because it did not include the larger app-server/core graph that normally handles that dependency shape. I patched the overlay build to usedefault-features = falsewithrustls-tlsfor that standalone executor build.There were also wrapper/runtime fixes. The standalone executor must ship with
codex-linux-sandboxbeside it and pass that helper path intoExecServerRuntimePaths, otherwise filesystem and patch operations fail with:I also had to handle the hidden filesystem-helper mode before starting Tokio. The first helper-aware wrapper still failed with:
so the generated wrapper now detects the filesystem-helper arg first, runs that helper path directly, and only starts Tokio for normal
--listenor--remoteexec-server modes.On the app-server side, I first tested shallower patches and learned they were not enough. Patching
command/execalone did not move the native Codex tool calls into my other virtual machine used for testing from my main "bastion/orchestrator" virtual machine, because the active tool path was going through the runtime/unified-exec path instead. I then moved the patch to the lower execution seam where requests are converted and forwarded to the remote exec-server. In my validated setup, the default environment points to a configured execution environment instead of always implicitly executing locally, but my model supports multiple configured VM environments behind the bastion.I caught another issue through a debug wrapper I had setup during a dry run while on my testing virtual machine which helped captured the raw JSON-RPC stream and showed the real failure: The virtual machine was receiving
process/startwith a bastion-local sandbox wrapper in the argv, including paths like:or later, the same shape without the
--apply-seccomp-then-execmarker:That explained the
No such file or directoryfailures: The internal virtual machine where I was testing the built version was trying to execute a sandbox helper path that only existed on the bastion. The app-server/exec forwarding patch now detects those bastion-local sandbox wrappers, strips them, and forwards only the real inner command to the selected internal VM environment.The final patch bundle also lets the bastion app-server default tool execution into a configured environment, using environment variables such as:
After that, I validated more than just "can it run hostname." I validated command execution and file/editor semantics separately. The final checks proved:
and validated
apply_patchadd/update/delete behavior against the testing VM environment, which is where thecodex-linux-sandboxhelper became necessary.I also pinned the known-good patchset instead of leaving this as one-off source edits. The validated patchset is tied to a specific upstream commit, artifact hashes, patch hash, and tooling hashes. The current known-good patchset records the [my internal software name] routing patch, the app-server artifact, the standalone exec-server artifact, and the
codex-linux-sandboxhelper. I added lifecycle/debug helpers so future upstream refreshes can run a lineage audit, scaffold a new patchset, rebuild, redeploy, and revalidate command plus filesystem/editor semantics before marking anything as known-good.Validation helped confirm that
environment_id=localexecuted on the bastion,environment_id=testing-vmexecuted inside the configured VM, filesystem operations worked, patch add/update/delete semantics worked, the sandbox helper was available, and the app-server/proxy/control-socket lifecycle still behaved correctly.I'm leaving my insights here for anyone curious on how to take things further when using non-stable versions, and how I came across this exact problem with my work with constraints, like keeping the internal nodes lean using a dogfooding approach to build out my own workflow narrowly etc. I like that the internal VM(s) do not need the CLI at all nonetheless a full app-server install. It only needs my standalone patched
codex-exec-serverbinary, thecodex-linux-sandboxhelper beside it, SSH connectivity from the bastion, and an environment entry configured on the bastion that launches that executor over SSH.Using direct SSH from the ChatGPT app can reach the host, but it does not give you the same single-thread Codex control plane with routed tool execution across internal VM environments.
With the approach I described, the Desktop/mobile connection talks to one remote Codex control plane, while that control plane can route execution into different internal VMs and still preserve the native Codex tooling surface, including things like applying patches.
My personal goal when I came across OP’s concern was to keep my workflow usable from both mobile and the Desktop app as native control devices, without patching the Desktop app and without needing to SSH directly from my phone, while staying inside one coherent Codex thread even when moving back and forth across multiple virtual machines.
Your repro points to a project-discovery boundary rather than a remote runtime boundary: the Mac host can see and run the SSH project, but the mobile selector does not surface the host's
remote-ssh-discovered:*projects.A practical distinction for the mobile design is whether the phone is controlling a Mac host that then proxies to SSH, or whether the owner process is already running on the Linux/SSH machine and the phone only attaches to that live process. The second model avoids depending on the Mac app's project selector to mirror remote SSH state.
I built Faryo as a narrow open-source route for that second model: run Codex/Claude/shell inside tmux on the owner machine, then use a phone/browser workbench for compact output, short input, approvals/interrupts, and handoff. It is not a native Codex mobile fix, but for Linux/tmux-first remote projects it may be a useful workaround/comparison: https://github.com/Snailflyer/faryo
I ran into this issue too. Later I found that it was caused by a corrupted state_5.sqlite, which made codex app-server fail to start. In the end, I also had to reset remote_control so the app and client could reconnect properly and fully resolve the problem.
https://github.com/openai/codex/issues/23247#issuecomment-4505637794
Additional repro from another setup, on newer builds.
Environment:
26.616.81150build4306codex-cli 0.142.026.5.1build25F80,arm64x86_64codex-cli 0.141.0andcodex-cli 0.142.0The Mac host can use the SSH remote project normally from Codex Desktop. The SSH host is configured in
~/.ssh/config, Codex is installed on the remote login shell, and Codex Desktop has active SSH app-server/proxy processes for the remote hosts.The Mac host's Codex state contains both discovered SSH remote connections and a saved SSH remote project. Redacted shape:
A/B reproduction:
你好) in a new thread.你好) in a new thread.Observed:
This makes the failure narrower than a general remote-control or account/workspace sync issue: mobile remote control can see the connected Mac host and can sync local-project threads from that host, but it does not surface the Mac host's saved
remote-ssh-discovered:*project or threads created under that project.This matches the earlier diagnosis in this issue: the missing boundary appears to be forwarding/preserving the Mac host's SSH remote project identity through mobile project/thread discovery and resume.
@damaged-soda
One useful follow-up from my side is that your repro appears to be hitting a different boundary than the one I worked around.
You are running real
0.141.x/0.142.xremote binaries and still reproducing a Mobile/SSH project visibility failure on newer builds. Your repro strongly suggests that Mobile does not currently preserve or expose Desktop-managedremote-ssh-discovered:*project identity across the Desktop -> Mobile boundary. That suggests this is not just a stale remote CLI/app-server problem. It looks more like a product-level gap in how Mobile discovers Desktop-managed projects, including nested SSH remote projects.In my setup, I am currently on Codex Desktop Beta
26.611.61753with a patched0.134.0-alpha.3app-server underneath. Because newer Desktop builds tightened compatibility checks, my older remote app-server stopped working until I updated the compatibility layer. My architecture made the remote server itself the Mobile-visible remote-control environment while still allowing Codex Desktop to use it as a normal SSH remote connection.First, here is what I noticed at the boundaries my workaround hit.
The Desktop-side break was not only
codex --version. Newer Desktop also inspectedinitialize.result.userAgentafter the websocket/control connection opened. So fakingcodex --versionwas no longer sufficient. The app-server initialization identity also had to begin with a compatible>= 0.139.0token. I solved that with an originator/user-agent compatibility mask while still running my validated patched0.134.0-alpha.3app-server underneath.That restored Desktop remote connection behavior, but it did not solve Mobile.
Mobile was a different path. It was not using the SSH bootstrap surface. It discovered my remote server through remote-control backend enrollment. The live app-server still enrolled with its compiled metadata:
The shell shim could not change that field. The fix was a narrow loopback forwarder between app-server and ChatGPT backend-api that rewrites only the remote-control enrollment payload:
Everything else, including ordinary HTTP traffic, auth, thread state, websocket payloads, and remote-control messaging, is relayed without semantic rewriting. I did have to fix the websocket relay implementation itself after real Mobile send traffic exposed a relay stability bug, but that was a transport bug, not another compatibility payload rewrite.
So the newer compatibility picture in my observed setup became:
Desktop compatibility required:
Mobile/backend enrollment compatibility required:
Mobile send-path stability also required:
Those turned out to be completely independent compatibility surfaces. Restoring Desktop compatibility did not restore Mobile compatibility, which is what led me to separate the investigation into distinct boundaries instead of treating "remote-control" as one monolithic feature.
I did not promote my runtime to a real
0.139.0app-server. I kept the patched0.134.0-alpha.3runtime because it carries my app-server/exec-server routing fabric. I only added compatibility masks at the places newer clients/backend started enforcing version identity.The other important detail is that the working Mobile path used my normal Codex home and existing state database, not an isolated canary home. My isolated canary successfully enrolled, but because it intentionally used a separate
CODEX_HOME, it also had a separate state database and therefore no existing projects or thread history. That told me the remaining problem was no longer enrollment itself, but continuity with the existing runtime state.The useful path was:
For your issue, though, the important distinction is this:
Your repro says Mobile can see the connected Mac host and local project threads, but cannot see the Mac Desktop's saved
remote-ssh-discovered:*SSH project or its threads. That appears to be a Desktop-to-Mobile SSH project identity propagation boundary rather than a basic remote-control or account synchronization problem.My setup avoids that nested boundary entirely by making the remote bastion enroll as its own remote-control environment instead of expecting the Mac host to forward a saved SSH remote project into Mobile.
A useful A/B test would therefore be to launch the SSH host's app-server directly with
--remote-controland see whether that host appears to Mobile as its own remote-control environment. If it does, that would support the theory that the remaining issue is specific to Desktop-managed SSH project propagation rather than remote-control itself.Since you are already on real
0.141.x/0.142.xbinaries, you probably do not need the compatibility masks I added for my older patched runtime. My compatibility work existed to preserve a validated0.134.0-alpha.3app-server while newer Desktop and Mobile compatibility checks evolved. Your setup is already beyond that point.If direct remote-control enrollment works for your SSH host, it would structurally represent a different architecture rather than a fix for the underlying Desktop-managed SSH project propagation behavior.
It changes the topology:
Instead of relying on Desktop to propagate a saved SSH project into Mobile, the SSH host itself becomes the remote-control environment that Mobile connects to directly.
That means the motivation for direct enrollment does not have to be tied to solving that particular product boundary; it can stand on its own as a cleaner deployment topology, even if the underlying product behavior remains unchanged.
One thing I found useful throughout this work was treating Desktop compatibility, Mobile enrollment compatibility, and remote execution routing as three independent boundaries. Fixing one did not imply the others were solved, and separating them made it much easier to understand where newer releases were actually introducing compatibility requirements.
Still an issue on Android->Windows->Linux, hopefully the slop posts above don't mean this issue gets ignored