本文首发地址 https://h89.cn/archives/627.html

Monorepo(单一代码仓库,多个项目共用一个 Git 仓库)里写了分层的 AGENTS.md,每个子目录一套规范,Agent 应该按需加载——想得很美。但你有没有验证过,Agent 真的读到了你放的那些文件?

我之前以为得同时维护 CLAUDE.md 和 AGENTS.md,后来发现只放一个就行。

封面

启动时发生了什么

OpenCode 启动时做了一件事:从当前目录向上遍历,按 AGENTS.mdCLAUDE.mdCONTEXT.md 的顺序查找指令文件。

// [instruction.ts](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/instruction.ts) - systemPaths()
for (const file of instructionFiles) {
  const matches = yield* fs.findUp(file, ctx.directory, ctx.worktree)
  if (matches.length > 0) {
    matches.forEach((item) => paths.add(path.resolve(item)))
    break  // 第一个匹配就停
  }
}

instructionFiles 的顺序是 ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"]。所以如果你项目根目录同时有 AGENTS.md 和 CLAUDE.md,只读 AGENTS.md

这里要插一句背景:CLAUDE.md 是 Claude Code 的默认规则文件,相当于 OpenCode 的 AGENTS.md。OpenCode 为了兼容 Claude Code 用户,把 CLAUDE.md 作为 fallback 加载——没有 AGENTS.md 时就读 CLAUDE.md。

启动阶段只加载根目录这一层。子目录的 AGENTS.md?启动时不管。

全局配置也会加载

除了项目级文件,OpenCode 启动时还会加载全局配置目录的指令文件:

// instruction.ts - systemPaths()
const globalFiles = [
  path.join(global.config, "AGENTS.md"),        // ~/.config/opencode/AGENTS.md
  ...(!flags.disableClaudeCodePrompt 
    ? [path.join(global.home, ".claude", "CLAUDE.md")]  // ~/.claude/CLAUDE.md
    : []),
]

遍历顺序:先找 ~/.config/opencode/AGENTS.md,存在就读它;不存在则 fallback 到 ~/.claude/CLAUDE.mddisableClaudeCodePrompt 默认 false,所以 ~/.claude/CLAUDE.md 默认会被读取——这是 OpenCode 的 Claude Code 兼容层,给迁移用户用的。

注意:~/.config/opencode/CLAUDE.md 不在加载列表里。全局只认两个位置,AGENTS.md 优先。

所以启动时完整的加载链路是:

  1. 全局:~/.config/opencode/AGENTS.md~/.claude/CLAUDE.md(第一个存在就停)
  2. 项目:从 CWD 向上 findUp,按 AGENTS.mdCLAUDE.mdCONTEXT.md 顺序,第一个匹配就停
  3. opencode.json 里的 instructions 字段指定的额外文件

三层叠加,全部注入 system prompt。

真正有意思的是运行时

当你用 OpenCode 读取一个文件时,比如 packages/backend/routes/auth.tsresolve() 函数干了这件事:

// [instruction.ts](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/instruction.ts) - resolve()
const target = path.resolve(filepath)
let current = path.dirname(target)

// 从文件所在目录向上遍历,直到项目根目录
while (current.startsWith(root) && current !== root) {
  const found = yield* find(current)  // 在当前目录找 AGENTS.md / CLAUDE.md
  if (!found || found === target || sys.has(found) || already.has(found)) {
    current = path.dirname(current)
    continue
  }
  // 找到了就读取内容,附加到 tool output
  results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` })
  current = path.dirname(current)
}

翻译成人话:

  1. packages/backend/routes/ 开始找——先看有没有 AGENTS.md,没有就看 CLAUDE.md
  2. 往上走到 packages/backend/,同样的逻辑
  3. 继续往上,直到项目根目录(根目录的已经在 systemPaths 里了,跳过)
  4. 每层目录只加载第一个匹配的文件(AGENTS.md 优先于 CLAUDE.md),同层不会两个都读
  5. 跨目录层级则是叠加——routes/AGENTS.mdbackend/AGENTS.md 都会被读到
  6. 同一条消息内不会重复加载同一个文件

find() 函数内部遍历的是 instructionFiles 列表,返回每层目录的第一个存在的文件。所以同层有 AGENTS.md 和 CLAUDE.md 两个文件时,只读 AGENTS.md——跟启动时的逻辑一样,first match wins。

也就是说,OpenCode 在你读写子目录文件时,会自动加载沿途所有子目录的 AGENTS.md 和 CLAUDE.md。

这个功能什么时候加的

不是一直有的。翻 anomalyco/opencode 的 git log 可以看到完整时间线:

日期 事件
2025-12-28 #6316 提出:请求子目录 AGENTS.md 自动发现(Context Auto-Discovery)
2026-01-10 #7576 提出:请求基于文件上下文自动选择嵌套 AGENTS.md
2026-01-26 PR #10678 合入feat: dynamically resolve AGENTS.md files from subdirectories as agent explores them
2026-01-28 Commit 5585907 修复:并行工具调用不会重复加载 AGENTS.md
2026-02-01 修复 #11581:读取指令文件时防止重复注入
2026-02-04 修复 #11536:优先使用 OPENCODE_CONFIG_DIR 查找 AGENTS.md

功能由 Aiden Cline (@rekram1-node) 实现,从 issue 提出到合入不到一个月。两个 issue 都已关闭。后续一周内连续修了三个 bug——并行加载重复、指令文件重复注入、配置目录优先级——说明这个功能上线后踩了不少坑。

如果你用的 OpenCode 版本早于 2026 年 1 月底,子目录 AGENTS.md 是不会被自动加载的。

Claude Code 的做法

Claude Code 的行为几乎一样:启动时加载根目录 CLAUDE.md,运行时按需懒加载子目录 CLAUDE.md(详见 官方文档)。

区别在细节:

维度 OpenCode Claude Code
启动加载 findUp 向上遍历,AGENTS.md 优先 向上遍历,CLAUDE.md
运行时加载 resolve() 从文件目录向上遍历 按需懒加载子目录
文件名优先级 AGENTS.md > CLAUDE.md 只认 CLAUDE.md
加载位置 附加到 tool output metadata 附加到 context
去重 claims Map + loaded metadata 类似机制

核心差异在文件名优先级。OpenCode 同时有 AGENTS.md 和 CLAUDE.md 时只读 AGENTS.md;Claude Code 只认 CLAUDE.md。所以你想两端通用,只放 CLAUDE.md 就够了——OpenCode 会 fallback 读取。

Codex 的做法

Codex(OpenAI)走了第三条路。它只认 AGENTS.md,不认 CLAUDE.md(相关讨论见 openai/codex#9836)。

启动时先向上找 project root(由 project_root_markers 判定,默认含 .git 等标记),再从 root 向下遍历到 CWD,每层目录按 AGENTS.override.mdAGENTS.md → 自定义 fallback 文件名的顺序查找。找到就加载,最多每层一个文件。

# ~/.codex/config.toml — 可以配置 fallback 文件名和大小上限
project_doc_fallback_filenames = ["TEAM_GUIDE.md", ".agents.md"]
project_doc_max_bytes = 65536

合并规则:从根往下把沿途各层的文件拼接(concatenate)起来(源码里用 --- project-doc --- 分隔),靠近 CWD 的排在后面——是叠加,不是覆盖。全局配置在 ~/.codex/AGENTS.md

关键区别:Codex 只在启动时做一次遍历,运行时不会动态发现子目录的 AGENTS.md。 你在 services/payments/ 放了 AGENTS.md,但从项目根启动 Codex 时如果没走到那个目录,就读不到。

Hermes Agent 的做法

Hermes 走了第四条路。它支持 AGENTS.md、CLAUDE.md,还多了一个 .hermes.md

启动时只看 CWD,按优先级加载一个

# prompt_builder.py — build_context_files_prompt()
project_context = (
    _load_hermes_md(cwd_path)      # .hermes.md / HERMES.md(向上遍历到 git root)
    or _load_agents_md(cwd_path)   # AGENTS.md / agents.md(CWD only)
    or _load_claude_md(cwd_path)   # CLAUDE.md / claude.md(CWD only)
    or _load_cursorrules(cwd_path) # .cursorrules / .cursor/rules/*.mdc(CWD only)
)

第一个找到就停。.hermes.md 会向上遍历到 git root,但 AGENTS.md 和 CLAUDE.md 只看 CWD。

运行时:SubdirectoryHintTracker(源码注释写明思路源自 Block 的 goose)在 agent 通过工具访问子目录文件时,自动发现沿途的上下文文件:

# subdirectory_hints.py
_HINT_FILENAMES = ["AGENTS.md", "agents.md", "CLAUDE.md", "claude.md", ".cursorrules"]

从文件所在目录向上遍历,每层目录按优先级找第一个匹配,最多走 5 层,限制在工作目录树内。发现的 hint 附加到 tool result,不是 system prompt。

跟 OpenCode 的 resolve() 思路几乎一样——沿途各层子目录都会叠加加载,同层也是 first match wins。真正的区别有两个:

  1. 限制在工作目录树内,不会加载 ~/.codex/AGENTS.md~/.claude/CLAUDE.md,避免跨 agent 的指令串味
  2. 最多向上走 5 层,防止深层路径一路扫到文件系统根目录

四端对比

维度 OpenCode Claude Code Codex Hermes Agent
默认规则文件 AGENTS.md(fallback CLAUDE.md) CLAUDE.md AGENTS.md .hermes.md > AGENTS.md > CLAUDE.md > .cursorrules
全局配置 ~/.config/opencode/AGENTS.md ~/.claude/CLAUDE.md ~/.codex/AGENTS.md ~/.hermes/SOUL.md(独立身份,非规则文件)
启动加载方向 CWD 向上 findUp CWD 向上 findUp project root 向下到 CWD CWD(仅 .hermes.md 向上到 git root)
运行时加载 ✅ 从文件目录向上遍历 ✅ 按需懒加载子目录 ❌ 只启动时一次 ✅ 从文件目录向上遍历(最多 5 层)
每层加载数量 一个(first match wins) 叠加(多文件拼接) 最多一个 一个(first match wins)
加载位置 tool output metadata context system prompt tool result
支持 override 文件 AGENTS.override.md
安全边界 无显式限制 类似 无显式限制 ✅ 限制在工作目录树内

四种工具,四种哲学:OpenCode 每层取一个但跨层叠加,Claude Code 按需加载全部,Hermes 每层只取一个且限制层数,Codex 启动时把 root→CWD 路径上的文件一次性拼好(路径之外不管)。

什么时候该放子目录 AGENTS.md

值得放的场景:

  • Monorepo 里前端/后端/移动端规范差异大
  • 某个模块有特殊约定(比如测试目录的 UserFactory 模式、API 目录的命名规则)
  • 根目录 AGENTS.md 已经超过 200 行,需要拆分

不需要放的场景:

  • 项目小,一个根目录文件覆盖得了
  • 子目录之间规范差异不大
  • 你维护了 CLAUDE.md 但没维护 AGENTS.md——OpenCode 会 fallback,但只有根目录那层会 fallback,子目录的 CLAUDE.md 也会被加载

一个容易踩的坑: 你在 src/ 放了 AGENTS.md,在 src/utils/ 也放了。读 src/utils/file.ts 时,两个都会被加载。如果内容有冲突,后加载的不会覆盖先加载的——它们是叠加关系,不是覆盖。

验证方法

想知道你的 AGENTS.md 到底有没有被读到?最简单的方式:读一个子目录文件,看 OpenCode 返回的 metadata 里有没有 loaded 字段。

或者在 AGENTS.md 里写一条明显指令,比如"每次读这个目录的文件时,在回复开头加 [LOADED]"。如果 Agent 照做了,说明加载成功。

opencode 会有类似 Loaded your-path/CLAUDE.md的输出。

写在最后

Agent 的指令文件不是"写了就生效"的魔法。它有明确的加载时机、遍历规则和去重机制。了解这些,你才能把 AGENTS.md 放对位置、写对内容。

别猜,读源码。


本文链接:OpenCode 会加载子目录的 CLAUDE.md 吗 - https://h89.cn/archives/627.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: Agent, Claude, codex, Hermes, OpenCode, CONTEXT

🎓 呈言英语 智能英语学习平台
📚单词学习 🎧听说练习 📖阅读理解 ✏️拼写练习 🌟 AI智能推荐 · 科学记忆曲线
🚀 立即开始免费学习

添加新评论