详细分析 Codex CLI 出现 401 错误的 9 种原因,从环境变量到 API 密钥,提供逐步排查方法。
TL;DR:Codex CLI 身份验证故障有九种不同形式,其中只有部分以字面上的 401 形式出现,这正是为什么状态码是输出中最无用的部分。消息体才是识别原因的关键。header 中缺少 bearer 或 basic 认证意味着根本没有发送任何内容,令人惊讶的是,在默认提供商上导出 OPENAI_API_KEY 并无帮助。"Incorrect API key provided"(提供了错误的 API 密钥)意味着你的密钥已到达但被拒绝。"You didn't provide an API key"(你没有提供 API 密钥)意味着 header 在传输中被丢弃,几乎总是因为值以换行符结尾。本地"Missing environment variable"(缺少环境变量)错误根本不是 401,base_url 缺少 /v1 的 404 也不是。下面的每一种情况都在 2026-07-30 的 Codex CLI 0.146.0 上重现过。
按这个顺序做三项检查。大多数人跳过第一项,花二十分钟重新签发一个本来没问题的密钥。
第 3 步中有一个陷阱值得提前指出,因为互联网上一半的故障排除建议都搞错了:不要用 /v1/models 作为聚合器上的密钥检查。在 2026-07-30 测试,https://api.ofox.ai/v1/models 用虚假的密钥返回 200 和完整目录,也用完全没有 Authorization header 返回。模型目录是公开的。OpenAI 自己的 api.openai.com/v1/models 在没有密钥时确实返回 401,这是这个习惯的来源,但这个习惯不能转移。使用一个实际运行推理的端点。
当你知道是哪一种身份验证故障时,修复成本很低;当你猜测时成本很高。粗略规则:
修复它 当消息名称了具体原因时:Missing environment variable、Incorrect API key provided,或任何提到刷新令牌的内容。这些有确定性的一步修复,每项花费不到两分钟。
切换认证路径 当你在 ChatGPT 登录流上已尝试三次时。API 密钥路径活动部分较少(没有刷新令牌,没有浏览器往返,没有 8 天刷新窗口),如果你在自动化任何东西,这是唯一在容器重启后幸存的路径。
停止并检查其他内容 如果你得到的是 404 而不是 401,或者 CLI 拒绝因配置错误而启动。这些不是身份验证问题,再多的密钥轮换也解决不了。如果错误提到使用限制而不是授权也是一样。
唯一重新签发密钥是正确第一步的情况是 Incorrect API key provided,且错误中显示的掩码后缀与你认为使用的密钥匹配。如果后缀不匹配,你有配置问题而不是密钥问题,新密钥会失败得完全相同。
这是核心表格。每一行都是针对实时端点重现的。
测试运行中的两个细节使输出更容易阅读。在默认 OpenAI 提供商上,Codex 首先尝试针对 wss://api.openai.com/v1/responses 的 WebSocket 传输,重试五次,然后回退到 HTTPS 再重试五次,所以单个认证失败在真实消息之前产生大约十行错误。在自定义提供商上,WebSocket 传输是关闭的(codex doctor 报告 supports websockets: false),所以你得到一个重试循环和更清晰的失败。如果你盯着 Reconnecting... 4/5 的错误墙,滚动到底部;最后一行是重要的那一行。
这是最常见的一个,而且它足够反直觉,值得排在第一位。
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 的默认提供商从 $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
环境变量确实有效,但只能通过自定义提供商的 env_key 字段,这是下面进一步涵盖的一个不同机制。
如果你遵循了 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-..."
# }
使用自定义提供商块,Codex 从你命名的环境变量读取密钥:
model = "openai/gpt-5.5"
model_provider = "ofox"
[model_providers.ofox]
name = "Ofox"
base_url = "https://api.ofox.ai/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() 过滤变量。所以导出 OFOX_API_KEY="" 和从未导出它对 Codex 来说是同一回事。
这个在 launchd、systemd 和 Docker 中咬得最硬,那里启动 Codex 的 shell 不是你导出变量的 shell。
这是最令人讨厌的,因为错误指控你没有提供一个你用自己眼睛看得到的密钥。
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.ai]
header 用换行符内部构建并被扔掉了。服务器真的从未看到凭证,所以它的消息是准确的;它只是听起来像你忘记设置任何东西。
值得知道什么不是原因这里,因为它是明显的怀疑对象而且是无辜的:尾随空格是好的。用 export OFOX_API_KEY="$REAL " 测试过,请求成功了。注意 Codex 没有为你清理它:api_key() 中的 trim() 只是一个空性检查,它返回的值是原始值,尾随空格包括在内。某些下游东西容许它。换行符,相比之下,彻底破坏了 header。无论如何,不要花时间追踪杂散空格。
另一方面,一个被引用的值确实会失败,有不同的措辞:
export OFOX_API_KEY='"sk-..."' # literal quote characters in the value
# ERROR: unexpected status 401 Unauthorized: Invalid or expired API key
当用一个天真的 export $(cat .env | xargs) 加载 .env 文件时保留引号时,那个很常见。
在导出时剥离有问题的字节并确认长度:
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
九个中最安静的故障。从上面的配置中删除一行:
[model_providers.ofox]
name = "Ofox"
base_url = "https://api.ofox.ai/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.ai/v1/responses
你的环境变量设置正确。你的密钥有效。错误说密钥无效,因为发送了不同的密钥。在同一 shell 上用两种方式测试过:env_key 出现时请求返回一个正常完成,删除该行时它 401 了。
反过来的情况也值得了解,而且这是个好消息:当 env_key 存在时,它的优先级高于 auth.json。经测试,即使在 auth.json 中故意保存一个无效密钥,并在环境变量中设置有效密钥,请求仍能成功。配置自定义提供商前,无需先退出登录。
如果你使用 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 在这里会输出五种变体之一,而且它们不能混为一谈。以下内容来自 rust-v0.146.0 标签下的 login/src/auth/manager.rs:
其中,“already used”这一变体最让人意外。刷新令牌只能使用一次,因此,如果你把 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 密钥方式。它不存在可能失去同步的刷新机制。
codex login status 告诉你一切正常它会以两种不同的方式误导你,而且这两种情况都已复现。
使用手动构造、包含过期 ChatGPT 令牌的 auth.json 时,每个请求都会因上述刷新错误而失败,但运行:
codex login status
# Logged in using ChatGPT
而在自定义提供商下,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、配置是否解析成功、认证存储模式,以及它能看到哪些认证环境变量;Connectivity 部分会报告当前使用的提供商、wire API、是否适用 WebSocket 传输,以及端点是否可访问。它仍然不会验证凭据,但如果 Codex 读取的配置文件并不是你一直在编辑的那个,它能在一秒内告诉你。当设置了 CODEX_HOME 时,这出人意料地经常是问题的根本原因。
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 令牌。
CODEX_HOME 会同时改变 config.toml 和 auth.json 的位置。如果它是在 shell 配置文件、包装脚本,或某个替你启动 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
}
如果 base_url 缺少 /v1,你得到的是 404,而不是 401:
ERROR: unexpected status 404 Not Found: 404 page not found,
url: https://api.ofox.ai/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 的端点。这个问题经常困扰从旧配置迁移过来的用户,而且由于它会在启动时终止进程,有时会被归类为“升级后认证坏了”。事实并非如此。
还有第三种相似情况:如果你位于企业代理之后,失败形式通常是 TLS 错误或连接错误,而不是 401,修复方法也完全不同。
相同的症状需要根据认证方式采用不同的修复方法。下面这张表值得收藏。
特别是对于容器和 CI 环境:不要从 ~/.codex 挂载任何内容,在容器环境中设置变量,并使用包含 env_key 的自定义提供商配置块。这种组合没有刷新状态,也不存在可能过期的文件。
以上所有内容汇总在一张表格中。这些结果于 2026-07-30 在 macOS 上使用 Codex CLI 0.146.0 复现;默认提供商相关行使用 api.openai.com,自定义提供商相关行使用 api.ofox.ai。
第 9 行和第 12 行能通过排除错误方向来节省时间。如果你一直在排查空白字符,或者认为配置网关之前必须先退出登录,那么这两个方向都是死胡同。
如果障碍来自认证路径本身,而不是拼写错误,你还有几个选择。
对于自动化环境,网关方案能消除最多的不稳定因素,因为它没有会过期的登录步骤。Ofox 就可以充当这样的网关:它提供 /v1/responses,因此满足 wire_api = "responses" 的要求;一个 OFOX_API_KEY 就能访问多个供应商的模型,这意味着即使某个上游出现凭据问题,你也不至于完全无模型可用。支持情况取决于具体模型,而不是网关本身,因此在决定采用前,请先检查你想使用的模型。配置块与“原因 3”中展示的相同。
下面这些习惯可以避免大多数重复发生的问题:
使用真实请求进行验证,而不是调用模型目录接口。请将以下内容放入设置脚本,而不是通过 /v1/models 发送 ping:
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST https://api.ofox.ai/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 或在机器之间同步它。单一使用的刷新令牌在设计上就是一场竞速。改为通过环境注入密钥。
在任何配置更改后运行 codex doctor。它大约在一秒钟内捕获 wrong-CODEX_HOME 和 config-did-not-parse 情况,这两个故障最可能让你对一个完全有效的密钥产生怀疑。
原文发布于 ofox.ai/blog。
如需进一步操作,你可以考虑封禁此人和/或举报滥用行为