Agent:门卫之后还能插一脚:PreToolUse / PostToolUse Hooks
门卫之后还能插一脚:PreToolUse / PostToolUse Hooks
系列回顾:主循环 · 代码库工具 · REPL · 项目上下文 · Skills · 权限 + Write · MCP 概念 · MCP 实现 · Context Budget · Bash · compact 2.0 · autocompact
到上一篇为止,react-agent-mini 的工具链已经很长:校验 → 门卫
canUseTool→Tool.call→tool_result。
但有一个缺口:项目想「自动拦某类命令 / 审计每次工具结果」时,只能改源码或盯着 REPL 点y/N。
这篇讲v5-hooks:用一份.agents/hooks.json,在工具真正执行前后插上命令型生命周期钩子——仍然不改query()。
门卫解决的是「人批不批」,不是「项目规约」
回忆权限篇:非只读工具(Write / Edit / Bash / 默认 MCP)会走 canUseTool。
门卫 canUseTool | Hooks(本篇) | |
|---|---|---|
| 谁决定 | 人(REPL y/N)或 headless 策略 | 项目配置里的命令 |
| 粒度 | 这次调用允不允许 | 可按工具名匹配;可拦、可记日志 |
| 位置 | call 前 | 门卫通过之后,仍在 call 前后 |
所以:
人说「可以写」≠ 项目说「允许跑 rm -rf」
Hooks 补的是后半句:把仓库级规约挂进工具流水线。
插在哪?
工具单次执行变成:
Zod 校验
→ canUseTool(门卫)
→ PreToolUse(可 deny,跳过 call)
→ Tool.call
→ PostToolUse(只观测 / 记日志,不撤销结果)
→ tool_result 回给模型
对应代码在 runToolUse:门卫通过后 runPreToolUse,工具跑完(或抛错)再 runPostToolUse。
| 事件 | 时机 | 失败默认 |
|---|---|---|
| PreToolUse | call 前 | exit 2 或 stdout JSON deny → 不执行工具;其它非 0 fail-soft 放行(可设 denyOnFailure) |
| PostToolUse | call 后 | 只警告,不改已有 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 如何拒绝:
exit 2(最简单)—— stderr/stdout 文本当作拒绝原因- stdout 末行 JSON:
permissionDecision/decision/behavior为"deny" - 非 0 且配置了
denyOnFailure: true
被拒时:不调用 Tool.call,回给模型一条 is_error: true 的 tool_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。
你可以从这里带走什么?
- 门卫和 hooks 是两层——人批之后,项目脚本还能再拦。
- Pre 可否决,Post 只旁观——别指望 Post「撤销」已经发生的写盘。
- 协议要极简——stdin JSON +
exit 2/ 末行 JSON deny,就够做拦与审计。 - 默认 fail-soft——hook 挂了不轻易误杀;要严再开
denyOnFailure。 - 命令型 = 信任边界——只信本工作区配置;
HOOKS=0是总闸。 - 不改
query()——扩展点挂在runToolUse,主循环继续干净。
仓库与相关文档
- GitHub:https://github.com/jimchou-h/react-agent-mini
- 上一篇:autocompact · 权限 + Write
- 示例:examples/hooks
- 源码:src/services/hooks
- 术语表:CONTEXT.md
欢迎 Star、Issue 和 PR。
本文基于 react-agent-mini 变更 v5-hooks(PreToolUse / PostToolUse + .agents/hooks.json)撰写。
更多推荐


所有评论(0)