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

Open
#35,495 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
52/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
electron, javascript, node.js

Research direction

Start with the controller transport in app.asar's main-process /.vite/build/src-*.js chunk, where the ws WebSocket is created without an agent, and compare it with the electron.net.fetch calls and the separate socksProxyUrl transport. Reproduce behind a macOS system HTTP proxy, then verify the controller WebSocket uses the resolved proxy and reaches the waiting-for-device state instead of timing out.

Written by the indexing model from the issue text.

Description

app bug connectivity remote
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:

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. 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:

    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:

    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 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.
Dominant language
Rust
Stars
125k
Forks
19.5k
Avg merge
1m
Merged PRs (30d)
1k

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openai/codex

All issues in openai/codex

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.