Stale proxy in $CODEX_HOME/.env causes misleading macOS `os error 61`; clean home works

Open 💬 3 comments Opened Aug 16, 2026 by kairosgold

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_PROXY
  • HTTPS_PROXY
  • ALL_PROXY
  • NO_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).

  1. Start with a clean recovery CODEX_HOME: the request returns OK.
  2. Copy only the old .env into that working home. Do not copy any chat, database, config, cache, or runtime file.
  3. Run:
CODEX_HOME="/path/to/recovery-home" \
/Applications/ChatGPT.app/Contents/Resources/codex \
exec --skip-git-repo-check "Reply with exactly: OK"
  1. The exact original failure is reproduced: WebSocket Connection refused (os error 61), followed by failed HTTPS fallback.
  2. Move the .env out of the recovery home.
  3. 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:

  1. Fully quit the ChatGPT/Codex desktop app.
  2. Determine the active home:
CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
  1. Check whether $CODEX_HOME_DIR/.env exists.
  2. 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.
  3. For every loopback proxy address found, verify that the port is actually listening:
lsof -nP -iTCP:7890 -sTCP:LISTEN
  1. Back up the file, then remove or correct only the stale proxy configuration.
  2. 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 request
  • Connection 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:

  1. Inspect $CODEX_HOME/.env (default ~/.codex/.env).
  2. Enumerate HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY and lowercase equivalents without exposing credentials.
  3. Check whether every referenced 127.0.0.1/loopback proxy port has a listening process.
  4. Also inspect process/launch environment and macOS system proxy settings, but do not assume they are the only proxy sources.
  5. A/B test the same Codex binary with:
  • the affected CODEX_HOME
  • a clean temporary CODEX_HOME
  1. If the clean home works, compare small environment/config/runtime files before touching session databases.
  2. Never advise deleting ~/.codex without 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/**/*.jsonl
  • archived_sessions/*.jsonl
  • session_index.jsonl
  • state_5.sqlite
  • thread_history_1.sqlite
  • sqlite/codex-dev.db
  • .codex-global-state.json
  • history.jsonl
  • memories_1.sqlite and memories/

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.9 still returned OK after 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 (build 6570)
  • 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.com resolved into the Fake-IP range and routed through the TUN interface.
  • TLS reached chatgpt.com.
  • GET /backend-api/codex/responses reached OpenAI and returned the expected 405 Method Not Allowed with allow: POST.
  • Stable Codex 0.144.4 worked with a clean home.
  • The bundled 0.148.0-alpha.9 also worked with that same clean home.
  • Removing only config.toml did 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

  1. Include the effective proxy target (with credentials redacted) in transport diagnostics.
  2. Identify the source of the proxy setting, such as process environment vs. $CODEX_HOME/.env.
  3. 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.

  1. Add a diagnostics command/page that reports effective network configuration safely.
  2. Avoid presenting a failed local proxy hop only as an error for the final chatgpt.com URL.

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.

View original on GitHub ↗

3 Comments

kairosgold · 11 days ago

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/.env before changing DNS/TUN/VPN settings or reinstalling Codex. In this case it silently forced HTTP_PROXY, HTTPS_PROXY, and ALL_PROXY to an unmonitored 127.0.0.1:7890. Copying that 135-byte file alone into a clean working home reproduced the exact WebSocket + HTTPS os error 61; removing it immediately restored OK.

The old chats/projects were intact and were recovered separately. The product problem that remains is diagnostic clarity: Codex showed only the final chatgpt.com URL and never revealed that the refused connection was the stale local proxy hop.

kairosgold · 11 days ago

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/.env is 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/.env so future Codex processes would inherit it.

The likely lifecycle was:

  1. ClashX was in use and the working local HTTP proxy was 127.0.0.1:7890.
  2. An AI-assisted repair wrote fixed HTTP_PROXY, HTTPS_PROXY, and ALL_PROXY values into $CODEX_HOME/.env.
  3. That repair worked at the time.
  4. The proxy client was later changed to Clash Verge Rev, whose default mixed port is 7897.
  5. The static Codex .env was not managed by the proxy client and did not update.
  6. Codex kept dialing the old 7890 endpoint after nothing was listening there.
  7. A temporary repair silently became a persistent future failure.

Relevant defaults:

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

  1. Prefer process-scoped/session-scoped proxy variables for diagnosis.
  2. Before writing any persistent proxy setting, ask for confirmation and state exactly which file will be changed.
  3. Never assume a loopback proxy port will remain stable across proxy-client changes, profile imports, reinstalls, or upgrades.
  4. If persistence is necessary, record:
  • why it was added,
  • when it was added,
  • the originating proxy client,
  • how to remove it,
  • and whether it is temporary.
  1. On every startup, verify that the referenced loopback port is listening before forcing traffic through it.
  2. If the listener is absent, fail with an actionable warning or fall back to the system/TUN path instead of repeatedly retrying the dead endpoint.
  3. At the end of a repair task, list every persistent mutation made to .env, shell profiles, launchctl, system proxy settings, DNS, and app configuration.
  4. Revert diagnostic-only overrides after the test unless the user explicitly chooses to retain them.
  5. When a user says “it worked before I changed VPN/proxy software,” inspect all persistent agent-created overrides before changing DNS, TUN, nodes, authentication, or reinstalling applications.
  6. Do not place an undocumented fixed proxy inside the state directory of the application being diagnosed.

In short:

An agent-generated workaround should not outlive the condition it was created for without validation, provenance, and rollback.

The most valuable diagnostic question remains: Does $CODEX_HOME/.env pin Codex to a proxy port from a previous proxy client?

jdcodes1 · 11 days ago

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/.env is injected into the process environment at startup by load_dotenv() in codex-rs/arg0/src/lib.rs (L296-L306), before any threads or clients are created. It filters CODEX_* keys for safety but injects everything else — including HTTP_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:

  1. Announce proxy-affecting injections. The canonical proxy key list already exists in codex-rs/network-proxy/src/proxy.rs (PROXY_URL_ENV_KEYS and friends, L546-L600). If load_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.)
  1. Name the actual hop in connection errors. The reported error names only the destination (wss://chatgpt.com/... + os error 61), but the refused TCP connection was to 127.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_HOME and hope" into a first-glance diagnosis.