42 已经出来了,日志却没写成
延续上一课的要求:先读 main.py,运行前问我,获准后再运行。文件依然只有一行:
print(6 * 7)现在加一条要求:记录每次工具请求和处理结果,维护期间暂停运行。
执行器很容易多出几个步骤:先记请求、检查维护状态、检查权限、运行工具、再记结果。这些步骤有一个共同位置,却不承担同一种责任。尤其是最后那次记录:如果程序已经输出 42,审计回调这时才报错,能不能把整次调用标成“未执行”,然后再运行一遍?
不能。审计失败没有撤销已经发生的进程执行。重跑还可能再次发出邮件、写入订单或更新文件。要扩展执行器,先要分清每条逻辑能在什么时刻改变什么。
把位置固定,把行为挂上去
第三课已经把循环需要的执行依赖收敛成 execute(call)。这一课继续使用这个接口,不往循环里添加维护、日志和通知分支。我们给执行入口留两个位置:
| 位置 | 此时已经知道什么 | 本课允许回调做什么 |
|---|---|---|
before | 工具名、参数、调用 ID;尚未执行 | 返回阻止理由,或返回 None 继续 |
after | 入口已取得一份处理结果 | 观察结果;故障转为宿主警告 |
挂在这些位置上的回调就是 Hook。事件发生时,Harness 调用注册的函数。模型没有额外获得一个“调用 Hook”的工具,也不负责决定检查是否触发。
“位置固定”让调用路径可预测;“行为可注册”让新增检查不必反复改入口的控制流。这里的收益是职责更清楚。只有一条固定逻辑时,直接函数调用也完全合适;当多个工具开始共享检查与记录,才值得把扩展点明确下来。
Harness 收到 run_main({}),调用 ID 是 run_1。执行器先发出 before 事件,把请求副本交给已注册回调。
run_1 · run_main({})- before 回调按顺序检查请求,首个阻止结束前置链。
- 没有阻止仍需通过原权限入口,才可能运行工具。
- after 观察处理结果,故障只收为警告,不重跑工具。
- 结果回到循环;警告留给宿主。
图从同一个 run_1 请求出发。切换“维护阻止”观察旁路,再切换“审计故障”走到程序结果之后:能否阻止,要看回调发生在执行之前还是之后。 图是本册设计的流程推演,实际函数与断点见实践篇。
没有阻止,不代表已经授权
假设两个前置回调按顺序登记:先记请求,再检查维护状态。第一个返回 None,第二个返回“维护期间暂停运行”。入口在第二个回调处停止,不调用剩余前置回调,也不到达权限询问或程序。
反过来,所有前置回调都返回 None,也只说明这组检查没有阻止。第三课的权限规则仍在后面:宿主禁止项依旧生效,用户仍可以拒绝。我们没有给 Hook 增加一个可以覆盖这些决定的 allow 返回值。
这个顺序还决定了日志的含义。维护检查之前的日志只能写“收到运行请求”,不能写“程序已运行”。如果希望被拒绝的请求也留下记录,就把请求记录放在可能阻止的回调之前;放在后面的记录可能根本不会执行。回调顺序因此属于行为约定,需要能从注册处直接读出来。
同一个异常,发生时机不同,处理也不同
先看前置检查。维护接口不可用,既没有证明允许,也没有返回明确的维护结论。本课把这种异常当成阻止,形成带原调用 ID 的“未执行”反馈。这样,检查坏了不会悄悄变成放行。
后置审计面对的是另一件事:入口已经拿到结果。此时回调抛出异常,宿主保存警告并继续后续观察者,同时保留原结果。下面这段独立 Python 代码缩小了这个时刻:result 先存在,观察者随后失败。
result = {"tool_use_id": "run_1", "content": "42", "is_error": False}
warnings = []
def audit(snapshot):
raise OSError("审计不可用")
try:
audit(dict(result))
except OSError as error:
warnings.append(str(error))
print(result["content"])
print("宿主警告:" + warnings[0])点击行号设置断点,再点“调试”;也可以直接运行。
输出是 42 和 宿主警告:审计不可用。在 audit(...) 处设置断点,可以看到结果已经生成;异常处理只增加警告,没有重新执行产生结果的动作。这是内存中的时机实验,没有启动文件或模拟模型调用。
本课将 after 定义为“入口取得处理结果后”,所以权限拒绝、维护阻止和工具错误也能被记录。它与 Claude Code 的 PostToolUse 不是一一对应:官方将工具成功后的 PostToolUse 和失败后的 PostToolUseFailure 分开。先明确本地事件契约,再考虑映射到具体产品,不能仅凭名称相似推断行为。
哪些逻辑不该塞进“尽力记录”
如果业务要求“审计可靠落盘以后才能执行”,一个可能丢失的后置观察者不够。需要在执行前确认审计已被可靠接受,并设计存储失败、进程崩溃和重复执行的处理。这里的内存列表只帮助读者观察调用顺序,进程退出后不会保存。
回调也不是沙箱。它与宿主处在同一个 Python 进程,拥有宿主的操作系统权限;给它数据副本只能避免通过这份数据覆盖请求或结果,不能约束恶意代码访问文件、卡住线程或修改全局对象。同步回调必须短且可信;超时、隔离和异步投递需要另外的运行机制。
停止条件也没有改变。工具前后的 Hook 不会让循环自动续跑,模型调用预算继续生效。若将来需要在退出前检查工作是否完成,就要单独定义退出事件的返回值、可追加的消息和继续次数;不能把本课的后置观察规则直接搬过去。
回到开头:程序输出了 42,日志没记成。现在可以分别报告两件事实——工具已经执行,审计发生故障。扩展点的价值,就在于新增行为以后,原有事实仍然说得清楚。
资料核对于 2026-10-18:课程 Hooks以生命周期扩展点组织行为;Claude Code Hooks 官方文档定义产品事件与输入输出。本册采用两个同步工具事件,错误策略和 Python 实现由本册独立设计。