AGENTS.md 与 CLAUDE.md:同一份项目说明书,四个工具的读法都不一样
先说清楚这篇要解决的那个具体问题。你有一个 monorepo,根目录写了一份 AGENTS.md,里面是构建命令、目录约定、几条「永远不要这么干」。团队里有人用 Claude Code,有人用 Codex,你自己在试 DeepSeek Harness,还有人在跑 Pi。于是有人顺手加了一个 CLAUDE.md 软链到 AGENTS.md,觉得这样就都照顾到了。
结果并不是。这四个工具对「项目说明书」的读法,在文件名候选、向上走到哪一层停、子目录里的那份什么时候进上下文、总共允许多少字节这四件事上都不同。下面按各自能核到的源码与官方文档走一遍。先提醒一句限定:DeepSeek Harness 仓库的 README 自述处于开发者预览阶段并明写会有破坏兼容性的变更,本文提到的配置字段与默认值随时可能变动。
DeepSeek Harness:一个目录里所有候选都读,读完再按内容去重
这块能力在 packages/context/agent-instructions/ 这个包里,不在 agent 主循环里。src/config.ts 顶部四个常量把默认行为写死得很清楚:DEFAULT_PROJECT_ROOT_MARKERS 是 ['.git'],DEFAULT_INSTRUCTION_FILE_CANDIDATES 是 ['AGENTS.md', 'CLAUDE.md'],DEFAULT_LOCAL_INSTRUCTION_FILE_CANDIDATES 是 ['AGENTS.local.md', 'CLAUDE.local.md'],DEFAULT_MAX_SOURCE_BYTES 是 1_048_576,也就是单文件 1 MiB。
发现顺序在 src/files.ts 的 discoverInstructionFiles() 里:先探 $DSH_HOME/AGENTS.md($DSH_HOME 默认 ~/.dsh,README 写明这个用户全局文件固定叫 AGENTS.md,没有 local 覆盖层),然后用 findProjectRoot() 从 cwd 往上找第一个含根标记的目录,再由 ancestorChain() 生成从项目根到 cwd 的目录链,每一层先跑一遍基础候选、再跑一遍 local 覆盖候选。
关键的一处在于:同一个目录里所有存在的候选都会被读进来,不是取第一个命中就收手。真正的收敛发生在读完之后的 dedupInstructionFilesByDirectory()——它按目录分组,用 trimmedInstructionDigest()(去掉首尾空白后的 SHA-1)做键,同一目录里内容相同的后来者被折叠掉,保留配置顺序里最早的那个。包 README 把这句话写得很直白:内容相同的 CLAUDE.md 和 AGENTS.md 只渲染一次,而且渲染的是 AGENTS.md;两份内容确实不同的兄弟文件,则都会生效。
顺带一提,deepseek-harness 仓库自己就是这么用的。我们数了一下:仓库里追踪的 AGENTS.md 共 19 个,其中 4 个在 examples/acp-agent/tests/snapshots/ 下面的测试夹具 workspace 里,剩下 15 个是真的约定文件;CLAUDE.md 有 5 个,git 索引里的模式位全是 120000,内容就是 AGENTS.md 这九个字节——全是软链。而根目录 AGENTS.md 的「Editing these instructions」一节写的是 CLAUDE.md 在 root、packages/、examples/ 三处软链 AGENTS.md,与我们数出的 5 处(另两处在 vendor/ 和 .agents/notes/implemented/)不一致,此处以实读为准,不再延伸。
符号链接能被读到,是因为 files.ts 里两条 stat 路径都用的是跟随最终一段链接的语义:Node 侧用 stat 而不是 lstat,provider 侧先 resolve() 再 stat(),注释写明链到普通文件就加载目标内容,链到目录或断链算「确认不存在」,而 resolve/stat 抛异常算「暂时不可用」——后者不会被当成文件被删。测试夹具里 examples/acp-agent/tests/snapshots/agent-instructions/workspace/AGENTS.md 本身就是一个指向 AGENTS.canonical.md 的软链,那个场景的 projectRootMarkers 还被覆盖成了 .dsh-project。
预算:三个数值分别管什么
Config 里 maxBytes 是必填的,README 自述的理由是让每个部署显式做出提示词预算选择。核到的实际取值:apps/cli/config/agent-presets/ 下 standard、code、cordis 三个 preset 以及 packages/bundle/base/cordis.patch.yml 里都写的 maxBytes: 65536;而 packages/bundle/web-app/cordis.patch.yml 直接把 agent-instructions 这一行标成了 disabled: true。也就是说同一套代码,在 web 那套 bundle 里这个能力是关掉的。
超预算时的处置顺序写在 src/render.ts:先整份丢掉更宽泛的文件,最具体的那份留到最后才截断,并且会在渲染文本里带一行可见提示,模板是 Workspace instruction budget ${maxBytes} bytes:,后面跟 omitted <路径> 与 truncated <路径> from <原字节> to <保留字节> bytes。这行提示是模型能看见的。
还有一处得照实标出来:src/files.ts 的 readBounded() 上方挂着 TODO(total-instruction-read-bound) 注释,写明目前只有单文件的 maxSourceBytes 上限,尚未对一次 baseline 或一次对账批次的总读取字节做聚合约束,渲染预算是在所有文件都读完之后才施加的。同一文件里还有一个 TODO(root-marker-unavailable),写的是根标记探测失败时目前按「不存在」继续往上走。这两条是源码里自己写的待办,不是已完成的能力。
Codex:默认只认 AGENTS.md,一层只取一个
Codex 仓库里 docs/agents_md.md 只有一句话,指向线上文档;机制要看 codex-rs/core/src/agents_md.rs,它开头的模块注释把规则写全了:从 cwd 往上找到第一个含 project_root_markers 的目录当项目根,收集项目根到 cwd(含两端)的所有 AGENTS.md 并按该顺序拼接,不越过项目根。默认标记同样是 .git,定义在 codex-rs/config/src/project_root_markers.rs 的 DEFAULT_PROJECT_ROOT_MARKERS。
候选名由 candidate_filenames() 组装,顺序是 AGENTS.override.md、AGENTS.md,然后才是配置项 project_doc_fallback_filenames 里的名字——而 codex-rs/config/src/config_toml.rs 里 default_project_doc_fallback_filenames() 返回的是空数组。所以默认配置下,Codex 不读 CLAUDE.md。而且 agents_md_paths() 里对每个目录是循环候选、命中即 return,一层只取一个文件,和 DeepSeek Harness 的「全读再去重」正好是两种思路。
字节预算是 DEFAULT_PROJECT_DOC_MAX_BYTES = 32 * 1024,即 32 KiB,在 core/src/config/mod.rs 里以 AGENTS_MD_MAX_BYTES 的名字作为 project_doc_max_bytes 的兜底值。这是整条链路的总额度:read_agents_md() 拿着 remaining 逐个文件递减,读到超额的那份直接 data.truncate(remaining),并打一条 project doc exceeds remaining budget; truncating 的 warn 日志。用户级的那份在 Codex home 目录下,由 codex-rs/codex-home/src/instructions/mod.rs 按同样的 AGENTS.override.md → AGENTS.md 顺序加载。拼接分隔符也是常量,AGENTS_MD_SEPARATOR 的值是 \n\n--- project-doc ---\n\n。
Claude Code:官方文档明写它不读 AGENTS.md
Claude Code 是闭源产品,只能看官方文档,这里不推断实现。它的记忆文档里有一节标题就叫 AGENTS.md,第一句是 Claude Code 读 CLAUDE.md、不读 AGENTS.md;给出的兼容做法是建一个 CLAUDE.md,里面用 @AGENTS.md 把它导入,再在下面追加 Claude 专属的指令。文档同时写明,如果不需要追加内容,软链也可以,但在 Windows 上创建软链需要管理员权限或开发者模式,因此官方在 Windows 侧建议改用 @AGENTS.md 导入。导入语法本身还有两条硬规则:相对路径按包含该导入的文件解析而不是按工作目录,递归导入最多 4 跳;反引号包起来的 `@README` 不会被当成导入。
加载范围上,官方文档写的是从当前工作目录沿目录树向上遍历,逐层查 CLAUDE.md 与 CLAUDE.local.md,全部拼接而不是互相覆盖,顺序从文件系统根往下到工作目录,同目录内 CLAUDE.local.md 排在 CLAUDE.md 之后。至于向上遍历以什么标记为界,我们在这份文档里没有找到对应说明,不与前两家的 .git 标记对比。子目录里的 CLAUDE.md 则不在启动时加载,文档写明是在 Claude 读取那些子目录里的文件时才纳入。
长度这一项的口径也不一样:官方文档写的是工作目录之上那条目录链里的 CLAUDE.md 与 CLAUDE.local.md 在启动时「完整加载」(loaded in full),而 200 行是文档给出的建议上限(原文用的是 target under 200 lines,理由写的是更长的文件占更多上下文、并会降低遵循度),不是一个会触发截断的硬阈值;我们在这份文档里没有找到 CLAUDE.md 的字节上限说明。真正写明 200 行或 25KB 硬限制的是自动记忆的 MEMORY.md 索引——文档写的是每次会话开始只加载前 200 行或前 25KB(以先到者为准),超出部分不加载,那是另一套机制。monorepo 里捡到了别的团队的 CLAUDE.md,文档给的开关是 claudeMdExcludes,按绝对路径 glob 排除,但托管策略层的那份排除不掉。另外文档还写明 /compact 之后项目根的 CLAUDE.md 会被重新读盘注入,而子目录里的嵌套文件和带 paths: 前置元数据的规则不会自动重注入,要等下一次读到匹配的文件。.claude/rules/ 这套可以按 paths: 前置元数据作用域加载的规则,只在 Claude Code 的文档里有;另外三家里,DeepSeek Harness 的包 README 反过来把这件事写死了——它明说候选语义「有意保持很小」,小写文件名、.claude/rules/、@path 导入都不解释,另外两家的源码与文档里我们也没有找到对应机制,这一层没得比。
Pi:一路走到文件系统根
Pi 的规则在 packages/coding-agent/src/core/resource-loader.ts。loadContextFileFromDir() 里的候选数组是 ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"],一个目录里同样是命中即返回,只取一个。loadProjectContextFiles() 先读全局的那份,然后从 cwd 开始 while (true) 往上走,直到 parentDir === currentDir 才停——也就是走到文件系统根,源码里没有项目根标记这一步。文档 packages/coding-agent/docs/usage.md 的说法与之对应:全局那份在 ~/.pi/agent/AGENTS.md,其余来自当前目录和逐级父目录;命令行开关 --no-context-files / -nc 可以整个关掉发现。
这些差异什么时候会咬到你
第一,同一个目录里 AGENTS.md 和 CLAUDE.md 内容不一样的时候。DeepSeek Harness 会把两份都读进上下文(内容不同就不折叠),Codex 默认只读 AGENTS.md,Pi 按候选顺序只读 AGENTS.md,Claude Code 按其文档只读 CLAUDE.md。同一条规则改在哪个文件里,四边看到的组合都不同。软链或 @AGENTS.md 导入之所以比手抄一份稳,原因就在这里:手抄的两份迟早分叉。
第二,仓库外面那一层。项目根上一级或者家目录里放了 AGENTS.md 的话,Pi 会一路读上去,DeepSeek Harness 和 Codex 会在 .git 那层停住。反过来,如果你的工作目录不在 git 仓库里,前两家的向上遍历也就没有边界依据了。
第三,子目录里的说明书。Codex 的搜索目录只到 cwd 为止,源码里的 search_dirs 是项目根到 cwd 的链,不往下探;DeepSeek Harness 要等一次成功的 read / write / edit 工具调用触达那个目录才会注入,README 写明没有文件监视器;Claude Code 按文档是在读到那个目录里的文件时纳入。所以把关键约束只写在深层子目录里,在不同工具下生效的时机不同。
第四,体积。Codex 的兜底额度是 32 KiB,DeepSeek Harness 那三个 preset 与 base bundle 里写的是 65536 字节(即 64 KiB),后者是这套代码里必须由部署方显式填的一个值、不是框架内置的默认;Pi 的源码与 packages/coding-agent/docs/usage.md 里我们没有找到对应的字节上限,不比。Claude Code 文档说的则是完整加载。一份写得很厚的根 AGENTS.md 加上几层子目录的份额,在额度小的那边会先撞上截断——DeepSeek Harness 至少会在渲染文本里留一行 Workspace instruction budget 提示,Codex 那边是一条 warn 日志。我们没有运行过任何一个工具,这里只陈述阈值与处置动作,不推算你的文件会不会超。
把上面这些拼起来,一个只依据各方公开机制推出来的写法是:正文放在 AGENTS.md,同目录用软链或 @AGENTS.md 导入生成 CLAUDE.md(Windows 上按 Claude Code 官方文档用导入),别让两份内容分叉;把体积压在小的那个额度以内;子目录里的规则当成「可能晚一步生效」来写,别把致命红线只放在深层目录。以上是按各自源码与文档语义组合的建议,未经实测,请以各自官方文档与实际行为为准。
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。
文中涉及的 Claude Code 内容依据其官方文档(code.claude.com/docs)整理,该产品闭源,本文不推断其实现;
Codex 依据 github.com/openai/codex 快照 c6058cc、Pi 依据 github.com/earendil-works/pi 快照 027a5847 整理。
本文只对照各方公开写明的机制,不对三者做优劣排名。