macOS: "Control other devices" never connects behind an HTTP proxy — controller-side WebSocket ignores the system proxy
What version of the Codex App are you using (From "About Codex" dialog)?
26.721.41059 (bundled Contents/Resources/codex 0.146.0)
What subscription do you have?
Business / Team
What platform is your computer?
macOS 26.5.2 (build 25F84), arm64. Electron 42.3.0 / Chromium 150.0.7871.128 / Node 24.14.0.
What issue are you seeing?
On a network where direct egress to chatgpt.com is unavailable and all traffic must go through a local HTTP proxy configured as the macOS system proxy, every part of the app works except Settings > Connections > Control other devices.
The target device sits permanently on Initializing secure connection, occasionally flashing Opening handshake has timed out before returning to the same state, retrying forever.
What works and what doesn't, on the same machine at the same time:
<!-- linear:table-colwidths:266,266,266 -->
| Path | Stack | Result |
| -- | -- | -- |
| All desktop REST API calls (/wham/..., device list, enroll) | electron.net.fetch → Chromium | ✅ honours system proxy |
| Renderer / webview page loads | Chromium | ✅ honours system proxy |
| Being controlled (wss://chatgpt.com/backend-api/wham/remote/control/server) | Rust Contents/Resources/codex, reads http_proxy | ✅ connects |
| Controlling another device (wss://chatgpt.com/backend-api/codex/remote/control/client) | ws npm package in the Electron main process | ❌ times out |
The controller-side WebSocket appears to be the only network path in the main process that does not go through Chromium's network stack, and it is given no proxy agent, so it lands on Node's https.request — which honours neither the macOS system proxy nor http_proxy/https_proxy.
Enabling a network-layer TUN/VPN-style interception makes it connect immediately, which is consistent with this diagnosis: only interception below the socket layer can help a connection that never consults any proxy configuration.
What steps can reproduce the bug?
- On macOS, configure an HTTP/HTTPS proxy as the system proxy (
System Settings > Network > Proxies, ornetworksetup -setwebproxy/-setsecurewebproxy). Confirm withscutil --proxy. - Ensure direct outbound TCP 443 to
chatgpt.comis not available (restricted network, or block it with a local firewall) so that only the proxy provides connectivity. - Launch [ChatGPT.app](<http://ChatGPT.app>). Sign in. Confirm the app is fully functional: chat, task list, and
Settings > Connectionscorrectly lists devices — all of this goes through the proxy. - Enable "Control this Mac" on a second Mac signed into the same account, and confirm the pairing is healthy (e.g. controlling that Mac from the ChatGPT iOS app works).
- From the first Mac,
Settings > Connections > Control other devices→ enable the connection to the second Mac. - It stays on
Initializing secure connectionindefinitely.
What is the expected behavior?
The controller-side remote-control WebSocket should use the same proxy configuration as the rest of the app. Since every other main-process request already resolves proxy settings through Chromium, the WebSocket should too.
Additional information
Evidence that the connection is made directly, bypassing the proxy
lsof -a -i -n -P -p <main process pid> while the UI is stuck shows exactly one socket on the Electron main process, going straight out with no proxy involved:
COMMAND PID FD TYPE ... NAME
ChatGPT 75363 190u IPv4 ... TCP 192.168.x.x:62430->A.B.C.D:443 (SYN_SENT)
Zero connections from that PID to the proxy's loopback port. At the same moment, on the same machine:
codex 75867 30u IPv4 TCP 127.0.0.1:62166->127.0.0.1:<proxy port> (ESTABLISHED)
codex 75867 31u IPv4 TCP 127.0.0.1:62393->127.0.0.1:<proxy port> (ESTABLISHED)
codex 75867 32u IPv4 TCP 127.0.0.1:62337->127.0.0.1:<proxy port> (ESTABLISHED)
— the Rust binary goes through the proxy correctly, and the Chromium network service process likewise has multiple established connections to the proxy port. Polling lsof alongside DNS resolution confirmed the direct SYN_SENT target always equals the current DNS answer for chatgpt.com, i.e. the main process resolves and dials chatgpt.com:443 itself instead of handing the hostname to the proxy.
Where it comes from in the shipped bundle
Initializing secure connectionis renderer i18n:settings.remoteConnections.deviceConnections.signedInDeviceInitializingSubtitle, inapp.asar→/webview/assets/remote-connections-settings-*.js.Opening handshake has timed outis thewspackage's own message (ws@8.18.3perpackage.json), bundled into the main process atapp.asar→/.vite/build/src-*.js.- The controller transport's connection state machine is in that same main-process chunk:
``jsinitializing
this.setConnectionProgress(); // UI: "Initializing secure connection"waiting-for-device
await this.connectAndWaitForOpen(); // never resolves
this.setConnectionProgress(); // never reached``
- The WebSocket is opened with no
agentand nocreateConnection, so it falls through tohttps.globalAgent:
``js``
const socket = new WebSocket(websocketUrl, {
headers, // x-codex-client-id, x-codex-protocol-version, auth
handshakeTimeout, // 10_000
perMessageDeflate: false,
});
The 10 s handshakeTimeout matches the observed cadence exactly: \~10 s on Initializing secure connection, a brief Opening handshake has timed out, then retry.
- For contrast, the controller-side REST calls in the same file (
/codex/remote/control/client/enroll/start|finish,/refresh/start|finish) go throughelectron.net.fetch, which is why enrollment succeeds and only the WebSocket fails. - There is a
socksProxyUrloption on a different websocket transport class in the same bundle that would build aSocksProxyAgent, but nothing in the bundle ever assigns it, and it is not on the remote-control controller path.
Why environment variables don't help
NODE_USE_ENV_PROXY=1 with HTTPS_PROXY set does work on stock Node 24.14.0 — verified using the Node binary shipped inside the app bundle:
$ ./Contents/Resources/cua_node/bin/node -e '<https.request to chatgpt.com>'
TIMEOUT 10021ms
$ NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://127.0.0.1:<port> \
./Contents/Resources/cua_node/bin/node -e '<same request>'
status 403 698ms
But launching [ChatGPT.app](<http://ChatGPT.app>) itself with NODE_USE_ENV_PROXY=1 + HTTPS_PROXY changes nothing — the main process still dials out directly (verified via lsof, and the launched process's parent was the shell, so the variables did reach it). So Electron does not appear to wire up Node's env-proxy support.
features.respect_system_proxy and network_proxy exist only in the Rust binary — zero occurrences in app.asar — so they cannot affect this path either.
Suggested fix
Give the controller-side WebSocket an agent derived from the app's existing proxy configuration, e.g. resolve via session.resolveProxy(websocketUrl) (which already reflects the system proxy, PAC, and any --proxy-server) and pass a matching HttpsProxyAgent/SocksProxyAgent as ws's agent option. RFC 6455 §4.1 specifies proxy traversal via CONNECT for wss://, and the proxy handles it fine — codex doctor reports HTTP 101 Switching Protocols for a proxied WebSocket handshake on this same machine. The gap is only that no agent is supplied.
Related issues
- openai/codex#29958 — Windows: WebSocket transport times out with
respect_system_proxybut works withHTTP_PROXY/HTTPS_PROXY. Same class of defect (HTTP honours the system proxy, WebSocket does not) on a different platform and layer. - openai/codex#27231 — macOS: Mac-to-Mac remote control fails in both directions while iPhone → Mac works. Same asymmetry this report explains: the iOS path exercises the Mac's proxy-aware server role, while Mac-to-Mac requires the controller role.
- openai/codex#34955 — macOS: app-side network paths not picking up proxy configuration. Adjacent, but distinct: in this report Chromium does correctly use the system proxy, and only the Node-side WebSocket does not.
- openai/codex#29233 — Windows: remote control only works with global proxy environment variables.