Codex CLI 401 未授权错误:9 种实测成因与相似陷阱

Codex CLI 0.146.0 认证会以 9 种方式失败,且并非都是真正的 401。看报文正文,别只看状态码:没发密钥、密钥被拒、多了换行。

Codex CLI 401 未授权错误:9 种实测成因与相似陷阱

太长不看: Codex CLI 认证会以 9 种不同方式出问题,而其中只有一部分表现为真正的 401,这正是状态码成为整段输出里最没用的部分的原因。真正能定位成因的是报文正文。Missing bearer or basic authentication in header 表示什么都没发出去,而意外之处在于:在默认 provider 上导出 OPENAI_API_KEY 根本没用。Incorrect API key provided 表示你的密钥送达了但被拒绝。You didn't provide an API key 表示请求头在传输途中被丢弃,几乎总是因为密钥值末尾带了换行符。本地报的 Missing environment variable 错误压根不是 401,一个丢了 /v1base_url 返回的 404 同样不是。下面每种情况都在 Codex CLI 0.146.0 上于 2026-07-30 复现过。

30 秒快速诊断

三项检查,按此顺序。多数人跳过第一项,然后花 20 分钟去重签一个本来没问题的密钥。

检查命令它告诉你什么
1. 到底发出请求了吗?在输出里找 Missing environment variable如果有,说明 Codex 根本没发起网络调用。你的配置指向了一个未设置或为空的变量。
2. 401 正文说了什么?阅读 401 Unauthorized: 后面的文字三种不同的报文,三种不同的成因。见下表。
3. 密钥到底能不能用?curl -s -o /dev/null -w "%{http_code}" -X POST https://api.ofox.io/v1/responses -H "Authorization: Bearer $YOUR_KEY" -H "Content-Type: application/json" -d '{"model":"openai/gpt-5.5","input":"hi","max_output_tokens":16}'200 表示密钥没问题,问题出在你的 Codex 配置上。401 表示密钥本身就是问题所在。

第 3 步里有个坑值得早点点明,因为网上一半的排错建议都搞错了:在聚合网关上,别拿 /v1/models 当密钥检查。 2026-07-30 实测,https://api.ofox.io/v1/models 就算带一个伪造密钥也会返回 200 并列出完整目录,甚至完全不带 Authorization 头也是如此。模型目录本来就是公开的。OpenAI 自家的 api.openai.com/v1/models 在不带密钥时确实会返回 401,这个习惯就是从那儿来的,但这个习惯迁移不过来。请用一个真正会跑推理的端点。

什么时候该修,什么时候该换,什么时候该停手

只要你知道自己遇到的是哪一种,认证失败修起来很便宜;靠猜就很贵。粗略规则:

  • 修它,当报文点名了具体成因时:Missing environment variableIncorrect API key provided,或任何提到刷新 token 的信息。这些都有确定的一步到位修复方案,每个耗时不到两分钟。
  • 换认证路径,当你在 ChatGPT 登录流程上已经折腾了三次。API 密钥路径可动的部件更少(没有刷新 token,没有浏览器往返,没有 8 天刷新窗口),而且如果你要做任何自动化,它是唯一能挺过容器重启的路径。
  • 停下来检查别的东西,当你收到的是 404 而不是 401,或者 CLI 因为配置错误干脆拒绝启动。这些都不是认证问题,再怎么轮换密钥也动不了它们。如果错误提到的是用量额度而非授权,同理;那条路径在 Codex 周额度耗尽 一文里有讲。

唯一一种重签密钥是正确首选动作的情况是:Incorrect API key provided 且错误里显示的密钥掩码后缀与你以为在用的密钥一致。如果后缀对不上,那你遇到的是配置问题而非密钥问题,换个新密钥照样会以同样方式失败。

读懂 401:三种报文,三种成因

这是核心表格。每一行都对着一个真实端点复现过。

报文正文实际发生了什么来自哪里修复
Missing bearer or basic authentication in header请求上没附带任何凭据未登录的默认 OpenAI provider,或仅导出了 OPENAI_API_KEYprintenv OPENAI_API_KEY | codex login --with-api-key
Incorrect API key provided: sk-proj-****7890请求头送达,服务器拒绝了里面的值auth.json 里的密钥错误、被吊销,或跨账户重签密钥,或登出后用正确的密钥登录
Invalid or expired API key同上,网关侧的措辞自定义 provider 的 env_key 值错误或被引号包裹检查该变量精确的字节内容,而不只是它被设置了
You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth请求头格式错误被丢弃密钥值里嵌了换行符去掉变量末尾的换行符
Your access token could not be refreshed...ChatGPT OAuth 刷新失败刷新 token 过期、被复用或被吊销codex logout 后重新登录
Missing environment variable: 'X'不是 401。 根本没发请求env_key 指向的变量未设置或为空在启动 Codex 的那个 shell 里导出该变量

Codex CLI 401 诊断流程:先检查本地是否有 Missing environment variable 错误,然后读报文正文,它分成没发密钥、密钥被拒、以及请求头被换行符丢弃三种情况

测试运行中有两个细节能让输出更好读。在默认 OpenAI provider 上,Codex 会先尝试对 wss://api.openai.com/v1/responses 走 WebSocket 传输,重试五次,然后回退到 HTTPS 再重试五次,所以一次认证失败会在真正的报文出现前产生大约十行错误。在自定义 provider 上,WebSocket 传输是关闭的(codex doctor 报告 supports websockets: false),所以你只会看到一轮重试和一次更干净的失败。如果你正盯着一整墙的 Reconnecting... 4/5,滚到最底部;最后一行才是关键。

成因 1:你导出了 OPENAI_API_KEY,以为这就够了

这是最常见的一种,而且反直觉到值得放在头条。

export OPENAI_API_KEY="sk-proj-..."
codex exec "say hi"
# ERROR: unexpected status 401 Unauthorized: Missing bearer or basic
# authentication in header, url: https://api.openai.com/v1/responses

注意服务器说的是什么:Missing bearer。不是「你的密钥错了」。什么都没发出去。Codex 0.146.0 里的默认 provider 从 $CODEX_HOME/auth.json 读取凭据,而不是从环境读取。设置那个变量什么也改变不了。

单变量验证法:拿完全相同的一个无效密钥,把它写进 auth.json 而不是环境里,报文就变了。

echo "sk-proj-invalidkeyfortesting1234567890" | codex login --with-api-key
codex exec "say hi"
# ERROR: unexpected status 401 Unauthorized: Incorrect API key provided:
# sk-proj-**************************7890 ... auth error code: invalid_api_key

同一个密钥,不同的报文。第一次运行根本没把它发出去;第二次发了,然后被拒。这个差异就是整个诊断的核心。

把密钥写到 Codex 真正会去找的地方:

printenv OPENAI_API_KEY | codex login --with-api-key

环境变量确实能用,但只能通过自定义 provider 的 env_key 字段,那是另一种机制,后面会讲。

成因 2:你用了旧的 —api-key flag

如果你照着一份 2026 年年中之前写的教程操作:

codex login --api-key "sk-proj-..."
# The --api-key flag is no longer supported. Pipe the key instead,
# e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`.

报文很清楚,但它退出时什么都没写入,而在安装脚本里这行输出会被滚过去。接下来的命令便以 Missing bearer 失败,密钥就此背锅。检查 auth.json 是否存在、内容是否符合预期:

cat ~/.codex/auth.json
# {
#   "auth_mode": "apikey",
#   "OPENAI_API_KEY": "sk-proj-..."
# }

成因 3:env_key 指向的变量没设置(不是 401)

在自定义 provider 配置块下,Codex 从你指定的环境变量读取密钥:

model = "openai/gpt-5.5"
model_provider = "ofox"

[model_providers.ofox]
name = "Ofox"
base_url = "https://api.ofox.io/v1"
env_key = "OFOX_API_KEY"
wire_api = "responses"

如果 OFOX_API_KEY 没设置,你收到的不是 401。你会收到一个本地错误,且完全没有网络调用:

ERROR: Missing environment variable: `OFOX_API_KEY`.

空字符串也会产生一模一样的错误,这与源码吻合:model-provider-info/src/lib.rs 在使用前用 !v.trim().is_empty() 过滤了该变量。所以对 Codex 而言,export OFOX_API_KEY="" 和从不导出它是一回事。

这个坑在 launchd、systemd 和 Docker 里咬得最狠,因为启动 Codex 的那个 shell 并不是你导出变量的那个 shell。

成因 4:密钥值里有换行符

这是这一组里最阴险的,因为报错指控你没提供密钥,而你自己在 printenv 里明明能亲眼看到它。

export OFOX_API_KEY="$(cat ~/keys/ofox.txt)"   # file ends with a newline
codex exec "hi"
# ERROR: unexpected status 401 Unauthorized: You didn't provide an API key.
# You need to provide your API key in an Authorization header using Bearer
# auth (i.e. Authorization: Bearer YOUR_KEY). [ofox.io]

请求头是带着一个内嵌换行符拼出来的,被直接丢掉了。服务器确实从没看到任何凭据,所以它的报文是准确的,只是听起来像你忘了设置。

值得知道这里不是成因的东西,因为它是最明显的嫌疑犯,却是清白的:末尾多个空格没问题。export OFOX_API_KEY="$REAL " 实测请求成功。注意 Codex 并没有帮你清理它:api_key() 里的 trim() 只是个空值检查,它返回的值是原样的,连末尾空格一起。下游某个环节容忍了它。而换行符则截然不同,它会直接破坏请求头。无论如何,别浪费时间去揪多余的空格。

而带引号的值确实会失败,措辞不同:

export OFOX_API_KEY='"sk-..."'   # literal quote characters in the value
# ERROR: unexpected status 401 Unauthorized: Invalid or expired API key

当一个 .env 文件被用天真的 export $(cat .env | xargs) 加载、把引号也保留下来时,这种情况就很常见。

在导出时就把碍事的字节剥掉,并确认长度:

export OFOX_API_KEY="$(tr -d '\n\r"' < ~/keys/ofox.txt)"
printf '%s' "$OFOX_API_KEY" | wc -c   # confirm the byte count matches the key length

成因 5:provider 配置块整个漏了 env_key

九种里最安静的一种失败。把上面配置里的一行删掉:

[model_providers.ofox]
name = "Ofox"
base_url = "https://api.ofox.io/v1"
wire_api = "responses"
# env_key line deleted

Codex 不会抱怨。它会回退到 auth.json 里的凭据(对多数人来说是一个 OpenAI 密钥),然后把那个发给网关。网关拒绝它:

ERROR: unexpected status 401 Unauthorized: Invalid or expired API key,
url: https://api.ofox.io/v1/responses

你的环境变量设置得没错。你的密钥有效。报错却说密钥无效,因为发出去的是另一个密钥。在同一个 shell 里两种情况都测过:有 env_key 时请求返回正常补全,删掉那行就 401。

反过来也值得知道,而且是好消息:当 env_key 存在时,它优先于 auth.json。用一个故意伪造、存在 auth.json 里的密钥,配上一个存在环境变量里的有效密钥实测,请求成功。你在配置自定义 provider 之前不需要先登出。

如果你要从零搭建一个网关 provider,完整且验证过的配置块在 Codex CLI 自定义模型 provider 里,文件中每个键都在 config.toml 参考 里有说明。

成因 6:ChatGPT 登录路径过期了

如果你是用 ChatGPT 订阅而非 API 密钥登录的,失败的样子完全不同:

ERROR: Your access token could not be refreshed. Please log out and sign in again.

注意周围日志行里的端点:wss://chatgpt.com/backend-api/codex/responses,而不是 api.openai.com。两种登录模式对接的是不同的后端,这是判断你实际处于哪种模式的一个快速方法。

Codex 0.146.0 在这里会打印五种变体之一,它们并不通用。来自 tag rust-v0.146.0login/src/auth/manager.rs

变体实际含义
...because your refresh token has expired真正过期了。重新登录。
...because your refresh token was already used两个客户端在同一份 auth.json 上抢跑。配置被复制过,或用了共享容器镜像。
...because your refresh token was revoked会话在服务器侧被终止,常见于改密码或在别处登出。
...could not be refreshed.(无原因)刷新端点返回了某种未归类的东西。重新登录。
...because you have since logged out or signed in to another account存储的账户与当前活跃账户不再匹配。

「already used」 这个变体是最让人意外的。刷新 token 是一次性的,所以把 auth.json 打进 Docker 镜像,或在两台笔记本之间同步 ~/.codex,得到的认证配置会在先完成刷新的那台机器上能用、在另一台上崩掉。同一个源文件把 TOKEN_REFRESH_INTERVAL 设为 8 天,所以一台闲置超过一周的机器会在下次运行时尝试主动刷新,冲突通常就在这时暴露。

只有一个修复方案,而且很粗暴:

codex logout
codex login          # browser flow
# or, for anything automated:
printenv OPENAI_API_KEY | codex login --with-api-key

对于无人值守的环境,请首选 API 密钥路径。它没有刷新语义可失步。

成因 7:codex login status 告诉你一切正常

它会撒谎,而且有两种不同的撒法,两种都复现过。

用一份手工构造、含过期 ChatGPT token 的 auth.json,每个请求都以上面的刷新错误失败,而与此同时:

codex login status
# Logged in using ChatGPT

在自定义 provider 下,status 汇报的是躺在 auth.json 里的那个密钥,而那根本不是请求实际使用的密钥:

codex login status
# Logged in using an API key - sk-proj-***n-999

两种输出描述的都是某个文件的内容。都没有做网络检查。用它们来回答「是否存有一个凭据」,绝不要用来回答「我的认证能不能用」。后者请运行 30 秒诊断里的那条 curl,或者干脆运行 codex exec "hi" 然后看最后一行。

codex doctor 在这里更有用。它的 Configuration 部分会显示加载了哪个 config.toml、是否解析成功、auth 存储模式,以及它能看到哪些认证环境变量;它的 Connectivity 部分会报告活跃的 provider、wire API、是否适用 WebSocket 传输,以及端点是否可达。它仍然不校验凭据,但它会在一秒内告诉你 Codex 是不是在读一个与你一直在编辑的那份不同的配置文件,当设置了 CODEX_HOME 时,这是出人意料地频繁的根本成因。

成因 8:auth.json 损坏了(不是 401)

更少见,但它产生的错误看起来完全不像认证问题,而这恰恰是它费时间的原因。一份被截断或手工编辑过的 auth.json

codex exec "hi"
# EOF while parsing a value at line 2 column 0

没提认证,没有 HTTP 状态码,没有文件路径。这发生在一次被中断的 codex login、一个部分同步的文件,或一次手工编辑漏了个右花括号之后。文件够小,可以直接查看:

python3 -m json.tool ~/.codex/auth.json > /dev/null && echo "valid JSON"

如果它解析不了,删掉重新登录。里面没有任何值得抢救的东西;要么是一个你可以重新粘贴的 API 密钥,要么是会被重新签发的 OAuth token。

成因 9:Codex 读的配置和你编辑的那份不是一回事

CODEX_HOME 会同时重定位 config.tomlauth.json。如果它在你的 shell profile、某个包装脚本里,或被某个替你启动 Codex 的工具设置了,那你对 ~/.codex/config.toml 做的每一处编辑,进的都是一个 Codex 从不打开的文件。症状是一个撑过了本该修好它的各种改动的 401。

codex doctor 在它的 state 部分一行内回答这个问题:

CODEX_HOME  /private/tmp/codex401/home7 (dir)

以及在 Configuration 下:

config.toml  /private/tmp/codex401/home7/config.toml
config.toml parse  ok

如果那个路径不是你一直在编辑的文件,别再调密钥了。

同一个命令还会标记出这个问题的第二个版本。在一台同时全局和本地安装了 Codex 的机器上,doctor 会打印:

✗ install   npm install -g @openai/codex would update a different install
✗ updates   update would target a different npm install

按字面理解,这意味着更新命令针对的包根目录和你正在运行的二进制不同,所以一次意在拉取认证修复的升级可能根本没动到正在运行的那份。在你断定「升个版本没用」之前,值得先解决它。

退出码不会告诉你的一件事

本文里每一种失败模式退出的都是 1。没凭据、缺环境变量、配置解析错误、auth.json 损坏、密钥被拒:全都是 1。如果你把 Codex 包在脚本里并按退出状态分支处理,你没法区分「你的密钥错了」和「你的配置文件有个拼写错误」。请捕获 stderr 并匹配报文文本:

out=$(codex exec "ping" 2>&1) || {
  case "$out" in
    *"Missing environment variable"*) echo "config points at an unset variable" ;;
    *"Missing bearer"*)               echo "no credential was sent" ;;
    *"Incorrect API key"*|*"Invalid or expired API key"*) echo "credential rejected" ;;
    *"could not be refreshed"*)       echo "ChatGPT session expired, log in again" ;;
    *) echo "other failure: $out" ;;
  esac
}

两种看起来像 401 却不是的情况

base_url 丢了它的 /v1 给你的是 404,不是 401:

ERROR: unexpected status 404 Not Found: 404 page not found,
url: https://api.ofox.io/responses

如果你看到 404 page not found,且 URL 少了一个你预期存在的路径段,修 URL,别再盯着凭据看了。

wire_api = "chat" 在任何请求之前就失败了,发生在配置加载阶段:

Error loading config.toml: `wire_api = "chat"` is no longer supported.
How to fix: set `wire_api = "responses"` in your provider config.
More info: https://github.com/openai/codex/discussions/7782

chat 这个值被移除了;这个枚举只剩下一个 Responses 变体。任何你让 Codex 对接的网关都必须暴露一个兼容 Responses 的端点。这会绊倒从旧配置迁移过来的人,而且因为它在启动阶段就杀掉进程,有时会被归为「升级后认证坏了」。它没坏。关于哪些模型能扛住这个约束,更多内容见 OpenCode vs Codex CLI

还有第三种擦边情况:如果你在企业代理后面,失败模式通常是 TLS 或连接错误而非 401,修复方案也完全不同。那条路径在 企业代理后的 Codex CLI 里有讲。

按认证模式分类的修复方案

同一个症状,根据你的认证方式不同需要不同的修复。这是那张值得收藏的表。

认证模式凭据存在哪最可能的 401 成因修复
ChatGPT 订阅auth.json OAuth token刷新 token 过期,或跨机器被复用codex logoutcodex login
OpenAI API 密钥auth.jsonOPENAI_API_KEY 字段密钥从没写进去(旧 --api-key flag,或只导出到了环境里)printenv OPENAI_API_KEY | codex login --with-api-key
自定义网关 providerenv_key 指定的环境变量变量在启动 shell 里未设置、值里带换行符,或配置里漏了 env_key在正确的 shell 里导出、剥掉换行符、确认那行存在
自动化或容器环境变量,运行时注入打进镜像的 auth.json 带着一次性刷新 token用 API 密钥路径,把密钥作为环境变量注入,绝不把 auth.json 打进镜像

具体到容器和 CI 的场景:不要从 ~/.codex 挂载任何东西,在容器环境里设置变量,并用一个带 env_key 的自定义 provider 配置块。这个组合没有刷新状态,也没有会失效的文件。

常见失败模式,以及各自实际打印出什么

上面所有内容,浓缩成一张表。在 Codex CLI 0.146.0 上于 2026-07-30 复现,macOS,默认 provider 的行对着 api.openai.com,自定义 provider 的行对着 api.ofox.io

#配置结果输出
1到处都没凭据401Missing bearer or basic authentication in header
2导出了 OPENAI_API_KEY,默认 provider401Missing bearer or basic authentication in header(与 #1 完全相同)
3通过 --with-api-key 写入了无效密钥401Incorrect API key provided: sk-proj-****7890auth error code: invalid_api_key
4codex login --api-key(旧 flag)退出The --api-key flag is no longer supported
5自定义 provider,env_key 变量未设置本地错误Missing environment variable: 'OFOX_API_KEY'
6自定义 provider,env_key 变量为空字符串本地错误与 #5 相同
7自定义 provider,无效密钥401Invalid or expired API key
8自定义 provider,密钥被字面引号包裹401Invalid or expired API key
9自定义 provider,密钥末尾带空格200请求成功,末尾空格被下游容忍
10自定义 provider,密钥末尾带换行符401You didn't provide an API key...
11自定义 provider,配置里删掉了 env_key401Invalid or expired API key(发出去的是 auth.json 里的密钥)
12auth.json 里是伪造密钥,env_key 里是有效密钥200env_key 优先
13ChatGPT auth.json,过期的刷新 token刷新错误Your access token could not be refreshed...
14base_url 丢了 /v1404404 page not found
15wire_api = "chat"配置错误wire_api = "chat" is no longer supported

第 9 行和第 12 行是那两个靠排除法省时间的。如果你一直在揪空白字符,或以为配置网关之前必须先登出,这两条都是死路。

当你修不了凭据时:现在就能用的替代方案

如果障碍是认证路径本身而非一个拼写错误,你有几个选择。

选项它解决什么代价
用 API 密钥代替 ChatGPT 登录去掉刷新 token、浏览器流程和 8 天刷新窗口按 token 计费,而非订阅覆盖
env_key 的网关 provider一个密钥、一个变量,在容器和 CI 里无需登录步骤即可工作需要一个兼容 Responses 的端点,所以各模型的支持情况不一
用第二个 provider 配置块做手动兜底当一个凭据失效时,让你能用 --config model_provider=... 切换手动,非自动
换一个 agentOpenCode 直接从环境变量读取 provider 凭据,无需登录步骤不同工具、不同默认值,见正面对比

对自动化配置来说,网关路径是去掉可动部件最多的那个,因为没有会过期的登录步骤。Ofox 就能当这样一个网关:它暴露了 /v1/responses,因此 wire_api = "responses" 的要求得到满足,而且一个 OFOX_API_KEY 就能触达来自多个供应商的模型,这意味着某个上游出了凭据问题,你也不至于无路可走。支持情况是按模型而非按网关来的,所以在承诺投入前先查一下你想用的那个模型。配置块就是成因 3 里展示的那个,带模型 ID 的更完整版本在 Codex CLI API 配置指南 里。

如果你是从 Claude Code 过来、想对比两者的认证使用体验,从 Claude Code 迁移到 Codex 讲了哪些能沿用、哪些必须重新配置。

怎样避免旧病复发

几个能防住大多数重复事故的习惯:

用真实请求验证,别用目录调用。 把这个放进你的安装脚本,而不是 /v1/models 探测:

code=$(curl -s -o /dev/null -w "%{http_code}" -X POST https://api.ofox.io/v1/responses \
  -H "Authorization: Bearer $OFOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"openai/gpt-5.5","input":"ping","max_output_tokens":16}')
[ "$code" = "200" ] || echo "auth check failed: HTTP $code"

在导出的那一刻就防住换行符,别等事后:

export OFOX_API_KEY="$(tr -d '\n\r' < ~/keys/ofox.txt)"

绝不在镜像里携带 auth.json,也不要在机器之间同步它。 一次性刷新 token 在设计上就让那成了一场竞态。改用环境注入一个密钥。

任何配置改动后都运行 codex doctor 它能在约一秒内抓出 CODEX_HOME 用错和配置没解析成功这两种情况,而这正是最容易让你去怀疑一个本来完好的密钥的两种失败。

来源