macOS: "Control other devices" never connects behind an HTTP proxy — controller-side WebSocket ignores the system proxy

Open 💬 0 comments Opened Jul 26, 2026 by Ray-Frost

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?

  1. On macOS, configure an HTTP/HTTPS proxy as the system proxy (System Settings > Network > Proxies, or networksetup -setwebproxy / -setsecurewebproxy). Confirm with scutil --proxy.
  2. Ensure direct outbound TCP 443 to chatgpt.com is not available (restricted network, or block it with a local firewall) so that only the proxy provides connectivity.
  3. Launch [ChatGPT.app](<http://ChatGPT.app>). Sign in. Confirm the app is fully functional: chat, task list, and Settings > Connections correctly lists devices — all of this goes through the proxy.
  4. 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).
  5. From the first Mac, Settings > Connections > Control other devices → enable the connection to the second Mac.
  6. It stays on Initializing secure connection indefinitely.

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 connection is renderer i18n: settings.remoteConnections.deviceConnections.signedInDeviceInitializingSubtitle, in app.asar/webview/assets/remote-connections-settings-*.js.
  • Opening handshake has timed out is the ws package's own message (ws@8.18.3 per package.json), bundled into the main process at app.asar/.vite/build/src-*.js.
  • The controller transport's connection state machine is in that same main-process chunk:

``js
this.setConnectionProgress(
initializing); // UI: "Initializing secure connection"
await this.connectAndWaitForOpen(); // never resolves
this.setConnectionProgress(
waiting-for-device); // never reached
``

  • The WebSocket is opened with no agent and no createConnection, so it falls through to https.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 through electron.net.fetch, which is why enrollment succeeds and only the WebSocket fails.
  • There is a socksProxyUrl option on a different websocket transport class in the same bundle that would build a SocksProxyAgent, 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_proxy but works with HTTP_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.

View original on GitHub ↗