2660 字
约 8 分钟
17
S24-X09 · Codex 的 config.toml:一个文件管住所有旋钮

跑 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. 自测

  1. 用户级和项目级配置同时存在,项目级只写了 model,其他键从哪来?
  2. env_key 里应该放什么?为什么不能直接写 key?
  3. 项目级 config.toml 完全不生效,最可能被什么拦住?
  4. 审批"全自动"和沙箱"可写范围"是两个维度,各管什么?
  5. 怎么用一条命令判断某个配置键是否真的被当前版本读取?
  6. read-only 沙箱下跑 npm install 会发生什么,为什么?
S24-X09 · Codex 的 config.toml:一个文件管住所有旋钮
https://www.clxhxhhr.top/posts/5701/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。