门卫之后还能插一脚:PreToolUse / PostToolUse Hooks

系列回顾:主循环 · 代码库工具 · REPL · 项目上下文 · Skills · 权限 + Write · MCP 概念 · MCP 实现 · Context Budget · Bash · compact 2.0 · autocompact

到上一篇为止,react-agent-mini 的工具链已经很长:校验 → 门卫 canUseToolTool.calltool_result
但有一个缺口:项目想「自动拦某类命令 / 审计每次工具结果」时,只能改源码或盯着 REPL 点 y/N
这篇讲 v5-hooks:用一份 .agents/hooks.json,在工具真正执行前后插上命令型生命周期钩子——仍然不改 query()


门卫解决的是「人批不批」,不是「项目规约」

回忆权限篇:非只读工具(Write / Edit / Bash / 默认 MCP)会走 canUseTool

门卫 canUseToolHooks(本篇)
谁决定人(REPL y/N)或 headless 策略项目配置里的命令
粒度这次调用允不允许可按工具名匹配;可拦、可记日志
位置call门卫通过之后,仍在 call 前后

所以:

人说「可以写」≠ 项目说「允许跑 rm -rf」

Hooks 补的是后半句:把仓库级规约挂进工具流水线。


插在哪?

工具单次执行变成:

Zod 校验
  → canUseTool(门卫)
  → PreToolUse(可 deny,跳过 call)
  → Tool.call
  → PostToolUse(只观测 / 记日志,不撤销结果)
  → tool_result 回给模型

对应代码在 runToolUse:门卫通过后 runPreToolUse,工具跑完(或抛错)再 runPostToolUse

事件时机失败默认
PreToolUsecallexit 2 或 stdout JSON deny不执行工具;其它非 0 fail-soft 放行(可设 denyOnFailure
PostToolUsecall只警告,不改已有 tool_result

这很重要:Post 不是「事后反悔」,工具已经跑完了;它适合审计、指标、通知。


配置长什么样?

工作区放 .agents/hooks.json(没有文件 / HOOKS=0 → 整条跳过):

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "command": "node examples/hooks/deny-bash.mjs"
    }
  ],
  "PostToolUse": [
    {
      "matcher": "*",
      "command": "node examples/hooks/log-post.mjs"
    }
  ]
}

字段很克制:

字段含义
matcher工具名精确匹配,或 * 匹配全部
command经 shell 执行的命令
timeoutMs可选,默认 5s;超时当失败(exit 124)
denyOnFailure仅 Pre:非 0 退出也按 deny(默认 false)

也兼容外面再包一层 { "hooks": { ... } },方便以后和更完整的 settings 形态对齐。


命令怎么和 Host 说话?

Host 把 JSON payload 写进 hook 的 stdin

{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "bun test" }
}

Post 还会带上:

{
  "hook_event_name": "PostToolUse",
  "tool_name": "Read",
  "tool_input": { "file_path": "README.md" },
  "tool_result": "…摘要文本…",
  "tool_is_error": false
}

Pre 如何拒绝:

  1. exit 2(最简单)—— stderr/stdout 文本当作拒绝原因
  2. stdout 末行 JSONpermissionDecision / decision / behavior"deny"
  3. 非 0 且配置了 denyOnFailure: true

被拒时:不调用 Tool.call,回给模型一条 is_error: truetool_result,说明被 hook 拦住——模型可以换策略,而不是假装工具成功了。


30 秒试一把

New-Item -ItemType Directory -Force .agents | Out-Null
Copy-Item examples/hooks/hooks.json .agents/hooks.json
bun run dev

然后让模型跑 Bash(或 mock 触发 Bash):

# Pre:exit 2 → 工具不执行
# stderr:examples/hooks: Bash blocked by PreToolUse demo

# 其它工具结束后:
# [hooks-demo] post Read error=false …

关闭:

$env:HOOKS = "0"

可观测:

$env:TRACE = "1"
# [trace] hooks.pre  tool=Bash matcher=Bash exitCode=2
# [trace] hooks.post tool=Read matcher=* exitCode=0

示例脚本就在 examples/hooks/deny-bash.mjs 拦 Bash,log-post.mjs 把结果摘要打到 stderr。


和门卫、Skills、MCP 怎么区分?

机制一句话
canUseTool人(或 headless 策略)批不批这次副作用
PreToolUse项目脚本在执行前再拦一层 / 改口风
PostToolUse执行后记账,不改结果
Skills给模型说明书(按需注入上下文)
MCP外挂工具 / 材料 / 开场模板

Hooks 不替代门卫:顺序是先门卫、再 Pre。人拒绝了,根本不会进 hook;人同意了,项目规约还能否决。


和主循环的关系

L1 CLI / REPL     → 可选加载 hooks;HOOKS=0 跳过
L2 query()        → 不变
L3 runToolUse     → 门卫后插 Pre / Post
L4 services/hooks → load + run(可注入 fake exec 单测)

Hooks 改的是「单次工具怎么进出」,不是 ReAct 怎么转。

测试里可把 hooksConfig / hookExec 注进 ToolUseContext,不必真起 shell——和 callModel / microcompact 同一套可测思路。


安全:为什么文档一直喊

命令型 hook = 任意 shell
配置写在工作区里,等于信任「能改这个仓库的人」。

默认姿态:

  • 没有 .agents/hooks.json → 什么都不跑
  • HOOKS=0 → 强制跳过
  • 只加载当前工作区这份配置(不扫全球用户目录)
  • Pre 默认 fail-soft:hook 自己挂了,不轻易误杀工具(除非你显式 denyOnFailure

生产环境:只提交你审过的 hook 命令;别把不可信脚本挂进 Pre。


刻意没做什么?

没做意味着什么
Stop hook模型说完一轮后再拦 / 要求继续——本版推迟
SessionStart / Agent hooks会话级、子代理级事件未接
PreCompact / PostCompact不挂在摘要管道上
完整 settings schema先独立 .agents/hooks.json,字段最小
用 hook 改 tool 入参 / 改写结果Pre 只能 allow/deny;Post 只观测

这一刀验证的是 harness 可扩展性的最小面:

配置加载 → matcher → 命令 stdin JSON
  → Pre 可拦 → call → Post 可记
  → TRACE 可观测 → HOOKS=0 可关

系列拼图

能力
门卫 + Write人批副作用
Bash终端钥匙
compact 三连上下文预算
本篇项目级工具生命周期扩展点

Harness 又多一根柱子:循环、工具、会话、上下文、技能、权限、外部协议、预算、hooks。


你可以从这里带走什么?

  1. 门卫和 hooks 是两层——人批之后,项目脚本还能再拦。
  2. Pre 可否决,Post 只旁观——别指望 Post「撤销」已经发生的写盘。
  3. 协议要极简——stdin JSON + exit 2 / 末行 JSON deny,就够做拦与审计。
  4. 默认 fail-soft——hook 挂了不轻易误杀;要严再开 denyOnFailure
  5. 命令型 = 信任边界——只信本工作区配置;HOOKS=0 是总闸。
  6. 不改 query()——扩展点挂在 runToolUse,主循环继续干净。

仓库与相关文档

欢迎 Star、Issue 和 PR。


本文基于 react-agent-mini 变更 v5-hooks(PreToolUse / PostToolUse + .agents/hooks.json)撰写。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐