规范文件
Codex 的 AGENTS.md 加载链:全局、项目与 override 文件怎样核对
按官方规则逐级核对 Codex 会读哪些 AGENTS.md,用离线脚本演算顺序与大小上限。
Codex 启动时按固定顺序拼出一条指令链:先读 Codex home 里的全局文件,再从项目根目录逐级走到当前目录。改了 AGENTS.md 却没生效,常见原因是被同目录的 AGENTS.override.md 盖住、文件为空、合并后超出大小上限,或 CODEX_HOME 指向了别处。本文依据官方文档把这些规则写成一个离线演算脚本,用临时目录核对哪些文件会进链、顺序如何。预期产物是你自己仓库的一张加载清单。脚本只是对文档规则的演算,没有启动 Codex,也没有调用模型。
文件位置
| 路径 | 适用范围 | 读取方式 |
|---|---|---|
~/.codex/AGENTS.override.md | 全局临时覆盖;存在且非空时,同级的 AGENTS.md 不再读取。 | 启动时读取;Codex home 默认是 ~/.codex,可用 CODEX_HOME 改变。[1] |
~/.codex/AGENTS.md | 全局默认指令,对所有仓库生效。 | 没有非空的 AGENTS.override.md 时读取。[1] |
./AGENTS.md | 仓库根目录的项目指令。 | 从项目根走向当前目录时逐级检查;每个目录最多取一个文件。[1] |
./services/payments/AGENTS.override.md | 子目录里的专用覆盖;官方示例中它让同目录的 AGENTS.md 被忽略。 | 位置越靠近当前目录,在合并后的提示里越靠后,因此优先级越高。[1] |
~/.codex/config.toml | 保存 project_doc_fallback_filenames 与 project_doc_max_bytes。 | 修改后重启 Codex 或开始新的运行才会重新读取。[1][2] |
什么时候用
- AGENTS.md 已修改但 Codex 的行为没有变化,需要判断哪些文件进入了指令链。[1]
什么时候不用
- 需要的是 Claude Code 或 Cursor 的指令加载规则,或希望靠指令文件做强制的权限控制。[1]
官方规则:全局、项目、合并三步
- 全局:在 Codex home(默认 ~/.codex,可由 CODEX_HOME 改变)里先找 AGENTS.override.md,没有再找 AGENTS.md,只取首个非空文件。
- 项目:从项目根目录(通常是 Git 根)走到当前目录;找不到项目根时只检查当前目录。每个目录依次找 AGENTS.override.md、AGENTS.md,再找 project_doc_fallback_filenames 里的名字,每个目录最多取一个文件。
- 合并:从根往下用空行拼接,离当前目录越近的文件越靠后,所以优先级越高。官方还说空文件会被跳过,合并大小达到 project_doc_max_bytes(默认 32 KiB)后不再追加。
- 重建时机:指令链在每次运行启动时重建(TUI 通常是每个会话一次),没有缓存需要手动清理。
用脚本演算加载顺序
下面的脚本不依赖 Codex:它接收项目根、当前目录和可选的字节上限,按上面的规则打印每个文件是否进链。Codex home 取自 CODEX_HOME,fallback 文件名取自环境变量 FALLBACKS(逗号分隔),都不读取真实的 config.toml。需要 Node.js,建议在一个临时目录里自建一棵测试树再运行。[1]
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const [root, cwd, limitArg = "32768"] = process.argv.slice(2);
const limit = Number(limitArg);
const fallbacks = (process.env.FALLBACKS ?? "").split(",").filter(Boolean);
const home = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
const firstNonEmpty = (dir, names) => {
for (const name of names) {
const file = path.join(dir, name);
if (fs.existsSync(file) && fs.statSync(file).size > 0) return file;
}
return null;
};
const dirs = [];
for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) {
dirs.unshift(dir);
if (dir === path.resolve(root)) break;
if (dir === path.dirname(dir)) throw new Error("cwd is not inside root");
}
const chain = [firstNonEmpty(home, ["AGENTS.override.md", "AGENTS.md"])];
for (const dir of dirs) chain.push(firstNonEmpty(dir, ["AGENTS.override.md", "AGENTS.md", ...fallbacks]));
let total = 0;
for (const file of chain.filter(Boolean)) {
if (total >= limit) { console.log("not added (limit reached):", file); continue; }
total += fs.statSync(file).size;
console.log(total > limit ? "added, crosses limit:" : "added:", file, total);
}
官方示例有全局文件、仓库根 AGENTS.md,以及 services/payments 下同时存在 AGENTS.md 与 AGENTS.override.md。我在临时目录里照此建了一棵树并跑了四组:全局 AGENTS.md 内容是「# global」(9 字节),根 AGENTS.md 是「# root」(7 字节),payments 下的 AGENTS.override.md 是「# payments override」(20 字节),同目录的 AGENTS.md 应被忽略;另有一个 override 为空文件的 search 目录;第四组把根 AGENTS.md 删掉并用 FALLBACKS=TEAM_GUIDE.md 演算 fallback 文件名;第三组把上限设为 20 字节。每行末尾的数字是累计字节数,不是单个文件大小,临时目录路径已替换为 TMP。官方文档没有说明跨线的那个文件是被截断还是整体丢弃,所以脚本只标记「跨线」。[1]
== case 1: payments, no fallbacks
added: TMP/codex-home/AGENTS.md 9
added: TMP/repo/AGENTS.md 16
added: TMP/repo/services/payments/AGENTS.override.md 36
== case 2: search (empty override skipped by script assumption)
added: TMP/codex-home/AGENTS.md 9
added: TMP/repo/AGENTS.md 16
added: TMP/repo/services/search/AGENTS.md 30
== case 3: limit 20 bytes
added: TMP/codex-home/AGENTS.md 9
added: TMP/repo/AGENTS.md 16
added, crosses limit: TMP/repo/services/payments/AGENTS.override.md 36
== case 4: only fallback name at root, FALLBACKS=TEAM_GUIDE.md, root without AGENTS.md
added: TMP/codex-home/AGENTS.md 9
added: TMP/repo/TEAM_GUIDE.md 22
added: TMP/repo/services/payments/AGENTS.override.md 42
没有覆盖的部分与待核对项
- 空的 AGENTS.override.md 是否让该目录回落到 AGENTS.md:文档写的是「跳过空文件」「全局只取首个非空文件」。脚本据此让空 override 回落,这是推断,需要用真实 Codex 确认。
- 大小上限的口径:AGENTS.md 指南说的是合并大小达到上限后停止追加;Advanced Configuration 页把 project_doc_max_bytes 描述为每个 AGENTS.md 文件读取多少。两处措辞并不完全一致,脚本按指南页的合并口径演算,不要据此判断截断位置。
- 项目根的判定:文档只写「通常是 Git 根」。脚本把根作为参数传入,不做识别,也不处理信任状态和 profile。
- 脚本不解析 config.toml;改了 project_doc_fallback_filenames 之后,应重启 Codex 并核对真实的加载结果。
在真实产品里核对与停止条件
官方给出的核对方式之一,是在仓库根运行 codex --ask-for-approval never "Summarize the current instructions.",看回答是否按优先级引用全局和项目文件;排查「错误指令出现」时先找 Codex home 或上层目录里的 override 文件,排查「什么都没加载」时先确认文件非空、当前目录在预期仓库里。本文没有执行这些命令。出现「加载清单与预期不符」时,先按 override、空文件、CODEX_HOME、fallback 名拼写、大小上限的顺序逐项排除,再改指令内容;不要为了让某条指令生效而反复加长根目录文件。本文只核验了官方说明,没有启动 Codex、读取真实的指令文件或调用模型。[1]
来源
以下均为官方页面,核验日期见各条;链接指向核验时的版本。