这篇想弄清楚几件事:Hook 在何时运行?哪些 Hook 真能阻止主流程?四种 Hook 类型怎样工作?想做审计、外部策略校验或上下文保留时该落在哪里?

Hook 能做门禁,做不了万能中间件

Hook 是一组按生命周期事件触发、按优先级串行运行的异步拦截器。它适合做门禁、审计、通知与校验;当前并不是可任意改写 prompt、工具入参或工具结果的通用中间件。

例Example text
用户消息 → user_prompt_submit → LLM
                             │
                             ▼
                       tool_use 请求
                             │
                             ▼
                    pre_tool_use Hook  ← 审计 / 拦截
                             │
                             ▼
              查工具 → 校验参数 → Permissions → 确认
                             │
                             ▼
                         tool.execute()
                             │
                             ▼
                   post_tool_use Hook  ← 记录结果 / 通知
                             │
                             ▼
                      ToolResult 回写 LLM

与 Permissions 的分工很清楚:Permissions 是全局统一的安全裁决闸门;Hook 是在生命周期节点上追加行为的扩展机制。比如「永远禁止读取密钥」优先放 PermissionChecker;「执行 Bash 前交给公司服务额外审查」适合 Hook。

下面的本地演示把这条边界拆开:选一个场景,看 Hook 是否只是在运行,还是它的结果真的被调用方消费,进而改变主流程。

图Fig. 谁运行,谁真的能阻止交互演示 · 本地模拟Interactive · simulated locally
Hook 调度场景Hook scenario
事件Event
pre_tool_use
类型Type
command

Bash 想写入受保护路径。匹配 Hook 先于工具查找和参数校验运行;调用方消费聚合结果后,主执行路径停止。

第 1 步Step 1放行Pass

QueryEngine._execute_tool_call()发出 pre_tool_use 事件

模型提出的 tool_name 与原始 tool_input 先交给 Hook。此时两者尚未经过 registry 或 Pydantic 校验。

event: "pre_tool_use" · tool_name: "bash"
调用方怎么处理What the caller does
阻止:错误 ToolResult 回给 LLMtool.execute() 未到达
第 1 步,共 6 步Step 1 of 6

源码地图

  • hooks/events.py:HookEvent,全部生命周期事件。
  • hooks/schemas.py:四种 Hook 配置模型与公共字段。
  • hooks/types.py:HookResult 与多结果聚合。
  • hooks/loader.py:加载 settings / plugins 的 Hook,并按 priority 排序。
  • hooks/executor.py:,负责匹配、执行与失败处理。
  • engine/query.py:工具调用前后与 stop 事件的真实调用点。
  • services/compact/__init__.py:压缩前后 Hook,以及压缩附件注入。
  • ui/runtime.py:创建 executor、会话启动与关闭事件。

Hook 能挂在哪些时刻

事件名必须是以下 snake_case 值:

例Example python
session_start       # 会话启动
user_prompt_submit  # 用户消息已提交
pre_compact         # 完整上下文压缩前
post_compact        # 压缩完成后
pre_tool_use        # 工具执行前
notification        # 即将显示权限确认提示时
post_tool_use       # 工具成功返回后
stop                # agent 正常结束时
subagent_stop       # 子 Agent 完成时
session_end         # 会话关闭时

哪些事件的 blocked 会真的改变主流程?

  • :调用方消费 。 工具不执行;阻止原因作为错误工具结果回给 LLM。
  • :调用方消费 。 取消后续完整摘要压缩。
  • :不作门禁。 输出会作为 compact hook notes 附进压缩后的上下文。
  • 其余事件:通常不消费 。 它们通常只做观察、通知或副作用;返回的 不会中断该流程。
易混点

Hook 返回 blocked=True 本身不会自动停止世界。必须由对应调用方读取它。当前最重要的两道真门是 和 。

pre_tool_use 到底在哪一步

工具执行路径的实际顺序是:

例Example text
pre_tool_use
  → registry.get(tool_name)
  → input_model.model_validate(tool_input)
  → PermissionChecker.evaluate(...)
  → 可选 notification + 用户确认
  → tool.execute(...)
  → post_tool_use

这带来两个结论:

  1. 能在工具产生副作用前阻止它。
  2. 它比工具查找和 Pydantic 校验都早:Hook 可能收到不存在的工具名或格式错误的 tool_input,所以不能假定输入可信或合法。
例Example execute-tool-call.py · simplifiedpython
pre_result = await hook_executor.execute("pre_tool_use", payload)
if pre_result.blocked:
  return ToolResultBlock(pre_result.reason, is_error=True)

# 只有未被调用方阻止时,才继续下面的路径。
tool = registry.get(tool_name)
parsed = tool.input_model.model_validate(tool_input)
decision = permission_checker.evaluate(...)
result = await tool.execute(parsed, context)
await hook_executor.execute("post_tool_use", result_payload)

四种 Hook,各适合干什么

  • :启动本地命令;适合本地规则脚本、审计;默认 block_on_failure=false。
  • :POST 事件 payload 到服务;适合策略服务、SIEM、远程日志;默认 block_on_failure=false。
  • :额外请求模型作判断;适合自然语言规则校验;默认 。
  • :以更深入提示词请求模型判断;适合较复杂的策略校验;默认 。

Hook 可使用两个运行时提供的信息:

例Example bash
OPENHARNESS_HOOK_EVENT      # 当前事件名
OPENHARNESS_HOOK_PAYLOAD    # JSON 字符串 payload

# 命令模板里的 $ARGUMENTS 会被替换成完整 JSON payload
python scripts/check_policy.py $ARGUMENTS

/ 期待模型返回类似:

例Example json
{"ok": true}
// 或
{"ok": false, "reason": "目标路径不允许写入"}
名字会误导人

Hook 不是启动一个带工具、可自主循环的子 Agent;当前实现只是一次额外的模型验证请求。

谁先跑,失败了怎么办

每个 Hook 都能设置:

例Example json
{
  "matcher": "bash",
  "priority": 100,
  "timeout_seconds": 10,
  "block_on_failure": true
}
  • 越大越先执行;同优先级保留注册顺序。settings Hook 通常先于插件 Hook。
  • 用 fnmatch 匹配。工具事件通常匹配 tool_name,用户消息事件匹配 prompt,其他事件通常匹配事件名。
  • 失败只有在 时才变成阻止结果。
  • 即使前一个 Hook 已阻止,后续匹配 Hook 目前仍会执行;因此 Hook 自身不宜依赖「前面已拦截就绝不会运行」。

最小例子:执行 Bash 前先跑策略脚本

下面是概念上的 settings 配置:执行 Bash 前先运行本地校验,校验脚本非零退出、超时或无法启动时,阻止 Bash 工具。

例Example settings.json · conceptualjson
{
"hooks": {
  "pre_tool_use": [
    {
      "type": "command",
      "command": "python scripts/check_bash_policy.py $ARGUMENTS",
      "matcher": "bash",
      "timeout_seconds": 10,
      "block_on_failure": true,
      "priority": 100
    }
  ]
}
}

它适合把组织策略放在一个可独立测试的脚本里,而非散落在每个 Bash 调用处。

审计要 pre 和 post 一起用

例Example text
pre_tool_use
  → 记录:模型想调用哪个工具、输入是什么
  → 若策略失败:阻止,不执行工具

工具执行成功
  ↓
post_tool_use
  → 记录:工具输出、是否为错误
  → 发 HTTP 到审计 / 监控服务

限制也要说清: 只会在工具走到 execute() 并正常返回后触发。若工具不存在、参数无效、权限拒绝、pre Hook 阻止或 execute() 抛异常,都不会走到这个正常的 post 事件。

此外,大输出会先被引擎 offload;所以 post Hook 对超长结果看到的可能是预览与 artifact 路径,而不一定是完整原文。

后置不等于回滚

适合记录结果、通知 SIEM 或检查质量,但不能撤销已经发生的工具副作用。真正的执行前门禁应放在 或 Permissions。

post_compact:现成的上下文注入点

绝大多数 Hook 输出不会自动影响模型上下文。但 是重要例外:它的输出会收集为 Compact hook notes,附入压缩后的会话上下文。

适合注入:

  • 压缩后仍必须保留的安全约束;
  • 外部部署环境状态;
  • 当前任务不可丢失的关键事实。
魔改关键点

如果你想让 Hook「改变后续模型知道什么」,优先研究 。如果你想真正重写工具参数或输出,则要显式扩展引擎的 Hook 结果契约,而不是假设现有 Hook 会自动替换数据。

事件管什么时候,类型管怎么做

这是最容易混淆的一组概念。四种 Hook 类型与 等事件不是同一层级:

  • 事件(event):例如 、、;回答「什么时候触发」——时机。
  • 类型(type):例如 、、、;回答「触发后怎么执行」——手段。
例Example text
                    ┌─ command:本地脚本
事件 pre_tool_use ──┼─ http:外部策略 / 审计服务
                    ├─ prompt:模型快速判断
                    └─ agent:模型深入判断

                    ┌─ command:本地记录脚本
事件 post_tool_use ─┼─ http:上报工具结果
                    ├─ prompt:结果质量检查
                    └─ agent:复杂结果分析

配置结构就是两个维度的笛卡尔组合:

例Example text
事件名
  → 一组 Hook 定义
      → 每个定义各自声明 type 和 matcher

原则上四种类型都能挂到任何事件上;语义是否合理取决于目的。

  • pre_tool_use + command:工具执行前跑本地策略脚本,可阻止。
  • pre_tool_use + http:执行前询问企业审批 / 策略服务。
  • post_tool_use + http:成功后上报审计,通常不阻止。
  • post_compact + prompt:让模型判断压缩后仍该保留哪些规则。
  • session_end + command:清理临时资源或提交会话统计。

一句话: 决定「在工具执行前」; 决定「通过本地命令处理」;两者组合才得到「工具执行前运行本地命令检查」的完整行为。

HookRegistry 怎么建、怎么用、怎么刷新

本身有三条运行时路径:

  1. 构建:ui/runtime.py 的 build_runtime() 调 load_hook_registry(settings, plugins) 建表,传给 。
  2. 执行:HookExecutor.execute() 通过 registry.get(event) 取该事件下按优先级排好的列表。
  3. 刷新:每条用户消息前 handle_line() 重新加载注册表并 update_registry();改 settings/插件后下一条消息即生效。

summary() 的链路则是:

例Example text
用户输入 /hooks
  → commands/registry.py 的 _hooks_handler()
  → CommandContext.hooks_summary
  → RuntimeBundle.hook_summary()
  → load_hook_registry(...).summary()

即 summary() 只负责把当前生效的 Hook 配置格式化成可读文本给 /hooks 展示,不参与实际执行。内部 API client 路径另有 HookReloader 按配置文件 mtime 决定是否重建注册表。

精读清单

主读:src/openharness/hooks/executor.py —— 执行、匹配、失败与四种 Hook 类型。

接着看:

  • src/openharness/hooks/events.py、schemas.py、types.py —— 事件、配置与结果契约;
  • src/openharness/hooks/loader.py —— 注册来源与 priority 排序;
  • src/openharness/engine/query.py —— / 的真实位置;
  • src/openharness/services/compact/__init__.py —— compact Hook 的门禁和 notes 注入;
  • tests/test_hooks/ 与 tests/test_engine/test_query_engine.py —— 测试对照。