跑 codex 的时候,多数人只记得两件事:敲提示词,回车。真正决定它"能改哪些文件、要不要问你、走哪个模型端点"的东西,全在一个 config.toml 里。这个文件不配也能跑,所以很多人从来没打开过它——直到某天发现它在改 ~/.ssh,或者公司内网模型怎么都不通。
这篇按"配置放哪 → 写什么段 → 三档审批到底差在哪 → 怎么验证生效"的顺序拆开,最后给一张踩坑表。
1. 配置放在哪,谁盖住谁
按当前版本核对,Codex 的配置分用户级与项目级两层,另有环境变量与命令行参数可以从外部覆盖。
$ ls -l ~/.codex/
config.toml # 用户级主配置
auth.json # 凭证(由登录流程写入,不要手改)
sessions/ # 会话记录
$ ls -l ./.codex/
config.toml # 项目级配置,跟仓库一起进 Git
| 层 | 路径 | 提交进仓库 | 典型内容 |
|---|---|---|---|
| 用户级 | ~/.codex/config.toml |
否 | 模型、provider、审批档位、个人 MCP |
| 项目级 | <repo>/.codex/config.toml |
是 | 项目约定、额外只读目录、项目 MCP |
| 环境变量 | OPENAI_API_KEY 等 |
否 | 密钥、临时覆盖 |
| 命令行参数 | codex -c key=value |
否 | 单次运行的临时覆盖 |
优先级从下往上:命令行 > 项目级 > 用户级 > 内置默认值。合并粒度是"键"而不是"整个文件",所以项目级只写要改的那几项就行,不必复制整份。
这里有个反直觉的点:项目级配置不是无条件生效的。仓库要被标记为"可信",项目级配置才会被读进来。见第 5 节。
2. 常用配置段:一张总览
先看全貌,再逐段展开。
| 段 | 键 | 作用 |
|---|---|---|
| 顶层 | model、model_provider |
默认模型与走哪个 provider |
| 顶层 | approval_policy |
什么时候停下来问你 |
| 顶层 | sandbox_mode |
命令在什么隔离级别下跑 |
| 顶层 | model_reasoning_effort |
推理投入档位 |
[model_providers.*] |
base_url、env_key、wire_api |
自定义端点 |
[sandbox_workspace_write] |
writable_roots、network_access |
沙箱可写范围与联网 |
[projects."/path"] |
trust_level |
目录信任级别 |
[mcp_servers.*] |
command、args、env |
MCP 服务注册 |
[shell_environment_policy] |
inherit、include_only |
传给子进程的环境变量 |
一个能跑的骨架长这样:
model = "MODEL_NAME"
model_provider = "corp"
approval_policy = "on-request"
sandbox_mode = "workspace-write"
model_reasoning_effort = "medium"
[model_providers.corp]
name = "Corp Gateway"
base_url = "https://gateway.internal/v1"
env_key = "CORP_API_KEY"
wire_api = "chat"
[sandbox_workspace_write]
writable_roots = ["/srv/app", "/tmp"]
network_access = false
[projects."/srv/app"]
trust_level = "trusted"
[mcp_servers.docs]
command = "npx"
args = ["-y", "@scope/docs-mcp"]
env = { DOCS_TOKEN = "env:DOCS_TOKEN" }
模型名和 provider 名是两个键。 只写 model 不写 model_provider 会走内置默认端点;写了个 provider 名却没在 [model_providers.*] 里定义,启动报"未知 provider"。
3. 自定义 provider:key 不进文件
内网网关、代理中转、自建推理服务,都靠这一段接进来。
[model_providers.gateway]
name = "Gateway"
base_url = "https://gateway.internal/v1"
env_key = "GATEWAY_API_KEY"
wire_api = "responses" # 或 "chat",取决于端点实现
query_params = { api-version = "v1" }
http_headers = { "X-Team" = "platform" }
| 键 | 含义 | 常见错法 |
|---|---|---|
base_url |
端点前缀,通常带到 /v1 |
带上具体路径 /chat/completions,被二次拼接 |
env_key |
从哪个环境变量读 key | 直接写 api_key = "sk-...",key 进 Git |
wire_api |
请求协议形态 | 与端点不符,表现为 400 或字段不认识 |
http_headers |
附加请求头 | 网关要鉴权头,只配了 key |
env_key 的语义是"引用变量名",不是"存值"。所以:
# 对的:变量在环境里,文件里只有变量名
export GATEWAY_API_KEY="$(pass show corp/gateway)"
codex
# 错的:key 写进 config.toml,然后 commit
api_key = "sk-live-xxxxxxxx"
env_key 指向的变量不存在时,报错出现在第一次请求而不是启动时。表现为"启动正常,一发提示词就 401",很容易被误判成 key 过期。
4. 审批三档:语义差在哪
approval_policy 决定的不是"能不能执行",而是"执行前停不停"。
| 档位 | 语义 | 适用场景 | 代价 |
|---|---|---|---|
每次确认(untrusted 一类) |
只读操作放行,写文件/跑命令都要问 | 首次接触陌生仓库、生产机 | 交互多,长任务会被打断几十次 |
自动(on-request / on-failure 一类) |
沙箱内自主执行,越界或失败才问 | 日常开发的主力档位 | 沙箱边界配错时,破坏发生得很快 |
全自动(never 一类) |
不问,直接执行 | CI、容器、一次性任务 | 无人值守时出错没人拦 |
具体枚举值与默认值按当前版本核对,各版本命名迭代过。单次覆盖:codex -c approval_policy=on-request。
判断标准只有一条:这次运行有没有人在旁边看着。有人在,用自动档;没人在,才考虑全自动,并且必须配合容器或一次性工作目录。
把
never当成"省事模式"是最贵的误用。它的真实含义是"把判断责任从人转移到了沙箱配置和前期的提示词上",而这两样一旦没配好,就没有第二道闸。
5. 沙箱与网络:两个开关的取舍
sandbox_mode 管"写到哪、能不能联网",与审批档位是两个独立维度。
| 模式 | 可写范围 | 网络 | 适合 |
|---|---|---|---|
read-only |
无 | 默认关 | 代码分析、出方案、审计 |
workspace-write |
工作目录 + writable_roots |
由 network_access 定 |
日常改代码 |
danger-full-access |
不限 | 开 | 容器内、一次性环境 |
[sandbox_workspace_write]
writable_roots = ["/srv/app", "/srv/app/.cache"]
network_access = true
network_access = false时,npm install、pip install、go mod download全失败。表现不是"没网",而是依赖装不上、构建卡住。要装依赖就临时开,装完关。writable_roots写太宽(加个/)等于沙箱失效,但审批档位还显示"自动",人会误以为安全。- 项目信任设置是另一道闸:
[projects."/path"] trust_level。未标记可信的仓库,项目级段不生效——这是"我配了 MCP 它说没有"的头号原因。
[projects."/srv/app"]
trust_level = "trusted"
[projects."/srv/scratch"]
trust_level = "untrusted" # 只读分析用
6. 多环境切换:profile 还是分目录
要在"公司网关 / 本地小模型 / 离线只读"之间来回切,两种做法:
做法一:profile 段。 差异收进独立段,运行时选一个。
[profiles.corp]
model = "MODEL_NAME"
model_provider = "corp"
approval_policy = "on-request"
[profiles.offline]
model = "LOCAL_MODEL"
model_provider = "local"
approval_policy = "untrusted"
sandbox_mode = "read-only"
$ codex --profile corp
$ codex --profile offline
做法二:分目录。 用 CODEX_HOME 指向不同目录,各自一份 config.toml。
$ CODEX_HOME=~/.codex-corp codex
$ CODEX_HOME=~/.codex-lab codex
| 做法 | 优点 | 缺点 |
|---|---|---|
| profile | 单文件、易 diff、易评审 | 段多起来后互相引用容易乱 |
| 分目录 | 环境彻底隔离,密钥各管各的 | 公共段要复制,容易漂移 |
7. 校验与生效
改完必须验证,别靠"看起来对"。
$ codex --help # 看当前版本支持哪些键与枚举
$ codex -c model=OTHER_MODEL # 临时覆盖,验证键名是否被识别
$ codex -c approval_policy=bogus # 故意写错,看是否报错
最后一条是关键测试:写一个明显非法的值,若它静默接受,说明这个键根本没被读。TOML 解析失败时的行为在不同版本下不一样,有的直接报错退出,有的回退默认值继续跑——所以"配置没生效"和"配置写错了"在界面上可能长得一模一样。
生效时机:config.toml 是启动时读取。改完必须重开会话;长驻的会话不会热加载。
8. 排错:六种"配了但没生效"
| 现象 | 原因 | 处理 |
|---|---|---|
| 启动正常,一用就 401 | env_key 指向的变量不存在 |
echo $VAR 确认;注意 GUI/IDE 启动的环境与 shell 不同 |
| 报未知 provider | model_provider 写了名字但没定义对应 [model_providers.*] |
补段,或删掉 model_provider 走默认 |
| 项目级配置完全没反应 | 仓库未被标记可信,项目段被跳过 | 在 [projects."/path"] 写 trust_level,或走首次信任提示 |
| 改了文件行为没变 | 会话未重启;或 TOML 静默回退默认 | 重开会话;用非法值测试键是否被读取 |
npm install 一直失败 |
network_access = false |
临时开启或改用宿主预装依赖 |
| 启动直接报解析错误 | TOML 语法错:重复表、键写在表之后、字符串未闭合 | python -c "import tomllib;tomllib.load(open('config.toml','rb'))" 单独验 |
9. 边界:这些事不该指望配置文件
| 需求 | 配置文件能做什么 | 该用什么 |
|---|---|---|
| 团队统一模型与审批档位 | 能,项目级 + 评审 | 仓库内 config.toml |
| 密钥分发与轮换 | 不能,只能引用变量 | 密钥管理器 / CI secret |
| 强制"不许改某个目录" | 弱,read-only + writable_roots 是粗粒度 |
文件权限、容器挂载、CI 只读 checkout |
| 审计"谁在什么时候改了什么" | 不能 | Git 提交 + CI 日志 |
| 让模型一定按规范写代码 | 不能 | 仓库内约定文件 + 评审 |
| 隔离不可信依赖的构建 | 不能 | 容器 / 一次性 VM |
一句话:config.toml 管的是"工具的旋钮",管不了"人的流程"。 把团队规范写进配置,最后得到的是一个没人敢改的文件和一堆绕过它的命令行参数。
10. 自测
- 用户级和项目级配置同时存在,项目级只写了
model,其他键从哪来? env_key里应该放什么?为什么不能直接写 key?- 项目级
config.toml完全不生效,最可能被什么拦住? - 审批"全自动"和沙箱"可写范围"是两个维度,各管什么?
- 怎么用一条命令判断某个配置键是否真的被当前版本读取?
read-only沙箱下跑npm install会发生什么,为什么?