cursor+codex proxy troubleshooting

Fixing “Cannot connect to Codex in the Cursor extension” (Region / Proxy issue)

A step-by-step postmortem of why the Codex (OpenAI ChatGPT) extension could not log in inside a remote Cursor session, and how it was fixed. The goal is not just the fix but the principles, so you can debug similar problems yourself.


1. Symptom

  • The Codex panel in the Cursor extension could not connect / could not log in.
  • The machine is a Linux container accessed through Cursor Remote (the editor runs on your laptop, but a cursor-server process runs on the remote host and hosts all extensions).

2. TL;DR of the root cause

Two independent problems stacked on top of each other:

  1. Network egress was in mainland China, and OpenAI’s login endpoint rejects that region: 403 Forbidden ... unsupported_country_region_territory.
  2. Even after starting a local Clash proxy, the proxy never reached the Codex process. Codex is a native binary spawned deep inside the Cursor server process tree, and that tree had no proxy environment variables. Shell config (~/.zshrc) does not apply to it.

The durable fix: inject HTTPS_PROXY/HTTP_PROXY into the Codex process by wrapping its binary, so it egresses through the proxy regardless of how it is launched.


3. How the diagnosis was done (and why each step)

3.1 Read the extension’s own logs first

The single most useful artifact. Cursor writes per-extension logs here:

~/.cursor-server/data/logs/<timestamp>/exthost*/openai.chatgpt/Codex.log

The smoking gun in the log:

codex_login::server: oauth token exchange returned non-success status
status=403 Forbidden error_code="unsupported_country_region_territory"
error_message="Country, region, or territory not supported"

Plus repeated:

Error fetching error="TypeError: fetch failed" url=https://ab.chatgpt.com/...

Principle: distinguish the authentication failure (403 region) from the network failures (fetch failed). They point at different layers. The 403 tells you where your traffic appears to come from; the fetch failed tells you your traffic isn’t going through a working path at all.

3.2 Confirm the egress IP and region

curl -sS https://ipinfo.io/json

Output showed "country": "CN", "city": "Beijing". That directly explains the unsupported_country_region_territory — OpenAI geolocates the request to a blocked region.

3.3 Notice DNS poisoning as a second signal

getent hosts chatgpt.com
openssl s_client -servername chatgpt.com -connect chatgpt.com:443 </dev/null \
  | openssl x509 -noout -subject -ext subjectAltName

chatgpt.com resolved to a Facebook/Meta IPv6 address, and the TLS certificate returned was CN=*.facebook.com. That is classic DNS poisoning: direct connections can never succeed because you are not even talking to OpenAI’s servers. This is why a proxy (which resolves DNS on the exit node) is required, not optional.

3.4 Check whether a proxy is even configured for the process

This is the key insight for remote editors. Two things were checked:

# environment of the current shell
env | grep -i proxy

# environment of the ACTUAL running processes (this is what matters)
tr '\0' '\n' < /proc/<PID>/environ | grep -i proxy

/proc/<pid>/environ shows the environment a process was started with. The Codex process, the extension host, and server-main.js all had no proxy vars. That proved the proxy (even once running) was not being used by Codex.

Principle — environment inheritance: a process inherits environment variables from its parent at spawn time. You cannot change the environment of an already-running process. If the parent didn’t have HTTPS_PROXY, neither do the children, forever, until they are restarted with a new environment.

3.5 Map the process tree

ps -ef | rg 'cursor-server|extensionHost|codex app-server'

The tree was:

bash (137)                                  # persistent launcher in the container
└─ sh .../bin/cursor-server --start-server  # bootstrap wrapper script
   └─ node .../out/server-main.js  (290)    # THE server; long-lived
      └─ node ... --type=extensionHost      # extension host (restarts on Reload Window)
         └─ codex app-server                # the native Codex binary (Rust)

Principle: in Cursor/VS Code Remote, “Reload Window” only restarts the extension host, not server-main. So a proxy env added to a file only read at server-main startup will not take effect on a mere window reload.


4. Why the “obvious” fixes did NOT work here

Attempt Why it failed
export HTTPS_PROXY=... in a terminal Only affects that shell, not the already-running Codex/server processes.
Put proxy in ~/.zshrc The container’s default shell is bash, and cursor-server is launched non-interactively. ~/.zshrc (interactive zsh only) and ~/.bashrc (which early-returns with [ -z "$PS1" ] && return when non-interactive) are never sourced.
Cursor setting "http.proxy" Helps the extension’s JavaScript network calls (VS Code’s proxy-agent), but does not export env vars to a spawned native binary. Codex is native (Rust/reqwest) and reads HTTPS_PROXY from its own env, which stayed empty.
server-env-setup hook The documented VS Code hook to set server env; not supported in this Cursor build (grepping the server bundle found no reference).
Transparent proxy (Clash TUN / iptables REDIRECT) /dev/net/tun did not exist and the container lacked CAP_NET_ADMIN, so no system-wide redirect was possible.
“Reload Window” Restarts only the extension host, which re-inherits the unchanged server-main environment. server-main (pid 290) kept running for 40+ minutes across reloads.

Principle: always match the fix to the exact process that makes the network call and how that process gets its environment. For a native child process, the environment must be present in some ancestor at the moment it is spawned.


5. The fix that worked: wrap the Codex binary

Because we could not reliably control the environment of server-main (it is launched by a persistent parent whose env we cannot change, and it doesn’t source any rc file), the most robust point of injection is the Codex binary itself.

The extension always spawns:

.../extensions/openai.chatgpt-<ver>-linux-x64/bin/linux-x86_64/codex app-server ...

So we replace that path with a tiny wrapper that sets the proxy env and then execs the real binary. Any time Codex starts (including on a simple window reload), it now has the proxy — no server-main restart needed.

5.1 Steps

D=~/.cursor-server/extensions/openai.chatgpt-26.623.141536-linux-x64/bin/linux-x86_64

# 1) Keep the real ELF binary aside
mv "$D/codex" "$D/codex.real"

# 2) Put a wrapper in its place (see content below)
#    ...write $D/codex...

# 3) Make it executable
chmod 755 "$D/codex"

5.2 Wrapper content ($D/codex)

#!/bin/sh
# Inject the local Clash proxy so Codex (reqwest) egresses through it.
export HTTP_PROXY="http://127.0.0.1:7890"
export HTTPS_PROXY="http://127.0.0.1:7890"
export http_proxy="http://127.0.0.1:7890"
export https_proxy="http://127.0.0.1:7890"
export ALL_PROXY="socks5://127.0.0.1:7891"
export all_proxy="socks5://127.0.0.1:7891"
export NO_PROXY="localhost,127.0.0.1,::1"
export no_proxy="localhost,127.0.0.1,::1"

exec "$(dirname "$(readlink -f "$0")")/codex.real" "$@"

Why this works:

  • The kernel honours the #!/bin/sh shebang, so the extension can spawn the wrapper exactly as it spawned the binary.
  • exec replaces the shell with the real Codex process, so there is no extra process layer and signals/exit codes pass through unchanged.
  • readlink -f "$0" resolves the wrapper’s own directory, so codex.real is found regardless of the working directory.
  • Codex is built on Rust/reqwest, which reads HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY (both upper and lower case). We confirmed the binary contains these strings before choosing this approach: strings codex.real | grep -iE 'HTTPS?_PROXY'.

5.3 Supporting changes (belt and suspenders)

  1. Bootstrap script .../bin/linux-x64/<hash>/bin/cursor-server: add the same export lines before the final exec node ... server-main.js. This covers the whole tree if/when server-main is ever fully restarted. Backup kept as cursor-server.orig.
  2. Cursor setting .../data/Machine/settings.json: the existing value was "http.proxy": "socks5h://127.0.0.1:7890" — wrong, because 7890 is the HTTP proxy port, not the SOCKS port. Corrected to:
{
    "http.proxy": "http://127.0.0.1:7890",
    "http.proxyStrictSSL": false,
    "http.proxySupport": "on"
}

This fixes the extension’s own JS fetch calls (the TypeError: fetch failed log lines).

5.4 Clash port reference (from config.yaml)

Port Purpose
7890 HTTP proxy
7891 SOCKS5 proxy
7892 redir (transparent, needs iptables)
7893 mixed (HTTP + SOCKS)
9090 external controller (API)

Match the scheme to the port: http://
:7890, socks5://
:7891. Mismatching them (e.g. socks5h://
:7890) silently breaks the proxy.


6. Applying and verifying

  1. Make sure Clash is running and its exit node is in an allowed region:
curl -x http://127.0.0.1:7890 https://ipinfo.io/json     # expect HK/US/etc, not CN
curl -x http://127.0.0.1:7890 -o /dev/null -w '%{http_code}\n' \
     https://api.openai.com/v1/models                    # 401 = reachable & TLS OK
  1. Reload Window in Cursor (Developer: Reload Window). This re-spawns the extension host and therefore Codex, now through the wrapper.

  2. Verify the running Codex process actually has the proxy (note the process is now named codex.real):

tr '\0' '\n' < /proc/$(pgrep -f 'codex.real')/environ | grep -i proxy

You should see HTTPS_PROXY=http://127.0.0.1:7890. Then log in to Codex again; the unsupported_country_region_territory error should be gone.


7. Rollback

D=~/.cursor-server/extensions/openai.chatgpt-26.623.141536-linux-x64/bin/linux-x86_64
mv -f "$D/codex.real" "$D/codex"          # restore original binary
B=~/.cursor-server/bin/linux-x64/<hash>/bin
mv -f "$B/cursor-server.orig" "$B/cursor-server"   # restore bootstrap
# revert settings.json as desired

8. Caveats / maintenance

  • All three changes live inside ~/.cursor-server. A Cursor server upgrade or a Codex extension update will overwrite them (the version hash in the path changes). After an update, re-apply the wrapper (steps in §5.1–5.2).
  • The clean long-term alternative is a host that supports Clash TUN or iptables transparent proxy (needs /dev/net/tun + CAP_NET_ADMIN). Then all traffic egresses through the proxy and no per-process env injection is required.

9. General debugging checklist for “extension can’t connect”

  1. Read the extension log under ~/.cursor-server/data/logs/.../<ext-id>/.
  2. Separate auth/region errors from network/transport errors.
  3. Check the real egress IP (ipinfo.io) and DNS/TLS sanity (openssl s_client).
  4. Inspect the actual process environment via /proc/<pid>/environ, not just your shell.
  5. Map the process tree (ps -ef) to learn which process makes the call and which ancestor supplies its environment.
  6. Inject the fix at the narrowest process you control that is re-created on a cheap restart (here: wrap the binary; reload window instead of restarting the whole server).
  7. Verify by re-reading /proc/<pid>/environ after the restart.