作业三: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 分钟以内,先建立词汇表,不求全懂):
- Claude Code Hooks 参考:https://code.claude.com/docs/en/hooks
- Claude Code Skills(含 SKILL.md 格式):https://code.claude.com/docs/en/skills
自查:你应该能回答——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 输出:
验证:启动 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 节)¶
- 两个实验的脚本如何写的、验证时模型的实际反应(各 2~3 句 + 截图或对话摘录)。
- 思考题:实验 1.1 中,如果同样的"幸运数字"写在 CLAUDE.md 里也能让模型答出来,那 hook 注入和静态记忆文件的本质区别是什么?(提示:动态、条件触发、可以读外部系统)
二、源码导读:OMC 如何用 Hook + Skill 编排工作流(60 分钟)¶
目标:在真实项目里走通一条完整的编排链路。不要全文阅读这些上千行的文件——按下面的问题清单做定向搜索(
grep是你的朋友)。
2.1 总览:hook 注册表(10 分钟)¶
打开 oh-my-claudecode/hooks/hooks.json。这是 OMC 全部 hook 的注册表。
- Q1:OMC 一共监听了哪些生命周期事件?把事件名抄下来,按你的理解分成三类:① 注入信息类 ② 拦截/校验类 ③ 状态维护类。
- Q2:
UserPromptSubmit上挂了两个脚本(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 源码:
oh-my-codex/docs/codex-native-hooks.md—— 重点看"Mapping matrix"那张大表。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 靠
Stophook 的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,导出三个函数 mean、median、mode,故意让其中至少两个有 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(
name、description); - 工作循环的描述:跑测试 → 读失败 → 派一个子 Agent(用 Task/Agent 工具)去修一个失败点 → 重跑测试 → 重复;
- 明确的完成判据:
npx vitest run退出码为 0; - 一条纪律:不许修改测试文件来让测试通过(这条留给组件 C 强制执行——skill 里写的是君子协定,hook 里写的才是法律)。
组件 B:触发 Hook —— UserPromptSubmit¶
仿照 OMC 的 keyword-detector:检测用户输入含关键词 fixgo 时——
- 写状态文件
.state/run.json,内容至少含{"mode": "fix-until-green", "iteration": 0, "max_iterations": 5}; - 输出
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¶
这是本作业的灵魂。模型每次想停时:
- 读
.state/run.json;不存在或 mode 不对 → 直接放行(输出{}或退出码 0); - 在 hook 里真实执行
npx vitest run(确定性校验,不信模型的口头汇报); - 测试全绿 → 放行,并把状态文件标记为
completed; - 测试仍红且
iteration < max_iterations→iteration + 1写回状态文件,输出{"decision": "block", "reason": "测试仍有失败:<贴入失败摘要>。这是第 N/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.json 的 iteration ≥ 1 且状态为 completed。
常见陷阱(先读,少走弯路)¶
- hook 改了不生效:新版 Claude Code 会自动监测 settings 文件变更并热加载 hooks;用
/hooks(只读面板)确认你的 hook 已注册、来源正确。若仍不生效,重启claude会话兜底,并检查 settings.json 是否为合法 JSON。 - stdout 里混入调试输出:hook 的 stdout 必须是合法 JSON(或为空),
console.log调试请改用 stderr 或写日志文件。 - Stop hook 死循环:
decision: "block"后模型继续干、再次触发 Stop——没有安全阀就是无限烧 token。OMC 用迭代计数兜底,你也必须有。 - 路径问题:hook 的工作目录是项目根目录,但稳妥起见用 stdin JSON 里的
cwd字段或绝对路径。 - 把工作流写进 hook、把强制逻辑写进 skill——方向反了。记住分工:skill 说"应当",hook 说"必须"。