Codex Desktop 代理配置实践
最近用 Codex Desktop 写代码蹬得很爽。唯一不爽的是,Codex Desktop 设置里没有提供代理设置,需要代理软件开 TUN 模式才能正常工作。尽管 TUN 模式可以透明接管网络,使用起来几乎无感,但我不喜欢。代理设置成配置系统代理,Codex 是可以工作的,但我也不喜欢。我希望代理的工作环境是:
- 不打开 TUN。
- 不配置 Windows 系统代理。
- 尽量只让 Codex 相关进程走本地代理
http://127.0.0.1:10808
这当然是可以的。尽管 Codex Desktop 没有显示提供代理设置,这不意味着它不进行任何代理规则处理。好在 Codex 是开源的 coding agent,我们可以让 Codex 研究自己的代码,从源码一窥究竟。
这篇文章先探讨 Codex 网络相关请求,再分析相应的代理设置,做出最适合我们的选择。
1. Codex App 里最重要的两类请求
为了讨论代理,不需要把 Codex Desktop 的每个接口都拆开。真正影响配置选择的,其实是两类请求:
1
2
3
4
5
6
7
App UI 请求
Electron / Chromium 发起
影响登录、账号、Settings、Usage、OAuth 页面
会话请求
Rust app-server / Codex CLI 发起
影响对话、模型流、auth refresh、rate limits、子进程和 MCP
Codex Desktop 可以粗略看成这样的结构:
1
2
3
4
5
6
7
Codex.exe
Electron / Chromium 桌面壳
-> App UI 请求
resources/codex.exe app-server
Rust app-server
-> 会话请求
Codex CLI 更简单一些。CLI 没有 Electron / Chromium 这层 App UI,请求基本都落在 Rust 侧,也就是上面的“会话请求”。换句话说,Desktop 的 Rust app-server 和 Codex CLI 共享了同一类后端请求路径:模型请求、auth refresh、rate limits、子进程等都属于这一层。
1.1 App UI 请求
App UI 请求由 Desktop 前端或 Chromium NetworkService 发起,常见于:
- 登录页、账号菜单。
- Settings 里的 Account / Usage。
- 用量入口。
- connector / OAuth 授权页面。
- 外部 ChatGPT/account/billing 页面。
从源码关系看,这一层属于 Electron/Chromium 网络栈,和 Rust app-server / Codex CLI 使用的后端请求路径不同。也就是说,App UI 请求要按桌面壳这一层来处理,而不是按 Rust 后端来处理。
1.2 会话请求
会话请求由 Rust app-server 或 Codex CLI 发起。它覆盖的是 Codex 真正干活的那部分:
- 模型对话。
- auth refresh。
- rate limits / usage 后端读取。
- 插件、marketplace、apps 的后端请求。
- Codex CLI。
- shell、MCP、插件子进程。
从源码结构看,这一层和 App UI 请求不同:它不经过 Electron/Chromium 的前端网络栈,而是由 Rust 进程里的网络客户端发起。Desktop 的 Rust app-server 和 Codex CLI 都会落到这一类请求里。
模型对话内部还有一个很关键的 fallback 机制。默认情况下,Codex 会话请求会优先使用 Responses WebSocket:
1
wss://chatgpt.com/backend-api/codex/responses
如果 WebSocket 连接失败、断流或超时,Codex 不会立刻放弃这一轮会话,而是先按会话重试策略继续尝试。默认重试次数是 5 次。重试耗尽后,会禁用当前 session 的 WebSocket,并 fallback 到 HTTPS/SSE:
1
2
POST https://chatgpt.com/backend-api/codex/responses
Accept: text/event-stream
所以会话请求内部其实有两条传输路径:
1
2
优先:Responses WebSocket
失败:HTTPS/SSE fallback
这两条路径都属于会话请求,不属于 App UI 请求。后面分析不同代理方式覆盖范围时,这个 fallback 机制非常关键。
1.3 localhost 要绕过代理
还有一类不是外部请求,但很重要:Desktop UI 和本地 app-server 之间要通过 localhost / 127.0.0.1 通信。这条本地控制通道不应该走代理。
所以无论采用哪种方案,都要确保本机地址被绕过。具体怎么配置,放到后面的方案对比里展开。
也可以把整个问题简化成一句话:
会话请求走 Rust 后端 / Codex CLI,App UI 请求走 Electron / Chromium,localhost 必须绕过代理。
2. 不同代理方式的覆盖范围
下面按几种常见网络配置逐一对比。重点不是评判哪种方式绝对更好,而是看它分别覆盖 App UI 请求、会话请求和 localhost 通信中的哪一部分。
2.1 不开代理直连
如果当前网络可以直接访问 chatgpt.com、auth.openai.com、api.openai.com,不开代理当然也可以正常使用。
但在需要代理的网络环境里,直连通常表现为:
| 请求类型 | 预期结果 |
|---|---|
| App UI 请求 | 失败或一直转圈 |
| 会话 / CLI 请求 | 取决于直连,通常不稳定 |
| localhost 通信 | 正常 |
这个模式下没有哪个代理层会帮 Codex 接管网络。所有外部请求都依赖直连。
2.2 TUN
TUN 是我一开始使用的方案。它是透明代理,作用在更底层的网络路由上。在 TUN 规则覆盖的情况下,应用不需要知道代理存在,https、wss、CLI 子进程、Chromium 请求通常都会被系统网络层接管。
预期结果:
| 请求类型 | 预期结果 |
|---|---|
| App UI 请求 | 正常 |
| 会话 / CLI 请求 | 正常 |
| localhost 通信 | 取决于 TUN 规则,不应被错误接管 |
TUN 的覆盖范围最完整,接近“让网络环境本身变好”。缺点也明显:它通常是全局影响,不符合“只让 Codex 走代理”的目标。
如果只追求省心,TUN 很强。这也是为什么一开始我用 TUN 时 Codex 基本可用。但如果想精确控制影响范围,尤其不想影响 WSL、Docker 和其他应用,TUN 就不够克制。
2.3 设置 Windows 系统代理
系统代理会明显改善 Desktop UI 请求,因为 Electron/Chromium 通常会读取系统代理。
预期结果:
| 请求类型 | 预期结果 |
|---|---|
| App UI 请求 | 大概率正常 |
| 会话 / CLI 请求 | 可能先尝试 WebSocket 并 reconnect;失败后再 fallback 到 HTTPS/SSE,之后对话成功 |
| localhost 通信 | 通常正常,但仍建议 bypass |
我们之前观察到的现象是:设置系统代理后,对话会先 Reconnecting... 5 次,然后能连上。
这和源码逻辑吻合:
1
2
3
4
5
默认先走 Responses WebSocket
-> WebSocket 没走通
-> 默认重试 5 次
-> fallback 到 HTTPS/SSE
-> HTTPS/SSE 走可用代理路径后成功
也就是说,系统代理能解决很多 UI 问题,但它不是对所有 Rust 网络路径和子进程都可靠。尤其是 WebSocket 和 CLI 工具这类请求,能不能吃到系统代理取决于具体库和工具实现。
2.4 ~/.codex/.env 配置代理
.env 是很多人第一时间会想到的方式:
HTTP_PROXY=http://127.0.0.1:10808
HTTPS_PROXY=http://127.0.0.1:10808
ALL_PROXY=http://127.0.0.1:10808
NO_PROXY=localhost,127.0.0.1,::1
这会被 Rust codex.exe 入口加载,因此它主要影响 Rust app-server 和它启动的子进程。
预期结果:
| 请求类型 | 预期结果 |
|---|---|
| App UI 请求 | 不覆盖,Usage 仍可能失败或转圈 |
| 会话 / CLI 请求 | 正常概率高 |
| localhost 通信 | 正常,前提是 NO_PROXY 配好 |
这就是我们实际遇到的状态:
1
2
3
只配 .env
-> 对话可用
-> Usage 页面仍然转圈
原因是 .env 只覆盖 Rust 进程环境,不会自动变成 Electron/Chromium 的代理配置。Desktop UI 里的 Usage、账号、设置页部分请求仍然可能没有走代理。
因此 .env 是必要但不充分的方案。
2.5 启动项补 --proxy-server
只配 .env 后,会话 / CLI 请求已经有代理,但 App UI 请求仍不一定覆盖。缺的这一块不是 Rust 环境变量,而是 Desktop / Electron / Chromium 的代理参数。
所以启动脚本只做一件事:启动 Codex Desktop 时给 Chromium 网络栈补 --proxy-server,并用 --proxy-bypass-list 保护本地 app-server 通信。
1
2
3
4
Start-Process -FilePath $CodexExe -ArgumentList @(
"--proxy-server=http://127.0.0.1:10808",
"--proxy-bypass-list=localhost;127.0.0.1;::1"
)
预期结果:
| 请求类型 | 预期结果 |
|---|---|
| App UI 请求 | 正常 |
| 会话 / CLI 请求 | 正常,来自上一节 .env |
| localhost 通信 | 正常,因为显式 bypass localhost |
这个组合的关键是:
1
2
3
4
5
6
7
8
~/.codex/.env
负责会话请求:Rust app-server / Codex CLI / 子进程
--proxy-server
负责 App UI 请求:Electron / Chromium
NO_PROXY / --proxy-bypass-list
保护 localhost 本地控制通道
这里不把 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY 写进脚本,是为了让会话层代理只维护在 ~/.codex/.env,Codex CLI 也能复用同一份配置。脚本只补 Desktop UI 这一层必须的 Chromium 启动参数。
3. 最终配置
最终我保留两份配置,但它们负责不同层次。
第一份是 ~/.codex/.env,给 Rust app-server 和 Codex CLI 使用:
HTTP_PROXY=http://127.0.0.1:10808
HTTPS_PROXY=http://127.0.0.1:10808
ALL_PROXY=http://127.0.0.1:10808
NO_PROXY=localhost,127.0.0.1,::1
这样即使不启动 Desktop,单独使用 codex CLI 时也能走同一份代理配置。
第二份是 Desktop 启动脚本,只给 Electron/Chromium 加代理参数。脚本保存为:
1
scripts/start-codex-desktop-proxy.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
param(
[string]$Proxy = "http://127.0.0.1:10808",
[string]$CodexExe = ""
)
$ErrorActionPreference = "Stop"
$scriptRoot = Split-Path -Parent $PSCommandPath
$pathCache = Join-Path $scriptRoot "start-codex-desktop-proxy.path.txt"
$runningDesktop = Get-CimInstance Win32_Process -Filter "Name = 'Codex.exe'" |
Where-Object { $_.ExecutablePath -like "*\WindowsApps\OpenAI.Codex_*\app\Codex.exe" }
if ($runningDesktop) {
Write-Warning "Codex Desktop is already running. Quit it completely first, then run this script again so proxy flags apply to the first process."
if (-not $CodexExe) {
$CodexExe = @($runningDesktop)[0].ExecutablePath
Set-Content -LiteralPath $pathCache -Value $CodexExe -Encoding UTF8
}
}
if (-not $CodexExe) {
$windowsApps = Join-Path $env:ProgramFiles "WindowsApps"
$CodexExe = Get-ChildItem -LiteralPath $windowsApps -Directory -Filter "OpenAI.Codex_*_x64__2p2nqsd0c76g0" -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
ForEach-Object { Join-Path $_.FullName "app\Codex.exe" } |
Where-Object { Test-Path -LiteralPath $_ } |
Select-Object -First 1
}
if (-not $CodexExe -and (Test-Path -LiteralPath $pathCache)) {
$cachedCodexExe = (Get-Content -LiteralPath $pathCache -Encoding UTF8 -TotalCount 1).Trim()
if ($cachedCodexExe -and (Test-Path -LiteralPath $cachedCodexExe)) {
$CodexExe = $cachedCodexExe
}
}
if (-not $CodexExe -or -not (Test-Path -LiteralPath $CodexExe)) {
throw "Could not find Codex Desktop executable. Pass it explicitly with -CodexExe 'C:\Program Files\WindowsApps\OpenAI.Codex_...\app\Codex.exe'."
}
Set-Content -LiteralPath $pathCache -Value $CodexExe -Encoding UTF8
$args = @(
"--proxy-server=$Proxy",
"--proxy-bypass-list=localhost;127.0.0.1;::1"
)
Write-Host "Starting Codex Desktop with Chromium proxy $Proxy"
Write-Host "Executable: $CodexExe"
Start-Process -FilePath $CodexExe -ArgumentList $args -WorkingDirectory $HOME
脚本没有绑定某个固定版本号。Codex Desktop 更新后,脚本会优先通过 OpenAI.Codex_*_x64__2p2nqsd0c76g0 通配符扫描 WindowsApps 目录并取最新目录;缓存路径只是扫描失败时的兜底,仍然可以通过 -CodexExe 手动指定。
可以再给这个脚本创建一个 Windows 快捷方式,比如叫 Codex Proxy,以后都从这个入口启动 Codex Desktop。
4. 总结
这次问题的核心不是“代理有没有开”,而是“如果不想一直开 TUN,代理配置应该作用到 Codex 的哪一层”。
可以总结成下面这张表:
| 配置方式 | App UI 请求 | 会话 / CLI 请求 | localhost 通信 | 影响范围 |
|---|---|---|---|---|
| 不开代理 | 取决于直连 | 取决于直连 | 正常 | 无代理 |
| TUN | 正常 | 正常 | 取决于 TUN 规则 | 全局/透明 |
| 系统代理 | 通常正常 | 不完全可靠 | 通常正常 | 系统级 |
.env | 不覆盖 | 正常 | 正常,前提是 NO_PROXY 配好 | Rust / CLI 进程树 |
.env + 启动脚本 | 正常 | 正常 | 正常 | Codex Desktop + CLI |
最终结论:
TUN 能兜住 App UI 和会话请求,但影响范围太大。迁移到更小范围的方案后,
~/.codex/.env负责会话 / CLI 请求;Desktop UI 由 Electron--proxy-server补齐。最终方案是.env管会话,脚本管 App UI,并同时绕过 localhost。