hook:规则之外的拦截点,能放宽规则吗?
本课一问:上一课说 Claude Code 的 deny 规则带不了例外。规则之外还有一个拦截点叫 hook——它能不能用来「放宽」规则?
这一课先不碰:hook 的完整事件列表与配置格式(官方 hooks 文档很长,这里只看 PreToolUse 和权限的关系)。
从已有出发
主线第 2 课里,一份规则交给 Claude Code 时先查 deny 组。于是「拒绝所有 git 命令、但允许 git status」写不出来:Bash(git *) 放进 deny,更具体的 Bash(git status) 放进 allow 也救不回来。
贯穿任务里有一个真实需求能说明这件事:我们希望 agent 能跑 python -m unittest …,但不能跑别的 shell 命令。miniagent 靠 last-match 写成「先拒绝 bash、再放行 unittest」(第 2 课);在 Claude Code 里这种写法行不通,只能换个方向:整体放行 Bash,再用 hook 拦下不想要的。这也是官方文档给的做法。
先自己设计:让 hook 返回 allow 来开例外
直觉做法:deny 规则照写,再写一个 PreToolUse hook,遇到 git status 就返回 allow。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
组件加载中……(需要 JavaScript;所有裁决都在你的浏览器里由移植的原算法现算)
把方向倒过来:不写 deny,把 Bash 整个放进 allow;再写一个 PreToolUse hook,遇到不想要的命令就以退出码 2 结束。退出码 2 在权限规则求值之前就中止调用,连 allow 规则、甚至 bypassPermissions 都救不回来。在上面的组件里切到「allow 整个 Bash + hook 以退出码 2 拦 rm」和「bypassPermissions + hook 以退出码 2 拦」两个情景,看它在第 ① 步就定案。
Bash(git push *) 仍然拒绝 git push origin main。hook 能收紧,不能放宽。 试三件事:① 默认情景下点「▶ 一步一步走」:hook 返回 allow,可调用在第 ② 步被 deny 规则拦下;② 把 hook 改成「退出码 2」,看它在第 ① 步就定案;③ 选「没有规则 + hook 返回 allow」:这时 hook 确实跳过了弹窗——它能「省掉问人」,但前提是没有 deny、ask 规则命中。
六步求值序
Agent SDK 文档把 Claude Code 的权限求值写成六步,前一步定案就不再往下走:
这个顺序说明了 hook 的定位 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方文档原文(2026-09-26);深度调研报告 §7、§8 三方核查满票。Claude Code 核心闭源,本页无法用源码验证实际实现。 出处Claude Code 文档 · Extend permissions with hooks ↗需联网 :它是规则之外的一道额外的闸,能让更多调用被拦下,不能让规则已经拦下或要求询问的调用通过。这就保住了第 2 课里 deny 优先的承诺——包括组织在托管设置里写的 deny。
其他几家的 hook:同一个方向
| harness | hook 在哪里 | 能拦吗 | 能放宽规则吗 |
|---|---|---|---|
| CoreCoder v0.7.0 | PreToolUse 在同意层之前跑(agent.py:97) | 能:退出码 2,stderr 作为理由回填给模型(hooks.py:43-45) | 不能:hook 没有 allow 这种输出;PostToolUse 只观察(agent.py:133-136) |
| learn-claude-code s04 | 第 3 章的三道闸整个搬进一个 PreToolUse hook(s04_hooks/code.py:153-154) | 能 | 权限本身就是 hook,不存在「放宽规则」 |
| Pi | beforeToolCall 挂载点(agent-loop.ts:619-647) | 能:返回 block,被拦的调用变成一条错误 tool result | Pi 核心没有规则;它的 34 行 permission-gate 本身就是一个 hook 扩展 |
learn-claude-code 的 README 把这件事写成一句口号:「Hook around the loop, never rewrite the loop」(README.md:215)——在循环外面挂拦截点,不改循环本身。
有一处值得对照第 4 课:CoreCoder 的 hook 出错或卡住时直接跳过(hooks.py:10-11,「hooks assist the loop, they never get to kill it」),也就是 fail open;而 Codex 的审批者出任何故障都不放行。两者都有道理:hook 是用户写的辅助脚本,不该因为它崩了就卡死整个 agent;审批者是安全措施,故障时放行就等于没有它。
常见误解:「PreToolUse hook 返回 allow,就能绕过 deny 规则。」
不能。Claude Code 在 hook 之后照样求值 deny 和 ask 规则;hook 能做的是更早地拦(退出码 2 连 bypassPermissions 都挡得住),以及在没有规则命中时省掉一次弹窗。
- Pi 核心、bypassPermissions、CoreCoder --yes解决了零打断,模型想跑什么马上跑。卡在L1 的 6 条本意危险的构造命令(含
rm -rf /)会全部直接执行。 - 朴素 黑名单:子串或正则lcc s01 / s03、CoreCoder bash 正则、Pi permission-gate解决了把已知危险的写法挡在执行之前,代码只有几行。卡在lcc s03 三道闸(pin 源码实跑):6 条危险命令里漏拦 4 条,5 条日常命令里误拦 2 条(
cat sudoku_solver.py因含子串 sudo 被拒)。 - 第一代 每次都问人CoreCoder 交互模式、Claude Code Manual、lcc s03 的第三道闸解决了判断交给人,规则写不全也不怕。卡在我们 26 次真实运行的 139 次工具调用,在 CoreCoder 交互模式下有 74 次要停下来问(53%);Anthropic 自报用户批准了约 93% 的弹窗——问多了,人就不看了。
- Claude Code(deny → ask → allow)、OpenCode(last-match)、Codex(最严格者胜)解决了把常见的放行和拒绝写成规则,只有规则没覆盖的才问人。卡在同一份规则换一种求值语义可能换一个答案:同一份
rules-standard.json改用 first-match,真实运行 pd-firstmatch-s1 里模型的python -m unittest -v test_dateparse.py被规则 #8 拒掉。 - 第三代 分层放行 + 模型审批Claude Code auto、Codex Auto-review(代码名 Guardian)、Hermes smart解决了只读直放、项目内编辑免审,其余交给另一个模型判断;弹窗接近清零。卡在Anthropic 自报:auto 分类器在真实越权动作上仍有 17% 漏判(n = 52),官方原话「does not guarantee safety」。
- 下一章 与 OS 沙箱分层Claude Code sandbox、Codex Seatbelt / bubblewrap解决了规则管「允许调用什么」,沙箱管「调用之后能碰到什么」。卡在L1 的
python3 -c "import shutil; shutil.rmtree(…)"在四个黑名单里被 4 个全部放行;规则看不见脚本在运行时做什么 → A4。
数字口径:黑名单与「每次都问」由原实现在 pin 源码上实跑(a03-permission-fixtures.json,2026-09-26);真实运行 = 本课程项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit 本机录制;93% 与 17% 为 Anthropic 官方自报。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 规则写不出的精细判断(例外、看命令全文、看参数)交给用户自己的代码 |
| ② 为什么只能收紧? | hook 排在 deny、ask 之前,而它的 allow 不跳过这两步:任何 hook 输出都不会让 deny 规则失效 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:在「Hooks → Deny → Ask → …」的顺序里,deny 组对 hook 的 allow 无条件生效;前提是官方文档描述的顺序与实现一致。 出处待补 |
| ③ 实践上? | Claude Code、CoreCoder、lcc、Pi 的 hook 都只能拦不能放;官方推荐的「allow 整体 + hook 收紧」正是利用这一点 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方文档;其余三家为 pin 源码直证。 出处Claude Code 文档 ↗需联网 |
一句话
hook 只能收紧、不能放宽:它在规则之前拦得住一切,却放不过规则已经拒绝或要求询问的调用。
回到主线:第 3 课:每个调用都问人,人会怎样?
下一个缺口
回到主线:规则再好,每个调用都要人点「允许」吗?hook 能省掉一部分弹窗,但前提是你为每一类调用写好了判断;没写到的,仍然落到「问人」。
你可能会问
- 「hook 返回 ask 有什么用?」 强制弹窗。比如你允许了
Bash,但希望所有git push都经你确认,又不想写 ask 规则时。 - 「hook 自己被注入了怎么办?」 hook 是你写的脚本,它读的是调用的 JSON;如果它把命令文字交给一个模型去判断,就又回到第 4 课的问题:那个模型该看到什么、能做什么。