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-serverprocess 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:
- Network egress was in mainland China, and OpenAIâs login endpoint rejects
that region:
403 Forbidden ... unsupported_country_region_territory. - 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/shshebang, so the extension can spawn the wrapper exactly as it spawned the binary. execreplaces 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, socodex.realis found regardless of the working directory.- Codex is built on Rust/
reqwest, which readsHTTP_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)
- Bootstrap script
.../bin/linux-x64/<hash>/bin/cursor-server: add the sameexportlines before the finalexec node ... server-main.js. This covers the whole tree if/whenserver-mainis ever fully restarted. Backup kept ascursor-server.orig. - Cursor setting
.../data/Machine/settings.json: the existing value was"http.proxy": "socks5h://127.0.0.1:7890"â wrong, because7890is 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
- 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
-
Reload Window in Cursor (
Developer: Reload Window). This re-spawns the extension host and therefore Codex, now through the wrapper. -
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â
- Read the extension log under
~/.cursor-server/data/logs/.../<ext-id>/. - Separate auth/region errors from network/transport errors.
- Check the real egress IP (
ipinfo.io) and DNS/TLS sanity (openssl s_client). - Inspect the actual process environment via
/proc/<pid>/environ, not just your shell. - Map the process tree (
ps -ef) to learn which process makes the call and which ancestor supplies its environment. - 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).
- Verify by re-reading
/proc/<pid>/environafter the restart.
