
给 Agent 的编辑工具接上一个检查脚本,看起来就能组成很自然的流程:先改文件,随后跑 Lint,检查不通过就继续修。可是,“脚本已经执行”和“检查失败会改变下一步”之间,还隔着一条返回链。
假设检查已经报错,工具回执却仍然显示编辑成功,调度器又开始了后面的工作。这时先别急着认定模型没有听话。应当追问:失败信息从哪里产生,经过哪些函数,最后有没有到达决定下一步的组件?
沿当前这份 Agent 的 Hook 实现看下去,会遇到两个很容易混淆的边界。后置检查不能阻止已经发生的动作;即使要阻止后续动作,也得有人接住并消费检查结果。仅把命令挂到工具后面,没有完成第二件事。
后置检查为什么挡不住已经发生的动作?
这里的 Hook 是围绕工具执行触发的一段命令。运行器把工具上下文作为 JSON 送到子进程的标准输入,再收集输出与退出码。前置和后置共用这个运行器,区别在于调用发生在哪里,以及返回值怎样被使用。
在后置路径中,执行器先调用工具的处理函数,取得 ToolResult,然后等待 PostToolUse 检查,最后返回工具原来的结果。若工具已经修改了内容,检查只能面对修改后的状态,无法让那次动作从未发生。
这并不削弱后置检查的价值。语法、格式或产物之间的一致性,往往需要在结果产生后才能判断。真正需要讲清的是:发现问题之后,系统怎样记录它、怎样决定继续修复或暂停后续工作,而不能把检查时点本身当作回滚能力。
为了观察这条边界,验证只注册一个改变内存标记的人工编辑工具。它不碰真实文件,也不运行真实 Lint。检查命令是临时目录里的短 Python 脚本,只接收人工上下文、输出固定诊断,再按指定退出码结束。
对照中,工具名、输入和人工处理函数保持不变,检查配置直接构造,不加载本机已有的命令。每次只改变检查的时点或退出码,才能把状态差异对应到当前调用路径,避免另一个检查器的行为混进结果。
把检查放到后置并使用退出码二时,两份原执行器都留下了同样的状态:内存标记已经改变,工具的处理函数确实运行过。检查没有把标记恢复。这项观察对应的是执行顺序,不能拿来说明一个尚未实现的撤销流程已经可靠。
工具动作是否成功,与动作产物是否满足某项检查,也可以同时有不同答案。编辑确实完成,不代表编辑后的内容合格。两种事实应能分别表达,否则一个笼统的成功状态很容易遮住后面的质量问题。

前置源码拒绝的是当前工具
原 run_hook 对退出码有一项特殊约定:二会生成 blocked=True,并把脚本标准输出作为消息;零以及其他非零值的处理不同。这个返回结构表达了拒绝意图,还需要上层把意图变成动作。
前置路径确实做了这一步。run_pre_tool_hooks 依次运行匹配的检查,遇到阻止结果就返回。执行器收到后,生成错误工具回执并提前结束当前工具调用,处理函数不会继续运行。


