Stale proxy in $CODEX_HOME/.env causes misleading macOS `os error 61`; clean home works
Confirmed root cause
This was ultimately caused by a stale proxy configuration in $CODEX_HOME/.env (default: ~/.codex/.env), not by Clash Verge Rev, DNS, TUN/Fake-IP, the proxy node, ChatGPT Plus authentication, database corruption, or a Codex binary regression.
The affected .env was only 135 bytes and defined:
HTTP_PROXYHTTPS_PROXYALL_PROXYNO_PROXY
The first three variables pointed to a loopback proxy on 127.0.0.1:7890. Nothing was listening on that port anymore. Codex therefore attempted to reach OpenAI through a dead local proxy and surfaced only the destination URL:
failed to connect to websocket: IO error: Connection refused (os error 61),
url: wss://chatgpt.com/backend-api/codex/responses
Falling back from WebSockets to HTTPS transport.
stream disconnected before completion: Connection refused (os error 61)
This was misleading because it looked like an OpenAI, WebSocket, VPN, DNS, or routing failure. The actual refused connection was the hidden local proxy hop.
Minimal causal reproduction
All tests used the same Mac, account, authentication, network, proxy node, application bundle, and bundled Codex binary (0.148.0-alpha.9).
- Start with a clean recovery
CODEX_HOME: the request returnsOK. - Copy only the old
.envinto that working home. Do not copy any chat, database, config, cache, or runtime file. - Run:
CODEX_HOME="/path/to/recovery-home" \
/Applications/ChatGPT.app/Contents/Resources/codex \
exec --skip-git-repo-check "Reply with exactly: OK"
- The exact original failure is reproduced: WebSocket
Connection refused (os error 61), followed by failed HTTPS fallback. - Move the
.envout of the recovery home. - Run the identical command again: it immediately returns
OK.
This establishes file-level causality. All discovered SQLite databases also passed integrity_check.
Resolution
Before deleting or replacing the entire Codex state directory:
- Fully quit the ChatGPT/Codex desktop app.
- Determine the active home:
CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
- Check whether
$CODEX_HOME_DIR/.envexists. - Inspect only the proxy-related keys locally. Do not paste the complete file into an issue or AI chat because proxy URLs can contain credentials.
- For every loopback proxy address found, verify that the port is actually listening:
lsof -nP -iTCP:7890 -sTCP:LISTEN
- Back up the file, then remove or correct only the stale proxy configuration.
- Relaunch Codex and test with the bundled CLI.
In this case, excluding the stale .env restored the current desktop app and bundled CLI without changing Clash, DNS, TUN, the OpenAI account, or the application version.
First checks for AI troubleshooting agents
If a user reports any of the following:
error sending requestConnection refused (os error 61)- repeated
Reconnecting... waiting for network - WebSocket failure followed by HTTPS fallback failure
- browser/curl works but Codex does not
Do these checks before recommending reinstalling the app, changing DNS, disabling TUN, switching proxy clients, rotating nodes, logging out, or deleting ~/.codex:
- Inspect
$CODEX_HOME/.env(default~/.codex/.env). - Enumerate
HTTP_PROXY,HTTPS_PROXY,ALL_PROXY,NO_PROXYand lowercase equivalents without exposing credentials. - Check whether every referenced
127.0.0.1/loopback proxy port has a listening process. - Also inspect process/launch environment and macOS system proxy settings, but do not assume they are the only proxy sources.
- A/B test the same Codex binary with:
- the affected
CODEX_HOME - a clean temporary
CODEX_HOME
- If the clean home works, compare small environment/config/runtime files before touching session databases.
- Never advise deleting
~/.codexwithout first making a complete backup; it contains local chats, projects, session indexes, and related state.
The highest-value first question is:
Does $CODEX_HOME/.env define a proxy, and is that local proxy port actually listening?
Recovering chats and projects safely
The clean-home test made old chats/projects disappear from the sidebar, but they were not deleted. They remained in the backed-up Codex home.
The durable data was found in:
sessions/**/*.jsonlarchived_sessions/*.jsonlsession_index.jsonlstate_5.sqlitethread_history_1.sqlitesqlite/codex-dev.db.codex-global-state.jsonhistory.jsonlmemories_1.sqliteandmemories/
A recovery lab was built from a known-good home, then user-data groups were migrated without the stale .env, old auth/config, locks, sockets, logs, caches, WAL/SHM sidecars, shell snapshots, or process-manager state.
Validated recovery result:
- 174/174 old session files recovered and parsed
- 103 active sessions
- 71 archived sessions
- 174 old thread records
- 11 projects
- 19 thread-to-project associations
- all active SQLite databases:
integrity_check=ok - the bundled
0.148.0-alpha.9still returnedOKafter every recovery group
For most affected users, simply backing up and removing/correcting the stale .env in the original home should preserve all chats and projects. The selective migration was used here only because a clean home had already been installed during troubleshooting.
Environment
- ChatGPT/Codex Desktop:
26.810.41047(build6570) - Bundle ID:
com.openai.codex - Bundled Codex CLI:
0.148.0-alpha.9 - Subscription: ChatGPT Plus
- Platform: macOS, Apple Silicon (
arm64) - Proxy client: Clash Verge Rev
Why the earlier diagnostics were misleading
The affected network path itself was healthy:
- Clash Verge Rev TUN + Global mode was working.
chatgpt.comresolved into the Fake-IP range and routed through the TUN interface.- TLS reached
chatgpt.com. GET /backend-api/codex/responsesreached OpenAI and returned the expected405 Method Not Allowedwithallow: POST.- Stable Codex
0.144.4worked with a clean home. - The bundled
0.148.0-alpha.9also worked with that same clean home. - Removing only
config.tomldid not help.
Those results correctly pointed to persistent state outside config.toml, but the error message did not reveal the effective proxy source or the refused local proxy address.
Requested product improvements
- Include the effective proxy target (with credentials redacted) in transport diagnostics.
- Identify the source of the proxy setting, such as process environment vs.
$CODEX_HOME/.env. - When a loopback proxy returns
ECONNREFUSED, show an actionable message such as:
Local proxy 127.0.0.1:7890 is not listening; check $CODEX_HOME/.env.
- Add a diagnostics command/page that reports effective network configuration safely.
- Avoid presenting a failed local proxy hop only as an error for the final
chatgpt.comURL.
A 135-byte stale .env caused hours of unnecessary investigation across DNS, TUN, WebSockets, versions, authentication, nodes, and reinstall attempts. Surfacing the effective proxy would make this failure immediately actionable.
3 Comments
Root cause confirmed and the issue body has been rewritten with the minimal reproduction, recovery results, and an AI-agent-first diagnostic checklist.
TL;DR: check
$CODEX_HOME/.envbefore changing DNS/TUN/VPN settings or reinstalling Codex. In this case it silently forcedHTTP_PROXY,HTTPS_PROXY, andALL_PROXYto an unmonitored127.0.0.1:7890. Copying that 135-byte file alone into a clean working home reproduced the exact WebSocket + HTTPSos error 61; removing it immediately restoredOK.The old chats/projects were intact and were recovered separately. The product problem that remains is diagnostic clarity: Codex showed only the final
chatgpt.comURL and never revealed that the refused connection was the stale local proxy hop.Important follow-up: the likely origin was an earlier AI-agent “network repair”
The file-level root cause is proven, but the exact process that originally created
$CODEX_HOME/.envis not available from filesystem metadata, so the following origin is a strongly supported timeline rather than a process-audit fact.The user recalls previously asking a Codex agent to repair repeated network reconnections. The agent likely discovered the then-active local proxy and persisted it into
~/.codex/.envso future Codex processes would inherit it.The likely lifecycle was:
127.0.0.1:7890.HTTP_PROXY,HTTPS_PROXY, andALL_PROXYvalues into$CODEX_HOME/.env.7897..envwas not managed by the proxy client and did not update.7890endpoint after nothing was listening there.Relevant defaults:
7890: https://github.com/yichengchen/clashX7897: https://github.com/clash-verge-rev/clash-verge-rev/blob/dev/src-tauri/src/config/verge.rsThis also explains why the browser, system proxy/TUN traffic, and the same proxy service on Windows could work while only Codex on this Mac failed: only this Mac's Codex home contained the stale private override.
Lesson for AI agents performing network repair
A localhost proxy port is runtime state, not a stable permanent constant. An agent should not turn a currently discovered port into a persistent configuration without lifecycle handling.
Recommended guardrails:
.env, shell profiles,launchctl, system proxy settings, DNS, and app configuration.In short:
The most valuable diagnostic question remains: Does
$CODEX_HOME/.envpin Codex to a proxy port from a previous proxy client?Excellent root-cause work. Adding the code-side pointers for anyone implementing the prevention, since the confusing part — "why does a clean-looking environment still use a proxy?" — has a specific mechanism in this repo.
Where the hidden hop comes from.
~/.codex/.envis injected into the process environment at startup byload_dotenv()incodex-rs/arg0/src/lib.rs(L296-L306), before any threads or clients are created. It filtersCODEX_*keys for safety but injects everything else — includingHTTP_PROXY/HTTPS_PROXY/ALL_PROXY— with no trace in logs. That's why shell-level debugging never shows the variable's origin: it doesn't exist outside the Codex process.Two cheap fixes that would have made this diagnosable in minutes:
codex-rs/network-proxy/src/proxy.rs(PROXY_URL_ENV_KEYSand friends, L546-L600). Ifload_dotenv()logged one line when a loaded key intersects that list — "loaded HTTPS_PROXY from ~/.codex/.env" — the misleading-failure class disappears. (Log the key name only, never the value: proxy URLs can embed credentials, as the issue notes.)wss://chatgpt.com/...+os error 61), but the refused TCP connection was to127.0.0.1:7890. At error-construction time the client can check the standard proxy env vars and append "(via proxy configured in HTTPS_PROXY)" to connection-refused/timeout errors. That single suffix redirects the whole debugging session from DNS/VPN/OpenAI-status speculation to the real target.Both are small, and together they convert this failure mode from "wipe
CODEX_HOMEand hope" into a first-glance diagnosis.