Repository navigation
Codex mobile pairing can get stuck when local global state keeps revoked client/environment IDs #23112
Description
Summary
Codex Desktop mobile pairing can get stuck in a stale state after a failed or revoked mobile enrollment. In my case, logging out and back in on the phone, restarting both apps, and enabling MFA did not recover the connection.
The desktop setup page stayed on "Approve on mobile device" with a QR code, while the iPhone stayed on the "Open Codex" / security setup flow and never completed pairing.
The underlying problem appears to be that Codex Desktop kept local remote-control enrollment state in ~/.codex/.codex-global-state.json even after the corresponding cloud-side client/environment records had been removed or no longer existed.
Environment
- Codex Desktop:
26.513.31313 - Local app server observed during debugging:
0.131.0-alpha.9 - ChatGPT iOS app:
1.2026.125 - Host OS: macOS
- Phone: iPhone / iOS
- MFA: enabled before the final retry
Symptoms
- Desktop shows the Codex mobile setup QR code and waits at "Approve on mobile device".
- iPhone ChatGPT/Codex flow remains stuck around "Open Codex" / security setup.
- Reinstalling/restarting the mobile app did not fix it.
- Logging out and back in on the mobile app did not fix it.
- Restarting Codex Desktop did not fix it.
- Enabling MFA did not fix it.
Diagnostics
Using the existing Codex Desktop auth token, I inspected the remote-control backend state:
GET /backend-api/wham/remote/control/clients?limit=100GET /backend-api/codex/remote/control/environments?limit=100GET /backend-api/wham/remote/control/mfa_requirement
At first, the account had many stale phone/tablet clients in pending_enrollment plus one enrolled desktop client. The account also had a Codex Desktop remote environment.
After deleting the pending mobile clients and the old desktop client/environment, the cloud-side state was clean:
clients = 0environments = 0mfa_requirement = required
The access token already reflected MFA (amr included MFA/OTP), so MFA itself did not appear to be the remaining blocker.
However, Codex Desktop still had stale local remote-control state in:
~/.codex/.codex-global-state.json
Specifically, the file still contained keys like:
electron-remote-control-client-enrollments
electron-local-remote-control-environment-id
electron-local-remote-control-installation-id
Those values pointed at the old client/environment IDs that no longer existed server-side.
Workaround
The workaround was to clear the local remote/mobile global-state keys after fully quitting Codex Desktop.
Important: do not edit this file while Codex is still running, because Codex may write the old in-memory state back on quit.
# 1. Quit Codex Desktop completely first, e.g. Cmd+Q.
# 2. Confirm the main app process is gone.
ps -axo pid,args | grep '/Applications/Codex.app/Contents/MacOS/Codex'
# 3. Back up and remove only the local mobile/remote pairing keys.
STATE="$HOME/.codex/.codex-global-state.json"
cp "$STATE" "$STATE.mobile-pairing-backup.$(date +%Y%m%d%H%M%S)"
node <<'NODE'
const fs = require("fs");
const path = require("path");
const file = path.join(process.env.HOME, ".codex", ".codex-global-state.json");
const state = JSON.parse(fs.readFileSync(file, "utf8"));
for (const key of [
"electron-remote-control-client-enrollments",
"electron-local-remote-control-environment-id",
"electron-local-remote-control-installation-id",
"codex-mobile-setup-completed",
]) {
delete state[key];
}
const atomState = state["electron-persisted-atom-state"];
if (atomState && typeof atomState === "object" && !Array.isArray(atomState)) {
for (const key of Object.keys(atomState)) {
const lower = key.toLowerCase();
if (
lower.includes("remote-control") ||
lower.includes("remote_control") ||
lower.includes("codex-mobile")
) {
delete atomState[key];
}
}
}
fs.writeFileSync(file, JSON.stringify(state, null, 2) + "\n");
NODEThen reopen Codex Desktop, start the Codex mobile setup again, and scan a fresh QR code from the iPhone.
Expected behavior
Codex Desktop should detect when the locally stored remote-control enrollment references a server-side client/environment that no longer exists, then automatically discard the stale local pairing state and start a fresh enrollment.
At minimum, the mobile setup UI should provide a clear Reset mobile pairing state or Repair mobile setup action that clears:
- stale pending mobile clients
- stale local desktop client enrollment
- stale local environment/installation IDs
Why this matters
The current failure mode is hard for users to recover from because all obvious steps look successful but do not actually reset the broken state:
- mobile logout/login
- desktop restart
- mobile reinstall/restart
- MFA enablement
The UI gives the impression that the user still needs to approve on the phone, but the phone and desktop are effectively waiting on incompatible/stale pairing state.
Activity
github-actions commentedon May 17, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
jackljp commentedon May 17, 2026
Adding a follow-up with the part that I think makes this report useful even if the user-visible symptom overlaps with #22700, #22714, #22770, #22781, #23055, and #23078.
I agree this is in the same bug family: revoked/stale Codex Mobile remote-control pairing prevents a fresh QR pairing. The non-duplicate/additional finding here is the exact layer where the stale state remained in my case.
What is different in this report
The server-side remote state was verified clean.
After using backend cleanup for stale records, both of these authenticated endpoints returned empty lists:
GET /backend-api/wham/remote/control/clients?limit=100->count = 0GET /backend-api/codex/remote/control/environments?limit=100->count = 0So this was not only the mobile app showing a stale backend host/environment.
MFA was not the remaining blocker.
MFA had been enabled and the refreshed local token included MFA/OTP auth method claims. The API still returned
mfa_requirement = required, but the setup flow remained stuck even with MFA satisfied.The stale record was still present in Codex Desktop local global state.
Even with cloud clients/environments empty,
~/.codex/.codex-global-state.jsonstill contained local remote-control pairing state:Those values referenced the previous desktop client/environment, which no longer existed server-side.
Clearing those Desktop global-state keys resolved the pairing issue in this case.
The important detail is that Codex Desktop must be fully quit before clearing the keys. If the file is edited while the app is still running, the app can write the stale in-memory state back on quit.
This appears distinct from the app-server SQLite enrollment recovery path.
The open-source app-server already has recovery for stale persisted remote-control enrollments when the WebSocket returns HTTP 404:
codex/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs
Lines 1148 to 1170 in 4c89772
There is also a test for that path:
codex/codex-rs/app-server-transport/src/transport/remote_control/tests.rs
Lines 1387 to 1470 in 4c89772
In other words, the missing cleanup in this case looks more like Desktop/Electron global-state reconciliation than only app-server
remote_control_enrollmentscleanup.Suggested fix direction
On the Desktop mobile setup path, Codex could validate the local Desktop global-state enrollment/environment against the current backend state. If the locally stored client/environment no longer exists, is revoked, or cannot be used to complete setup, Desktop should clear the local pairing state and start a fresh enrollment.
At minimum, the UI could expose a Reset/Repair Codex Mobile pairing action that clears:
This would make the recovery path explicit instead of requiring users to discover and edit
~/.codex/.codex-global-state.jsonmanually.lubluniky commentedon May 18, 2026
https://x.com/borkiss727/status/2056521742550536681?s=20
easy fix via ssh + tailscale + ChatGPT app mobile
Jarvis-Baba commentedon Jun 5, 2026
This stale-state description matches what I am seeing, but with Windows Desktop + ChatGPT iOS.
The machine had paired successfully before. After the phone/device entry was accidentally deleted from Codex Desktop, the desktop Connections page now shows no controlling devices, while the iOS app still keeps the old Codex Desktop host record as disconnected/offline.
After that, a fresh QR scan does not seem to create a fresh enrollment. The mobile flow still appears to reuse the old/offline host record, and attempting setup ends with:
Setup failed / Couldn't finish secure setup. Try again(Chinese UI:设置失败 / 无法完成安全设置。请重试。).So the broken state seems to be:
This would be much easier to recover from if Codex mobile exposed a remove/forget action for stale hosts, or if scanning a new QR invalidated/replaced the old host enrollment when the desktop no longer recognizes the previous phone pairing.
joinnpengfei-wq commentedon Jul 14, 2026
Adding a reproducible follow-up from a newer Desktop/iOS build. The local stale-state workaround described above was extended all the way to a genuinely new installation and environment, but QR pairing still failed at the claim/binding boundary.
Environment
26.707.71524(build5263)0.144.215.3(24D2059), Apple Silicon1.2026.132; backend-reported iOS26.5mfa_requirement = suggested(not required)What was reset and verified
.codex-global-state.json.remote_control_enrollmentsrows instate_5.sqlite;PRAGMA integrity_check = ok.~/.codex/installation_id(an additional stable identity source not covered by the original workaround).localhostenvironment then became offline; the newbogonenvironment remained online.Result
remoteControl/pairing/startsucceeded at2026-07-14T05:18:02.100Z.remoteControl/client/listrepeatedly through05:19:54.497Z.paired_client_count = 0.last_seen_atdid not advance during the attempt.claimedresult was observed.stdio; nocodex_chatgpt_ios_remote, no remote-control RPC transport, and no iOS-originatedthread/listreached the host.This rules out the old installation ID, old environment ID, SQLite enrollment, conflicting sidecar, host availability, and MFA requirement as the remaining cause. The failing boundary appears to be the iOS QR/deep-link secure-setup flow not creating the backend environment-to-client binding. Environment discovery succeeds, but secure claim does not.
This also explains the skeleton symptom discussed in #22762, but here the failure occurs one step earlier: the new environment never gains a paired client at all.
Questions for maintainers:
~/.codex/installation_idwhen a truly fresh host identity is required?Raw logs, reset logs, and the screenshot are retained locally and can be provided in redacted form through an appropriate private channel.
iM3SK commentedon Aug 20, 2026
Windows + Android + multi-account corroboration
I can reproduce the same claim/binding failure boundary on Windows with ChatGPT for Android. This is not just a generic network or sign-in failure.
Environment
OpenAI.Codex 26.814.5517.0OpenAI.CodexBeta 26.727.4816.0Observed behavior
The Android pairing/Remote Control flow does not become usable. The device can appear paired at the UI/account level, but the currently authenticated desktop environment has no bound mobile client.
Instrumented Beta trace:
remoteControl/pairing/startwas dispatched twice.errorCode=null(about 1.86 s and 1.32 s).remoteControl/client/list.previousConnectionCount=0nextConnectionCount=0autoConnectConnectionCount=0The host transport itself is healthy:
Connecting -> Connected.codex.exeprocess had established TCP/443 connections tochatgpt.com.Allowrules.Multi-account state evidence
The shared
.codex/state_5.sqlitecontained two enabled rows inremote_control_enrollments:wss://chatgpt.com/backend-api/wham/remote/control/serverendpointaccount_idvaluesremote_control_enabled=1The current desktop auth account matched the newer enrollment. The older account enrollment remained persisted.
The older enrollment also produced seven consecutive WebSocket failures with:
The current enrollment later connected successfully, but the mobile client list still remained empty. This separates the historical ownership conflict from the current claim/binding failure.
A/B observations performed
A — Stable vs Beta
pairing/start succeeds -> client/list stays emptybehavior remained.B — transport vs pairing layer
The mobile-side account/session could not be inspected directly because Android debugging was not available, so I cannot distinguish from host evidence alone between (1) Android retaining the old account/environment binding and (2) the secure claim flow failing to create a new binding. Both produce the verified empty current client list.
Suggested regression matrix
Remote app server already online, a UI-visible stale pairing, orclient/list = 0after a successful pairing response.Product/UX request
Multi-account use on one device is already a supported ChatGPT web workflow ("Add another account"). Remote Control should remain security-isolated per account/workspace, but Desktop should handle that workflow explicitly:
client/listis empty;This is not a request to allow cross-account control. It is a request for correct isolation, visible account ownership, and automatic recovery.
Related reports that cover separate dimensions of this reproduction:
409 Remote app server already onlinemeirion commentedon Sep 7, 2026
Check system clock times on the computer and phone. I had this error on a phone and computer which had time set manually (2 mins fast). Changing both to automatic time fixed the Codex mobile pairing error immediately.