跳转至

作业三:Hooks 与 Skill —— Agent 工程中的"流程层"与"知识层"

预计用时:4 小时(自我探索式,含动手实验) 前置要求:会用终端和 git;本机安装了 Claude Code(claude 命令可用);能阅读 JavaScript/JSON。 最终交付:一个 zip 或 git 仓库(结构见 第五部分),外加一份 REPORT.md

作业提交

  • 截止日期:待教师发布时通知

  • 提交: 邮件: mjay.lanlan2943914182[at]gmail.com

  • 格式要求:zip 文件或 git 仓库链接

  • 标题注明:"你的昵称-Agent实战-作业三"

为什么是 Hooks?

一个 Agent 系统里有两类完全不同的"控制手段":

Skill(提示词层) Hook(进程层)
本质 一段被注入上下文的 Markdown 指令 一个在生命周期事件上被 harness 执行的外部程序
谁来执行 模型"读到后照做"(概率性) harness 直接运行(确定性)
能做什么 定义工作流、给出方法论、约定输出格式 注入上下文、拦截工具调用阻止 Agent 停止、读写外部状态
模型能不能"不听" 能(模型可能忽略或偏离) 不能(hook 在模型之外运行)

单独看,Skill 是"软约束",Hook 是"硬约束"。真正有意思的是两者配合:用 Hook 把 Skill 在正确的时机塞进上下文,再用 Hook 在出口处校验 Skill 承诺的事情是否真的发生了。本次作业要研究的两个项目—— oh-my-claudecode(下称 OMC,基于 Claude Code)和 oh-my-codex(下称 OMX,基于 Codex CLI)——就是把这套"软硬配合"做到极致的多智能体编排层。


〇、环境准备(15 分钟)

mkdir -p ~/agent-homework && cd ~/agent-homework
git clone --depth 1 https://github.com/Yeachan-Heo/oh-my-claudecode
git clone --depth 1 https://github.com/Yeachan-Heo/oh-my-codex

通读一遍官方文档中关于 hooks 的两页(各 10 分钟以内,先建立词汇表,不求全懂):

自查:你应该能回答——hook 注册在哪个文件里?hook 脚本通过什么拿到输入(stdin 的 JSON)?通过什么影响 harness(stdout 的 JSON / 退出码)?


一、最小 Hook 实验(45 分钟)

目标:亲手让一个 hook "说话"和"拦人",建立肌肉记忆。本部分产物要进最终交付。

新建一个练习项目 ~/agent-homework/playground,在其中完成两个实验。Hook 注册写在项目级 .claude/settings.json"hooks" 键下,脚本放 .claude/hooks/

实验 1.1:会注入上下文的 Hook(UserPromptSubmit)

写一个 UserPromptSubmit hook,功能:检测用户输入里是否包含暗号 tellme,如果包含,就向模型注入一段它本来不可能知道的话(比如 "今天的幸运数字是 42")。

关键 API:hook 从 stdin 收到 JSON(含 prompt 字段),向 stdout 输出:

{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "(你要注入的文本)"
  }
}

验证:启动 claude,输入"tellme 今天的幸运数字是多少?"——模型应当答出 42。再问一次不带暗号的,确认它答不出。

实验 1.2:会拦截工具的 Hook(PreToolUse)

写一个 PreToolUse hook(matcher 设为 Bash),功能:凡是命令里出现 rm -rf 就拒绝执行,并告诉模型原因。两种实现任选:

  • stdout 输出 {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "..."}}
  • 或者:向 stderr 打印原因,并以退出码 2 退出(退出码 2 = 阻断,stderr 会回灌给模型)

验证:让 Claude "帮我执行 rm -rf /tmp/test-dir",观察它被拦下后的反应——注意它会读到你的拒绝理由并调整策略,这就是 hook 与模型的"对话"。

✍️ 记入 REPORT.md(第 1 节)

  1. 两个实验的脚本如何写的、验证时模型的实际反应(各 2~3 句 + 截图或对话摘录)。
  2. 思考题:实验 1.1 中,如果同样的"幸运数字"写在 CLAUDE.md 里也能让模型答出来,那 hook 注入和静态记忆文件的本质区别是什么?(提示:动态、条件触发、可以读外部系统)

二、源码导读:OMC 如何用 Hook + Skill 编排工作流(60 分钟)

目标:在真实项目里走通一条完整的编排链路。不要全文阅读这些上千行的文件——按下面的问题清单做定向搜索(grep 是你的朋友)。

2.1 总览:hook 注册表(10 分钟)

打开 oh-my-claudecode/hooks/hooks.json。这是 OMC 全部 hook 的注册表。

  • Q1:OMC 一共监听了哪些生命周期事件?把事件名抄下来,按你的理解分成三类:① 注入信息类 ② 拦截/校验类 ③ 状态维护类。
  • Q2UserPromptSubmit 上挂了两个脚本(keyword-detector 和 skill-injector),Stop 上挂了三个。猜测为什么这两个事件是编排层的"兵家必争之地"?

2.2 入口:关键词如何变成 Skill 调用(15 分钟)

scripts/keyword-detector.mjs(1400+ 行,只看这两处):

  • 文件头部注释:列出了所有"魔法关键词"(ralph、autopilot、ultrawork……)。
  • 搜索 MAGIC KEYWORD,找到 createSkillInvocation 函数(约 1006 行附近):看它生成的注入文本长什么样。

  • Q3:用户输入 "ralph 帮我修完所有测试" 之后、模型开始干活之前,发生了什么?用 3~4 步描述(提示:hook 检测 → 写状态文件 → 注入 [MAGIC KEYWORD: RALPH] + skill 调用指引 → 模型读到注入后主动调用 /oh-my-claudecode:ralph)。

  • Q4:注入文本里为什么要带一句 "Read fallback: open …/skills/ralph/SKILL.md"?这防御的是什么失败模式?

2.3 知识层:Skill 定义了什么(10 分钟)

skills/ralph/SKILL.md 的前 60 行(frontmatter + Purpose + Use_When + PRD_Mode)。

  • Q5:ralph skill 承诺的循环是什么?(PRD 里每个 user story 都 passes: true 且通过 reviewer 验证才算完)。注意:这一整页全是写给模型看的自然语言——它本身没有任何强制力。那么是谁来保证模型"没干完不许停"?带着问题进入下一节。

2.4 强制层:Stop hook 如何让"巨石不停滚"(15 分钟)

scripts/persistent-mode.mjs。搜索 decision: "block"(出现十余处)。

  • Q6:Stop hook 输出 {"decision": "block", "reason": "..."} 时 harness 会怎样?reason 字段的内容去了哪里?(它会作为新指令回灌给模型——这就是"自我续命循环"的实现原理。)
  • Q7:这个 hook 是个无状态的一次性进程,它怎么知道"ralph 模式还在进行中、第几轮了"?去 .omc/state/ 相关代码里找答案,并总结:状态文件是 hook 之间、轮次之间的通信总线
  • Q8(安全阀):搜索 iteration / MAX 相关逻辑——无限循环靠什么兜底?如果你设计这个系统,不设上限会发生什么?

2.5 多智能体:hook 如何"监工"子 Agent(10 分钟)

OMC 编排多 Agent 时,主模型通过 Task/Agent 工具派生子 Agent,而 hook 在 SubagentStart/SubagentStop 两个事件上旁观全程。读 scripts/verify-deliverables.mjs 的头部注释(前 25 行就够)。

  • Q9:这个 hook 解决什么问题?(子 Agent 声称"完成"但实际一个文件都没写。)它是阻断式还是建议式(advisory)?从注释里的输出格式找证据。
  • Q10:对照你在 2.2~2.4 看到的内容,总结 OMC 中多智能体编排的分工:Skill 负责告诉主 Agent "怎么拆任务、派什么角色"(如 team/ultrawork skill),Hook 负责在每个子 Agent 的起止点做记录与验收。两者谁也替代不了谁——为什么?

✍️ 记入 REPORT.md(第 2 节)

Q1~Q10 的简答,外加一张时序图(手画拍照或 mermaid 均可):从用户输入 "ralph ..." 开始,画出 keyword-detector → 状态文件 → SKILL.md → 模型工作 → Stop hook block → 继续工作 → 完成放行 的完整循环,标出哪些环节是确定性的(hook)、哪些是概率性的(模型)。


三、对照阅读:当 Harness 没有原生 Hooks(30 分钟)

目标:理解 hook 是一种可移植的抽象——OMX 把同一套事件词汇表搬到了原生支持远不完整的 Codex CLI 上。

只读两份文档,不需要读 OMX 源码:

  1. oh-my-codex/docs/codex-native-hooks.md —— 重点看"Mapping matrix"那张大表。
  2. oh-my-codex/docs/hooks-extension.md —— 重点看"Native event pipeline (v1)"的事件词汇表。

✍️ 记入 REPORT.md(第 3 节)

填写下表(从映射矩阵中各找至少一例)并回答 Q11:

实现方式 事件举例 说明
原生 Codex hook 直接支持(native)
部分原生 + 运行时补全(native-partial)
纯运行时模拟(runtime-fallback,如 tmux/notify 监听)
暂不支持(not-supported-yet)
  • Q11:OMC 的 ralph 靠 Stop hook 的 decision: "block" 续命;OMX 文档里说 Codex 原生 Stop hook 用的是同样的续命契约,但 SubagentStop 在 Codex 上"not-supported-yet"。如果让你在 Codex 上实现 OMC 那个"子 Agent 交付物验收"功能,你会用文档里提到的哪种 fallback 机制来近似?代价是什么(时机精度?可靠性?)?

四、中场小结(10 分钟,纯思考)

把前三部分压缩成一句话写进 REPORT.md(第 4 节),格式不限,但必须覆盖三个词:触发(trigger)、知识(knowledge)、强制(enforcement)

参考靶心(写完再对照):"Hook 在事件点触发并注入/拦截(强制层),Skill 提供工作流知识(知识层),状态文件让无状态的 hook 拥有跨轮次记忆;搭建时你写的是注册表 + 脚本 + SKILL.md 三件套,运行时它们构成 检测→注入→执行→校验→续命 的闭环。"


五、综合建造任务:fix-until-green(100 分钟)

目标:从零搭一个微缩版 OMC——一个靠 hook 驱动的"修到测试全绿才许停"编排系统,并让它真实跑通一次。这是批改的主要依据。

5.1 任务背景

~/agent-homework/capstone/ 新建项目,先人为制造一个"坏仓库":

mkdir -p ~/agent-homework/capstone && cd ~/agent-homework/capstone
git init
npm init -y && npm install --save-dev vitest

写一个 src/stats.js,导出三个函数 meanmedianmode故意让其中至少两个有 bug(例如 median 不排序、mode 永远返回第一个元素);再写 test/stats.test.js 覆盖这三个函数(≥6 个用例),确认 npx vitest run 当前是红的。把这个"出厂即坏"的状态打成第一个 commit——批改者要能 checkout 回来复现。

5.2 需要建造的四个组件

组件 A:Skill —— .claude/skills/fix-until-green/SKILL.md

定义工作流知识,至少包含:

  • frontmatter(namedescription);
  • 工作循环的描述:跑测试 → 读失败 → 派一个子 Agent(用 Task/Agent 工具)去修一个失败点 → 重跑测试 → 重复;
  • 明确的完成判据:npx vitest run 退出码为 0;
  • 一条纪律:不许修改测试文件来让测试通过(这条留给组件 C 强制执行——skill 里写的是君子协定,hook 里写的才是法律)。

组件 B:触发 Hook —— UserPromptSubmit

仿照 OMC 的 keyword-detector:检测用户输入含关键词 fixgo 时——

  1. 写状态文件 .state/run.json,内容至少含 {"mode": "fix-until-green", "iteration": 0, "max_iterations": 5}
  2. 输出 additionalContext,注入类似 [MAGIC KEYWORD: FIX-UNTIL-GREEN] 的引导文本,指引模型去读你的 SKILL.md 并立即开始。

组件 C:守卫 Hook —— PreToolUse

强制执行组件 A 里的纪律:拦截任何写入/编辑 test/ 目录下文件的工具调用(matcher 覆盖 Edit|Write,从 stdin JSON 的 tool_input.file_path 判断),拒绝并说明理由。仅当 .state/run.json 存在且 mode 是 fix-until-green 时生效(否则放行——hook 要懂得只在自己的模式内执法)。

组件 D:续命 Hook —— Stop

这是本作业的灵魂。模型每次想停时:

  1. .state/run.json;不存在或 mode 不对 → 直接放行(输出 {} 或退出码 0);
  2. 在 hook 里真实执行 npx vitest run(确定性校验,不信模型的口头汇报);
  3. 测试全绿 → 放行,并把状态文件标记为 completed
  4. 测试仍红且 iteration < max_iterationsiteration + 1 写回状态文件,输出 {"decision": "block", "reason": "测试仍有失败:<贴入失败摘要>。这是第 N/5 轮,请继续修复。"}
  5. iteration >= max_iterations放行但注入告警(安全阀!参考 Q8 的教训)。

提示:Stop hook 里跑测试可能要十几秒,必要时在 settings.json 里给这个 hook 调大 timeout(单位是,command 类 hook 默认 600 秒,通常够用)。

5.3 实际运行

启动 claude,输入一句话:"fixgo 把这个项目修好"。然后尽量不要再人工干预,观察你的系统自己转起来。请用交互式会话(headless 的 claude -p 模式下 hooks 同样会触发,但不便于观察循环过程和留痕)。

把整个过程留痕:会话结束后导出对话(复制粘贴到 RUN_LOG.md,或截图序列),要求能看到 ① 注入的 MAGIC KEYWORD 文本生效 ② 至少一次 Stop 被 block 后模型继续干活 ③ 最终测试全绿、正常停止。如果模型试图改测试文件被组件 C 拦下,那是加分镜头,务必收录。

5.4 交付清单(批改以此为准)

提交一个 git 仓库(或 zip),结构如下:

capstone/
├── .claude/
│   ├── settings.json              # 三个 hook 的注册(含 matcher 和 timeout)
│   ├── hooks/
│   │   ├── trigger.{sh,mjs}       # 组件 B
│   │   ├── guard.{sh,mjs}         # 组件 C
│   │   └── stop-gate.{sh,mjs}     # 组件 D
│   └── skills/fix-until-green/SKILL.md   # 组件 A
├── src/stats.js                   # 修复后的实现(git 历史里能 checkout 到坏版本)
├── test/stats.test.js             # 与首个 commit 完全一致(diff 必须为空!)
├── .state/run.json                # 运行后的最终状态(completed,含最终 iteration 数)
├── RUN_LOG.md                     # 5.3 的运行留痕
└── REPORT.md                      # 第 1~4 节笔记 + 第 5 节反思(见下)
playground/                        # 第一部分的两个实验(可放同一仓库)

REPORT.md 第 5 节(反思,每题 3~5 句):

  • R1:你的系统里,哪些行为是确定性保证的,哪些仍依赖模型自觉?如果换一个能力更弱的模型,哪个组件最先暴露问题?
  • R2:对照 OMC:你的 .state/run.json 对应它的什么机制?你的组件 D 对应哪个脚本?OMC 比你的玩具版多做了哪三件你认为最重要的事?
  • R3:如果要把组件 D 的"验收"从"测试通过"推广到"子 Agent 都交付了承诺的文件"(即 OMC 的 verify-deliverables),你需要监听哪个事件、读什么输入?在 Codex CLI 上做同样的事呢(呼应 Q11)?

评分标准(共 100 分)

项目 分值 评分要点
Part 1 实验留痕 10 两个 hook 均验证成功,有真实对话证据
Part 2 源码导读 Q1~Q10 + 时序图 20 答案基于源码证据而非泛泛而谈;时序图正确区分确定性/概率性环节
Part 3 映射表 + Q11 10 四类各有真实例子;Q11 提到具体 fallback 机制及其代价
组件 A:SKILL.md 8 frontmatter 合法;含子 Agent 派发指引与完成判据
组件 B:触发 hook 10 关键词检测 + 状态文件 + additionalContext 三者齐备
组件 C:守卫 hook 10 正确拦截测试文件写入;非本模式时放行(常见丢分点)
组件 D:续命 hook 17 hook 内真实跑测试;block/放行逻辑正确;有迭代上限安全阀
端到端运行证据 10 RUN_LOG 可见完整闭环;test/ 与首 commit 零 diff;首 commit 可复现红色测试
反思 R1~R3 5 能准确对应回 OMC/OMX 的机制

批改者快速校验脚本思路(学员也可自测):git stash && git checkout <首commit> -- test/ src/ && npx vitest run 应为红;git checkout HEAD -- . && npx vitest run 应为绿;diff <(git show <首commit>:test/stats.test.js) test/stats.test.js 应为空;.state/run.jsoniteration ≥ 1 且状态为 completed。


常见陷阱(先读,少走弯路)

  1. hook 改了不生效:新版 Claude Code 会自动监测 settings 文件变更并热加载 hooks;用 /hooks(只读面板)确认你的 hook 已注册、来源正确。若仍不生效,重启 claude 会话兜底,并检查 settings.json 是否为合法 JSON。
  2. stdout 里混入调试输出:hook 的 stdout 必须是合法 JSON(或为空),console.log 调试请改用 stderr 或写日志文件。
  3. Stop hook 死循环decision: "block" 后模型继续干、再次触发 Stop——没有安全阀就是无限烧 token。OMC 用迭代计数兜底,你也必须有。
  4. 路径问题:hook 的工作目录是项目根目录,但稳妥起见用 stdin JSON 里的 cwd 字段或绝对路径。
  5. 把工作流写进 hook、把强制逻辑写进 skill——方向反了。记住分工:skill 说"应当",hook 说"必须"