Codex CLI 在 WSL 里跑得好好的,一到登录就翻车,终端里永远只有那一句:Token exchange failed: 403 Forbidden。第一次遇到的人基本都会去检查账号密码,其实账号一点问题没有,这串 403 是登录链路里某个环节断了的典型信号。我在 Windows WSL2、远程 SSH、VS Code Remote 三个环境里都踩过这个坑,同一个 403 背后至少有四条完全不同的故障链路。这篇文章会把这几条链路一条条拆开讲清楚,并给出每一步的排查命令和修复方法。如果你正被这个问题卡住,照着顺序走一遍,多数情况下十分钟内就能定位到根因。
1. 错误码拆解:403 背后其实是几条不同的故障链路
1.1 先看报错末尾的补充文案
Codex 登录报 403 时,完整错误信息往往比终端里显示的那一行长得多,关键线索全在末尾。根据 Codex 社区里大量复现案例,我整理了这几类典型文本:
Token exchange failed: token endpoint returned status 403 Forbidden: country, region, or territory not supportedSign-in could not be completed. Token exchange failed: token endpoint returned status 403 ForbiddenError code: token_exchange_failed, details: token exchange failed: token endpoint returned status 403 Forbiddencc switch local proxy failed while handling codex endpoint /responses
第一类错误已经把原因写在脸上了:country, region, or territory not supported,意思是服务方基于当前的访问来源判断,认为请求不在它支持的范围里。这类错误属于服务策略限制,技术层面说直白点:必须从符合服务支持范围的网络环境去发起登录,在受限环境下怎么改配置都白费。
后两类就复杂一些。token exchange failed本身只表示“授权码换 token 这一步没有成功”,但为什么没成功,要看有没有附带error sending request、connection refused、timeout这类网络层提示。如果能看到这些,说明问题出在请求根本没到达 token endpoint,或者往返途中被拦截。还有一类不在登录时出现、而是在 Codex 运行中出现的cc switch local proxy failed while handling codex endpoint /responses,它指的是 Codex 在调用本地辅助服务时失败,通常和本地端口占用、配置指向错误有关。这类问题和登录 403 是两回事,但很多人在同一个 VS Code 或 WSL 环境里会连续撞见,一并说清楚。
1.2 Codex 登录为什么对“回调链路”这么敏感
要理解为什么 403 在 WSL/SSH 里特别高频,得先知道 Codex CLI 的登录机制。
Codex CLI 走的是标准的 OAuth Authorization Code + PKCE 流程,本地会起一个轻量的 HTTP 服务,监听 127.0.0.1 的随机端口。用户执行codex login后,流程是这样的:
- CLI 生成随机端口并启动回调服务;
- 调用系统默认浏览器打开登录页面;
- 用户在网页上完成授权;
- 登录页把授权码重定向到
http://127.0.0.1:端口; - CLI 收到授权码,拿着它向 token endpoint 发起交换请求;
- 拿到 access token 后写入本机凭证文件。
这个流程里,第 4 步和第 5 步最容易出问题。第 4 步要求浏览器能访问到本地回调端口;第 5 步要求 CLI 进程能正常发出 HTTPS 请求并完成 TLS 校验。WSL2 默认 NAT 网络模式会让“Windows 浏览器访问 WSL2 里的 127.0.0.1”变得可用,但也存在不少边界情况;SSH 远程环境则更尴尬,远程服务器上根本没有你能看到的浏览器。这些矛盾叠加起来,就成 403 的重灾区。
1.3 快速给当前报错分类的速查表
| 报错文本特征 | 故障链路 | 高发环境 |
|---|---|---|
403 +country, region, or territory not supported | 访问来源受服务策略限制 | 任何环境 |
403 +error sending request/timeout/refused | 网络请求未到达 token endpoint | WSL2 |
403 + 纯token exchange failed,无附加提示 | 环境变量污染 / 回调中断 | WSL2 / SSH |
cc switch local proxy failed(运行时) | Codex 本地服务访问失败 | VS Code / WSL |
记住一个原则:先看报错文本里的附加信息,再决定往哪个方向排查,不要在“重新安装、重启电脑”这种盲试上浪费时间。
2. WSL 里的三个隐藏坑:系统时间、环境变量、网络模式
2.1 系统时间偏差:被忽略的隐形元凶
OAuth 和 token 交换协议强依赖时间。token endpoint 在签发和校验授权码、验证 access token 时,会检查iat(签发时间)和exp(过期时间)字段;如果客户端本地时间偏差超过一定阈值,请求会被直接判为无效,返回 403 或 401。
WSL 和 Windows 虽然共享同一套时钟,但在 Windows 睡眠、休眠或长时间待机后,WSL 里的系统时间偶尔会出现偏差。这不是 WSL 独有的问题,原生 Linux 虚拟机里也会遇到,但 WSL 因为和宿主机共生的关系,更容易让人忽略。
排查方法很简单。先在 WSL 内执行:
date再打开 PowerShell:
Get-Date如果两者相差超过几十秒,先做时间同步:
sudo hwclock -shwclock -s会把硬件时钟写入系统时间。WSL 里这个命令通常直接可用。如果你的 WSL 里没有hwclock,或者想强制对齐网络时间源:
sudo apt update sudo apt install -y ntpdate sudo ntpdate -u ntp.aliyun.com做完之后,再跑一次date对比,偏差进入秒级范围就可以继续了。这个检查成本极低,却经常能解决看似莫名其妙的 403,强烈建议放在任何其他操作之前。
2.2 WSL 继承 Windows 环境变量时的“地址错位”
WSL 默认会读取 Windows 的用户级环境变量,这意味着你在 Windows 系统设置里配置过的HTTP_PROXY、HTTPS_PROXY这类变量,在 WSL 终端里同样可见。这本是为方便设计的,但这里埋了一个大坑。
WSL2 默认是 NAT 网络模式,WSL 内部和 Windows 宿主是两个不同的网络命名空间。Windows 上的回环地址 127.0.0.1 指向 Windows 自己,WSL 里的 127.0.0.1 指向 WSL 自己。如果 Windows 侧配置的HTTP_PROXY写的是http://127.0.0.1:端口,WSL 继承过来之后,这个地址在 WSL 里指向的是 WSL 自身,而真正提供转发服务的进程其实跑在 Windows 宿主上。请求发过去,直接 connection refused 或一直转圈,最终表现为 token exchange 失败。
检查方式:
env | grep -i proxy如果在输出里看到了HTTP_PROXY、HTTPS_PROXY等条目,去核实它们的值。如果确实指向 127.0.0.1,且你确实需要这些变量,可以把它改成宿主机在 WSL 网络中的实际地址:
# 查看宿主机地址 ip route show | grep -i default | awk '{print $3}'得到宿主机 IP 之后,在 WSL 内临时修正:
export HTTP_PROXY="http://<宿主机IP>:端口" export HTTPS_PROXY="$HTTP_PROXY"这里我只讨论环境变量本身的正确性问题。如果你的环境里没有这些变量,直接跳过这一节,不要为了排查而引入任何额外设置。反过来,如果 WSL 里确实有这类变量且指向错误,它很可能就是 Codex 登录失败的直接原因。这个错位问题起初很容易被忽略,因为 Windows 原生终端里 Codex 一切正常,换到 WSL 就挂,你会误以为是 WSL 环境的问题。
2.3 WSL 网络模式与 localhost 回调的边界
WSL1 和 WSL2 的网络行为完全不同。WSL1 与 Windows 共享网络栈,localhost 互通天然无障碍;WSL2 走 Hyper-V 虚拟化,默认 NAT 模式下,Windows 访问 WSL2 内的 127.0.0.1 端口是通过一个叫 localhostForwarding 的机制自动转发的,默认开启。
这个机制保证了一个很关键的场景:Windows 上的浏览器可以访问 WSL2 里 Codex 回调服务器的地址。但机制并不总是可靠,WSL 重启、端口被占用、.wslconfig 里显式关闭了转发,都会让回调断掉。
先确认你用的是 WSL2:
wsl -l -v再检查 Windows 用户目录下的 .wslconfig:
notepad "$env:USERPROFILE\.wslconfig"Windows 11 22H2 及以上版本还支持 Mirrored 网络模式,在 .wslconfig 里配置:
[wsl2] networkingMode=mirrored启用后 WSL2 与 Windows 共享网络接口,localhost 双向互通,很多“Windows 访问不了 WSL2 服务”的怪问题会直接消失。但注意,改完 .wslconfig 必须执行wsl --shutdown重启 WSL 才能生效。
在 Codex 登录这个场景下,如果 Windows 浏览器打开登录页后能正常登录,但授权后回调请求失败,优先怀疑 localhostForwarding 是否正常工作。可以先做一个连通性测试:在 WSL 里用 Python 起一个临时 HTTP 服务:
python3 -m http.server 18888然后在 Windows 浏览器访问http://127.0.0.1:18888,能打开说明转发正常。不通就检查 .wslconfig、Windows 防火墙、WSL 状态,把这层链路打通了再回来跑 Codex。
3. SSH 与 VS Code Remote 场景:把回调链路拉通才是关键
3.1 远程服务器上直接登录,为什么必然失败
SSH 到一台远程服务器后再执行codex login,Codex 会在远程服务器的 127.0.0.1 上启动回调服务,然后尝试打开远程服务器的浏览器。如果你是通过 SSH 客户端连过去的,远程服务器上根本没有你能看到的浏览器;就算 Codex 自动调用了 xdg-open 之类的命令,弹出来的也只是远程桌面或终端环境里的浏览器,你根本完成不了交互。
退一步说,就算你在本地浏览器里手动访问登录页并完成授权,回调地址仍是指向远程服务器的 127.0.0.1,你本地浏览器根本够不着。这就是 SSH 环境下Sign-in could not be completed最高频的成因:授权码无处可回。
3.2 用 SSH 端口转发给回调链路搭一座桥
解决思路是让远程服务器的 127.0.0.1 端口能被本地访问到。SSH 的本地端口转发可以做到:
ssh -L 1455:127.0.0.1:1455 user@remote -N执行后,本地 1455 端口会映射到远程服务器的 127.0.0.1:1455。然后把远程 Codex 的回调端口固定成 1455。Codex 是否支持固定回调端口取决于版本,可以先看帮助:
codex login --help如果不支持固定端口,观察它启动时输出的端口号,再用同样的-L参数做映射。比如看到端口是 23456,就执行:
ssh -L 23456:127.0.0.1:23456 user@remote -N端口转发是一条可行路径,但缺点是要配合端口号动态调整,稍嫌繁琐。实际操作中我更推荐另一种思路——在本地完成登录,再把凭证同步到远程。
3.3 更省事的做法:本地登录后同步凭证
Codex 登录成功后,凭据会写入~/.codex/auth.json文件里。在本地 Windows 原生终端或本地 WSL 里完成 Codex 登录,然后把这个文件复制到远程服务器的对应路径,比在远程折腾回调链路稳得多。
# 本地确认登录状态 codex login # 查看本地凭证文件 cat ~/.codex/auth.json # 用 scp 复制到远程 scp ~/.codex/auth.json user@remote:~/.codex/auth.json复制完成后,在远程执行codex测试。这种方法绕开了登录回调的整条链路,我实际在不同云主机上验证过,多数场景都能直接见效。需要提醒的是,凭证文件属于敏感信息,传输和保存都要注意权限,落地后顺手chmod 600是基本操作。
3.4 VS Code Remote 里两个容易误判的高频问题
VS Code 的 Remote-WSL 和 Remote-SSH 插件,本质是在远端启动一个 VS Code Server,终端里跑的codex就是远端的 CLI。所以前面说的 WSL/SSH 问题在 VS Code 里会原样复现,处理思路也一样。唯一区别是 VS Code 里还可能遇到扩展进程的网络栈与终端不一致的情况,但只要你在终端里用的是 CLI,就按 CLI 的路径排查。
另有一个高频的 Windows 侧问题:VS Code Remote-SSH 连接时,OpenSSH 可能会报bad owner or permissions on C:\Users\<用户名>\.ssh\config。这通常是因为 .ssh\config 文件的 ACL 权限太开放,OpenSSH 出于安全策略拒绝读取。修复方式是在 PowerShell 里收紧权限:
icacls "C:\Users\<用户名>\.ssh\config" /inheritance:r /grant:r "$($env:USERNAME):R"执行完后重新连接。这个问题本身不是 Codex 的 403,但它会让 SSH 连接建立不起来,间接导致你在 VS Code 里根本无法进入远端,容易被误当成 Codex 登录问题。
4. 完整实操:WSL 里从零修复 Codex 登录的五个步骤
4.1 第一步:清理旧凭证与检查配置
旧凭证或残留的登录状态会让后续排查失真。先用一条命令把 Codex 的认证信息清掉:
rm -f ~/.codex/auth.json同时打开配置文件看一眼,确认里面没有自己不小心写进去的奇怪内容:
cat ~/.codex/config.toml # 部分版本路径不同 cat ~/.config/codex/config.toml也可以直接列目录:
ls -la ~/.codex确认auth.json已被删除后,先别急着执行codex login,按下面的顺序把环境热起来。有时候 WSL 里的 profile 脚本会在启动时注入一些环境变量,所以最好开一个新的终端窗口再继续。
4.2 第二步:时间、网络、环境变量三项体检
把前面讲过的检查落成一套可执行命令,按顺序跑一遍:
# 时间体检 date # HTTP_PROXY 检查 env | grep -i proxy # 默认路由,确认宿主地址 ip route show | grep -i default # DNS 状态 cat /etc/resolv.conf如果发现时间偏差大,就执行sudo hwclock -s或用 ntpdate 同步。如果发现HTTP_PROXY指向 127.0.0.1,根据实际需要修正为宿主机 IP。如果确认没有这些环境变量,继续下一步。
4.3 第三步:带日志执行登录,锁定失败阶段
Codex 登录时开启详细日志,能看到回调服务器启动、浏览器打开、token 请求发起等环节的状态:
codex login --verbose如果没有--verbose参数,或者想实时观察日志:
tail -f ~/.codex/log/*.log如果在 WSL 里执行登录后,浏览器没有自动弹出,大概率是 wslview 没装或失效。此时不用慌,看终端输出里有没有打印登录 URL,直接手动复制到 Windows 浏览器打开即可。
登录失败后,从日志里找三个关键信息:
- 回调服务器监听在哪个端口;
- token 请求是否真正发出;
- token endpoint 返回的状态码和响应体。
有了这三个信息,就能和第一节的速查表对照,判断故障环节。
4.4 第四步:按失败阶段执行对应修复
如果日志显示 token 请求根本没发出去,或者发出去就卡住,重点检查网络出口、DNS 解析和时间。
如果日志显示请求出去了,但响应是 403,就要区分处理:响应体里带着country, region, or territory not supported,那是服务策略限制,需要把环境切换到服务支持的网络范围再登录;响应体里是其他内容,则考虑证书校验失败、时间偏差、环境变量污染。
如果浏览器能打开登录页,但授权后回调端口收不到请求,重点检查 WSL 的 localhostForwarding 是否正常、端口是否被占用、Codex 监听的是不是 127.0.0.1。我遇到过一台机器上端口被其他进程占了,Codex 静默换了端口,而浏览器里的回调地址还是旧端口,这种情况看日志就能发现。
4.5 第五步:验证结果并固化经验
登录成功后,先验证一次实际调用是否正常:
codex能正常进入对话或响应命令,就说明 token 交换和运行时都没问题。最后可以把关键的宿主 IP、端口配置整理成一个小脚本,下次新环境直接复用,不用再逐条排查。我在多台机器上反复踩过同样的问题之后,逐渐养成了“先看错误后缀、再查环境变量、最后动配置”的习惯,效率比盲目搜报错高很多。
5. 常见问题速查表与排查顺序建议
5.1 高频报错与处理方式对照
| 场景 / 报错 | 可能原因 | 处理建议 |
|---|---|---|
403 +country, region, or territory not supported | 访问来源不在服务支持范围 | 切换到符合服务支持范围的网络环境后再登录 |
| 浏览器能开登录页,授权完回调失败 | WSL localhostForwarding 失效 / 端口占用 | 检查 .wslconfig、重启 WSL、确认端口未占用 |
| WSL 里 codex login 一直转圈 | 环境变量指向错误 / 网络出不去 | `env |
| 时间准确但 TLS/证书报错 | 根证书或 CA 证书缺失/过期 | sudo apt install -y ca-certificates && sudo update-ca-certificates |
SSH 远程登录Sign-in could not be completed | 远程无法回调本地 | SSH 端口转发,或本地登录后同步 auth.json |
VS Code Remote-SSH 连不上 +.ssh/config权限报错 | OpenSSH ACL 校验失败 | icacls收紧权限后重连 |
登录成功但运行时报local proxy failed | 本地辅助服务不可达 | 检查相关端口占用与配置指向 |
5.2 我的排查顺序:时间 → 环境变量 → 回调 → 配置
我建议的顺序是:时间 → 环境变量 → 回调链路 → 配置。时间检查成本最低,排除掉它再往后走;环境变量是 WSL 里最容易踩的隐性坑;回调链路多发生在 SSH/VS Code Remote 场景;最后才需要动配置和凭证。按这个顺序走,可以最大程度避免在东一下西一下的尝试里浪费时间。
遇到 403 不要只看状态码。403 本身只是一个结果,它前面的错误文本才是线索。把完整报错复制下来,拆开读:先看有没有附加限制描述,再看有没有网络层错误,最后才考虑是不是账号问题。这个习惯我反复用在 WSL、SSH、VS Code 三种环境里,绝大多数情况都能快速定位。
最后分享一个小技巧:在 WSL 里执行codex login之前,先跑一遍env | grep -i proxy,再顺手看一眼date和宿主机时间差。这两个命令加起来不到五秒钟,能挡掉大半莫名其妙的 403。我自己在新配置的开发机上,已经把这当成登录前的固定动作了。Codex 这种 CLI 工具对环境的敏感度远超普通命令行程序,一旦把网络、回调、时间三条链路摸清楚,后续再遇到类似问题,就都是排列组合的事了。