learn-claude-code s04 Hooks:把扩展逻辑挂在 Agent 循环上,而不是写进循环里
发布时间:2026/9/5 21:09:36 作者:尧图编辑部 阅读量:1,286

learn-claude-code s04 Hooks把扩展逻辑挂在 Agent 循环上而不是写进循环里【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code本文基于 learn-claude-code 仓库的 s04 章节文档 s04_hooks/README.ja.mdHooks 篇另有 英文 与 中文 版本编写完整覆盖其核心脉络为什么 Agent 主循环不应该随扩展逻辑膨胀、如何用 4 个生命周期事件UserPromptSubmit / PreToolUse / PostToolUse / Stop 钩子注册表实现非侵入式扩展以及被拦截、被强制续跑等控制流的底层语义。读完后你能够理解该课程中register_hook()/trigger_hooks()的完整实现、s03 权限检查迁移为 Hook 的前后对比并能直接运行 s04_hooks/code.py 亲手验证各 Hook 的触发时机。一、问题每加一个检查就要改一次主循环s03 的 Agent 已经有了权限检查管线。但现实中任何新需求——把每次 bash 调用写日志、写文件后自动 git add、通知 Slack——都要求直接修改agent_loop函数本身。循环会迅速长成这样def agent_loop(messages): while True: # ... LLM call ... for block in response.content: if block.type ! tool_use: continue log_to_file(block) # 加一行 check_permission(block) # 加一行 notify_slack(block) # 再加一行 output execute(block) auto_git_add(block) # 再加一行 # ... 循环已经面目全非核心矛盾在于你想扩展的是 Agent 的行为动的却是循环本身。主循环是 harness 中最稳定的核心它应当只负责调 LLM、分发工具、收集结果这几件事扩展逻辑应该挂在外侧。s04 就是为了解决这个问题给循环加 4 个固定的扩展点Hook扩展通过注册回调实现循环只负责在固定节点触发。二、方案4 个事件覆盖一个完整的 agent cycles04 完整保留了 s03 的循环结构与权限逻辑唯一的变化是把check_permission()从循环体内挪到了 Hook 上。循环不再直接调用任何检查函数而是调用trigger_hooks(PreToolUse, block)由注册表决定实际运行哪些逻辑。4 个事件及其触发时机引自 s04_hooks/README.ja.md 的事件表与 s04_hooks/code.py 的实现一一对应事件触发时机典型用途UserPromptSubmit用户输入后、进入 LLM 前输入校验、上下文注入PreToolUse工具执行前权限检查、日志记录PostToolUse工具执行后副作用如自动 git add、输出检查Stop循环即将结束时后处理、判断是否继续循环扩展侧通过register_hook()添加回调循环侧只调用trigger_hooks()。二者职责完全分离。三、核心机制钩子注册表与触发器钩子注册表就是一个事件名 → 回调列表的映射字典。s04_hooks/code.py 中的完整实现HOOKS { UserPromptSubmit: [], PreToolUse: [], PostToolUse: [], Stop: [], } def register_hook(event: str, callback): HOOKS[event].append(callback) def trigger_hooks(event: str, *args): for callback in HOOKS[event]: result callback(*args) if result is not None: # 返回值 ≠ None → 该 Hook 说停下 return result return Nonetrigger_hooks的遍历语义值得特别注意它按注册顺序依次调用同一事件下的所有回调只要有一个回调返回非None值立即短路返回该值。这个返回值语义在不同事件中的作用并不相同是理解 s04 的关键PreToolUse返回非None当前这次工具调用被中止返回值字符串会作为tool_result内容写回给模型模型会看到被拒绝的原因Stop返回非None必须是一个字符串消息循环不结束该字符串以 user 消息身份注入messages并继续循环——即强制续跑UserPromptSubmit与PostToolUse的返回值在当前实现中不影响控制流trigger_hooks的返回值在这两处被忽略它们只做观测/记录。四、四个 Hook 的实现逐个拆解以下 5 个 Hook 回调与 s04_hooks/code.py 中的代码一致。4.1 UserPromptSubmit进入 LLM 之前的拦截点s04_hooks/code.py中的示例 Hook 用于在每条 prompt 进入模型前记录当前工作目录def context_inject_hook(query: str) - str | None: Inject current working directory info into every prompt. print(f\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m) return None # return None 不修改放行 prompt register_hook(UserPromptSubmit, context_inject_hook)它在主入口循环中、用户输入拿到之后立刻触发s04_hooks/code.pyquery input(s04 ) trigger_hooks(UserPromptSubmit, query) # ← 进入 LLM 之前 history.append({role: user, content: query}) agent_loop(history)从源码结构看该事件为输入改写/上下文注入留了扩展位理论上回调可以返回改写后的 prompt但当前示例中循环未消费其返回值只做观测。4.2 PreToolUse权限检查从循环迁移而来s03 的权限检查逻辑s03_permission/code.py 中硬编码在agent_loop里的check_permission(block)在 s04 中被整体包成permission_hook并在 s04_hooks/code.py 中注册为 PreToolUse 钩子与日志钩子、大输出钩子放在一起# PreToolUse: 权限检查s03 的逻辑从循环移入 Hook def permission_hook(block): if block.name bash: for pattern in DENY_LIST: if pattern in block.input.get(command, ): return Permission denied by deny list if block.name in (read_file, write_file, edit_file): path block.input.get(path, ) if not (WORKDIR / path).resolve().is_relative_to(WORKDIR): choice input( Allow? [y/N] ).strip().lower() if choice not in (y, yes): return Permission denied by user return None # PreToolUse: 日志 def log_hook(block): print(f[HOOK] {block.name}(...)) # PostToolUse: 大输出提醒 def large_output_hook(block, output): if len(str(output)) 100000: print(f[HOOK] ⚠ Large output from {block.name}) register_hook(PreToolUse, permission_hook) register_hook(PreToolUse, log_hook) register_hook(PostToolUse, large_output_hook)注意源码中的细节与文档略有扩展permission_hook除了文档列出的拒绝列表DENY_LISTrm -rf /、sudo、shutdown、reboot、mkfs、dd if还有一组破坏性命令关键字DESTRUCTIVE [rm , /etc/, chmod 777]s04_hooks/code.py命中后会交互式询问用户Allow? [y/N]拒绝即返回Permission denied by user。文件类工具read_file/write_file/edit_file的路径检查使用(WORKDIR / path).resolve().is_relative_to(WORKDIR)阻止越出工作区的读写越界同样需要用户显式批准。被拦截后的消息流也很清晰permission_hook返回的字符串会作为tool_result的content写回log_hook因返回None而被视为放行不会打断权限检查的短路逻辑。4.3 Stop阻止循环退出的最后防线Stop在循环即将结束stop_reason ! tool_use时触发。s04_hooks/code.py 的示例 Hook 统计本次会话的工具调用次数def summary_hook(messages: list) - str | None: Print a summary when the loop is about to stop. tool_count sum(1 for m in messages for b in (m.get(content) if isinstance(m.get(content), list) else []) if isinstance(b, dict) and b.get(type) tool_result) print(f\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m) return None # return None 允许结束返回字符串 强制续跑 register_hook(Stop, summary_hook)对应的agent_loop内触发点在 s04_hooks/code.pyif response.stop_reason ! tool_use: force trigger_hooks(Stop, messages) # ← 退出之前 if force: # Hook 返回了消息 → 注入并继续 messages.append({role: user, content: force}) continue return这正是 Stop 事件最强的能力一个 Hook 可以否决模型我认为做完了的结论把一段提示作为新的 user 消息塞回去让循环强制再跑一轮——用于实现未完成清单不许收工这类自动质检策略。4.4 循环体内只改了一处s04 相对 s03 在agent_loop中真正的改动只有一行——把直接调用换成触发钩子s04_hooks/code.pyfor block in response.content: if block.type ! tool_use: continue # s03: if not check_permission(block): ... # s04: Hook 取代硬编码 blocked trigger_hooks(PreToolUse, block) if blocked: results.append({type: tool_result, tool_use_id: block.id, content: str(blocked)}) continue handler TOOL_HANDLERS.get(block.name) output handler(**block.input) if handler else fUnknown: {block.name} trigger_hooks(PostToolUse, block, output) results.append({type: tool_result, tool_use_id: block.id, content: output})4 个钩子覆盖了 agent cycle 的关键节点输入 → 执行前 → 执行后 → 结束。循环只调用trigger_hooks()具体逻辑全部位于钩子回调之中。五、s03 → s04 的完整变化对照以下是原文档给出的变化表并与两版源码逐条核对过组件之前 (s03)之后 (s04)扩展方式check_permission()硬编码在循环内HOOKS注册表 trigger_hooks()新增函数—register_hook、trigger_hooksHook 回调—context_inject_hook、permission_hook、log_hook、large_output_hook、summary_hook循环直接调用check_permission()调用trigger_hooks(PreToolUse, ...)结束控制无trigger_hooks(Stop, ...)可阻止退出输入拦截无trigger_hooks(UserPromptSubmit, ...)可注入上下文对比源码可以更直观地理解这次搬家s03 的 check_permission 是三闸门管线拒绝列表 → 规则匹配 → 用户确认在循环中以if not check_permission(block)硬编码出现s04 把同等逻辑塞进 permission_hook 并register_hook(PreToolUse, permission_hook)。检查策略从改循环变成了加注册循环代码因此保持零变化。六、运行验证动手试三个提示词运行方式与原文档一致cd learn-claude-code python s04_hooks/code.py适用前提程序通过load_dotenv()读取环境变量s04_hooks/code.py 中MODEL os.environ[MODEL_ID]要求必须配置MODEL_ID可选ANTHROPIC_BASE_URL指向兼容 API 的网关依赖anthropic与python-dotenv见 requirements.txt。输入q/exit或空行退出。按原文档建议依次输入以下 3 条 promptRead the file README.md—— 应直接放行观察执行前的[HOOK]日志log_hook的 PreToolUse 输出Create a file called test.txt—— 创建完成后观察 PostToolUse 是否触发输出小于 100000 字符时large_output_hook静默放行Delete all temporary files in /tmp—— 触发bash rmpermission_hook命中DESTRUCTIVE中的rm 关键字弹出交互式确认拒绝后模型会收到Permission denied by user的 tool_result。观察要点每次工具执行前是否先出现[HOOK]日志权限被拒时拦截到底来自 Hook 回调还是循环里的硬编码答案s04 中全部来自 Hook循环里只剩trigger_hooks调用。仓库里还有一份对应的模拟场景数据 web/src/data/scenarios/s04.json按UserPromptSubmit → PreToolUsepermission_hook, log_hook→ 工具执行 → PostToolUselarge_output_hook→ Stop的时序逐步演示同一轮 agent cycle可用于在网页版课程中逐步回放各 Hook 的触发点。七、后续演进钩子系统在 s15 集成 harness 中继续服役从源码结构看s04 建立的钩子协议并未止步于教学示例。在 s15_integrated_harness/code.py 中HOOKS注册表与register_hook/trigger_hooks被原样继承并注册了user_prompt_hook、permission_hook、log_hook、large_output_hook、stop_hook五类钩子s15_integrated_harness/code.py主循环里trigger_hooks(UserPromptSubmit, query)、trigger_hooks(PreToolUse, block)、trigger_hooks(PostToolUse, block, output)、trigger_hooks(Stop, messages)四个触发点依旧完整存在见 s15_integrated_harness/code.py 与 L3072。可以说s04 定义的这套事件名 回调列表 非 None 即短路的最小协议就是整个 harness 的扩展底座后续的记忆、任务、后台任务等章节都在这个循环骨架上叠加能力而扩展本身仍然只挂在 Hook 与工具注册表上。八、下一步至此 Agent 已经能安全地执行操作。但它会不会停下来想一想先做什么、再做什么面对复杂任务它是直接上手还是先列计划→ s05 TodoWrite给 Agent 一个计划工具。先列清单再执行见 s05_todo_write/。参考路径索引内容路径s04 章节文档日/英/中s04_hooks/README.ja.md · s04_hooks/README.md · s04_hooks/README.zh.mds04 完整实现s04_hooks/code.pyHook 注册表 L123-L133钩子回调 L136-L193主循环 L200-L255s03 权限管线迁移来源s03_permission/code.py机制总览图s04_hooks/images/hooks-overview.svg网页课程模拟场景web/src/data/scenarios/s04.json集成 harness 中的钩子继承s15_integrated_harness/code.py【免费下载链接】learn-claude-codeBash is all you need - A nano claude code–like 「agent harness」, built from 0 to 1项目地址: https://gitcode.com/GitHub_Trending/an/learn-claude-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考