多 AI Coding Agent 单源配置:从 AGENTS.md 到跨端 Hooks
目录(9)
团队同时使用 Claude Code、Cursor、Codex 后,很容易出现一种隐蔽的工程债: 每个工具都有一套规则文件,内容看起来差不多,却在数周后悄悄分叉。
常见症状有三个:
CLAUDE.md、Cursor Rules、AGENTS.md分别维护,规则彼此矛盾;- 所有说明堆进一个超长文件,每次对话都占用大量上下文;
- 配置文件已经提交,但 Agent 实际没有加载,团队却误以为规则生效。
解决办法不是再写一份“更完整”的规则,而是明确三件事:
- 每类内容只有一个事实源;
- 不同 Agent 的配置只做桥接和协议转换;
- 能机械判断的规则交给 Hooks 与 CI,而不是期待模型自觉遵守。
本文行为基线核对日期为 2026-07-31。Coding Agent 更新很快,正式采用前应重新核对官方文档。
单一事实源不是单一文件
SSOT(Single Source of Truth)强调的是内容只维护一份,不要求整个仓库只能有一个配置文件。
一个可维护的目录可以这样组织:
project/
├── AGENTS.md # 全局项目约定
├── CLAUDE.md # Claude Code 桥接:@AGENTS.md
├── .agents/
│ ├── skills/
│ │ └── domain-workflow/SKILL.md # 按需加载的领域流程
│ └── hooks/
│ ├── core/ # 跨端共享策略
│ └── adapters/ # 各产品 JSON 协议适配
├── .claude/settings.json # Claude Code hooks 配置
├── .codex/hooks.json # Codex hooks 配置
└── .cursor/hooks.json # Cursor hooks 配置
这里有三类实质内容:
AGENTS.md:每次工作都需要知道的命令、架构边界与完成定义;.agents/skills/:只在特定任务出现时才需要的操作流程;.agents/hooks/core/:可以实时机械判断的策略。
其余文件只是入口、链接或适配器,不应复制业务规则。
确定性挂载与概率性发现
这两个概念经常被混在一起:
- 路径规则的
globs/paths是路径驱动的确定性挂载; - Skill 的
description帮助模型判断当前任务是否相关,但自动触发仍是概率性的; - 明确点名一个 Skill 比等待自动触发可靠;
- CI 才是违反规则后一定失败的确定性门禁。
所以 SKILL.md 的 description 应描述“什么时候使用”,而不是只解释“它是什么”:
---
name: api-routes
description: 在 src/server/routes/ 下新增或修改接口、校验 schema 或错误响应时使用。
---
Agent Skills 是开放格式,核心结构就是一个包含
SKILL.md 的目录。Claude Code、Cursor、Codex 对技能目录的发现方式并不完全相同,
因此格式可以共享,入口仍可能需要桥接。
四层治理:Hooks、CI、全局规则、Skill
不是每条约束都应该写进 AGENTS.md。可以按下面的判定顺序分层:
例如:
| 约束 | 推荐位置 |
|---|---|
禁止强制推送、禁止 --no-verify | PreToolUse / beforeShellExecution Hook |
| TypeScript 类型错误、格式不合规 | CI |
| 项目不是 SSR、必须走指定数据入口 | AGENTS.md |
| 新增文章时如何维护标签与 frontmatter | 写作 Skill |
Hooks 可以被关闭、未信任或执行失败,因此它是快速护栏,不是安全边界;即使已经有 Hook, 能在 CI 中复核的规则仍应保留 CI 检查。
三端 Hooks 并不完全相同
Codex 和 Claude Code 在重叠事件上高度兼容:都使用 stdin JSON,shell 命令位于
tool_input.command,PreToolUse 可以返回 hookSpecificOutput,exit code 2
也可以阻止调用。
但“高度兼容”不等于“完全相同”。Claude Code 的事件集合更大,Codex 还有自己的扩展字段; Cursor 则使用另一套命名和 payload。
| 项目 | Claude Code | Codex | Cursor |
|---|---|---|---|
| 项目配置 | .claude/settings.json | .codex/hooks.json 或 config.toml | .cursor/hooks.json |
| shell 执行前 | PreToolUse | PreToolUse | beforeShellExecution |
| 命令字段 | tool_input.command | tool_input.command | command |
| 拒绝字段 | permissionDecision: "deny" | 同 Claude Code | permission: "deny" |
| 原因字段 | permissionDecisionReason | 同 Claude Code | agent_message |
| 会话启动 | SessionStart | SessionStart | sessionStart |
Cursor 当前使用 snake_case:agent_message、user_message、additional_context,
不能写成 agentMessage。sessionStart 可以返回环境变量和附加上下文,但它是
fire-and-forget,不能用来阻塞会话启动。
各端细节可分别参考 Claude Code Hooks、 Codex Hooks 和 Cursor Hooks。
共享策略核心
真正需要避免重复的是“什么命令危险”的判断,而不是三端 JSON 的形式。
// .agents/hooks/core/shell-policy.mjs
export function checkShell(command) {
for (const rule of RULES) {
if (rule.test(command)) {
return { allow: false, code: rule.code, reason: rule.reason };
}
}
return { allow: true };
}
策略核心不认识 Claude Code、Codex 或 Cursor。它只接收命令,返回判断结果。
Claude Code 与 Codex 可以共用一个 adapter:
const input = JSON.parse(await new Response(process.stdin).text());
const checked = checkShell(input.tool_input?.command);
if (!checked.allow) {
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: checked.reason,
},
}),
);
}
Cursor 单独处理顶层 command 和 snake_case 输出:
const input = JSON.parse(await new Response(process.stdin).text());
const checked = checkShell(input.command);
if (!checked.allow) {
console.log(
JSON.stringify({
permission: 'deny',
user_message: checked.reason,
agent_message: checked.reason,
}),
);
}
三份配置文件只需要声明事件并指向 adapter。配置可以有多份,策略仍只有一份。
Codex Hooks 的两个特殊点
第一,项目级非托管 command hook 必须经过审查和信任。新增或修改 hook 定义后,
使用 /hooks 查看来源并确认状态,否则配置可能存在却被跳过。
信任绑定当前 hook 定义的哈希。如果 adapter 指向的脚本内容变化、配置命令却没有变化, 不要假设 Codex 一定会重新要求审查。团队仍应通过代码评审与 CI 检查脚本内容。
第二,同一事件下多个匹配的 command hook 会并发启动。一个 hook 无法阻止另一个启动, 因此不要把 hooks 设计成依赖执行顺序的流水线。
Codex 为插件携带的 hooks 提供 PLUGIN_ROOT / PLUGIN_DATA,并额外设置
CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA 兼容已有 Claude 插件。
普通项目 hooks 不应依赖这些插件变量。
用 CI 检查 Hooks 自己
Hooks 也是代码,同样会漂移。至少应自动检查:
- 安全命令不会被误拦;
- 危险命令会命中正确规则;
- Claude Code / Codex adapter 输出
hookSpecificOutput; - Cursor adapter 输出
permission、agent_message; - 三份配置仍然指向共享 adapter。
然后把检查聚合到一个统一入口:
{
"scripts": {
"check:hooks": "bun run scripts/check-hooks.ts",
"verify": "bun run lint && bun run check:types && bun run check:hooks && bun run build"
}
}
这样,Hook 负责动作发生前的反馈,CI 负责保证 Hook 本身没有悄悄失效。
用加载探针观察规则是否到达
可以对 AGENTS.md、SKILL.md 和 hooks 策略核心计算哈希,把短指纹写回
AGENTS.md,并要求 Agent 在新会话首条回复中回显。
它能帮助发现:
- 完全没有回显:入口或桥接没有加载;
- 回显旧指纹:会话缓存、分支或规则副本过期;
- 指纹正确但 Skill 无标记:Skill 没有触发。
但这个指纹只是加载 canary。它只能说明当前指纹文本进入了上下文,不能证明全文被完整理解, 也不能证明 Agent 会遵守。最终保证仍来自权限系统、Hooks、CI 与人工审查。
落地检查清单
-
AGENTS.md只保留全局项目边界、命令和完成定义; - 领域流程拆进
.agents/skills/<name>/SKILL.md; - Claude Code 通过
CLAUDE.md和逐技能链接接入共享内容; - 实时策略只写在
.agents/hooks/core/; - 三端配置只引用 adapters,不复制策略;
- 用三端真实 payload 测试 adapters;
- Hooks 检查进入统一 CI 命令;
- Codex 用户通过
/hooks审查项目信任; - 三端分别新开会话,验证规则、Skill 与 Hook 的实际触发;
- 每隔数周重新核对产品支持矩阵。
多 Agent 配置真正困难的不是“写规则”,而是建立一条可追踪的传递链:
一个事实源
→ 各端桥接
→ 加载探针
→ 实时 Hooks
→ CI 最终门禁
只有这条链路可观察、可测试、可复核,SSOT 才不是一句口号。