让 Claude 自己管自己:Hooks 拦截 rm -rf 的那一次救场

发布时间:2026/7/18 8:20:52

让 Claude 自己管自己:Hooks 拦截 rm -rf 的那一次救场 上周同事问我你让 Claude 跑命令不怕它 rm -rf我愣了几秒。不是没想过是真被它坑过。我那会儿在CLAUDE.md里白纸黑字写着禁止执行 rm -rf、禁止动 .git 目录模型读得也挺欢回我好的我会注意。结果某次让它清理/tmp下的测试产物它顺手把rm -rf /tmp/*扩成了rm -rf /*我这坑躺了三天才把项目从 git 历史里捞回来。提示词拦不住这事我搜了 2 小时 issue 才想明白为什么提示词是自然语言约束本质是拜托模型别这么做。模型理解了但它在生成 token 那一刻并不保证遵守。更别说有些危险是它自己组合出来的——单看每条命令都人畜无害串起来就是灾难。确定性的事必须交给确定性机制。这就是 Hooks 要解决的问题。等一下这里我漏说一个前提——Hooks 不是模型能力是 Harness 能力。它发生在工具真正执行之前/之后由 Claude Code 的运行时触发模型根本碰不到这一层。你在提示词里写一万遍别 rm -rf不如一个 PreToolUse Hook 管用。事件会话级和工具调用级两大家族Hooks 挂的事件分两类。会话级事件管生命周期SessionStart会话开始可以注入环境变量、SessionEnd、StopClaude 准备停下来时触发适合做质量门控。工具调用级事件管每一次工具执行PreToolUse在工具执行前拦截能 allow/deny、PostToolUse在执行后回调适合审计、格式化、还有SubagentStart/SubagentStop管子智能体。我这次救场用的就是PreToolUse——在 Bash 命令真正敲进 shell 之前先过一遍我的脚本。配置六个位置六种用途Hooks 配置分散在六个层级企业级策略 → 用户级~/.claude/settings.json→ 项目级.claude/settings.json→ 本地级.claude/settings.local.json→ Skill 的 frontmatter → 命令行参数。越具体优先级越高。我这次把危险命令拦截放在项目级团队共享把本地敏感路径放在 local不上库。三种处理器确定性优先处理器类型有三档按确定性 vs 灵活性权衡command跑个 shell 脚本规则硬编码最快最确定。拦 rm -rf 这种就用它。prompt单次 LLM 评估传一段 prompt 让小模型判断。灵活但有延迟、有不确定性。适合这段代码有没有安全漏洞这种模糊判断。agent起一个子智能体多轮验证最强但最慢。适合跑测试套件、验证复杂输出。我的原则是能用 command 解决的绝不请 LLM。rm -rf 这种字符串匹配就能搞定的没必要请模型请了反而引入不确定性——你拦危险命令总不希望拦截器自己抽风吧。hookSpecificOutput和 Claude 说话的协议Hook 通过 stdout 输出 JSON 跟 Claude Code 通信核心是这个结构{hookSpecificOutput:{hookEventName:PreToolUse,permissionDecision:deny,permissionDecisionReason:拦截危险命令模式: rm -rf /}}permissionDecision取allow或denydeny 时permissionDecisionReason会喂回给 Claude让它知道为什么被拦。还有一个additionalContext字段可以塞额外上下文。关键exit code 2 表示阻断0 表示放行。救场脚本PreToolUse 危险命令拦截这是我那次的完整脚本放在.claude/hooks/block-dangerous.sh#!/bin/bash# .claude/hooks/block-dangerous.shset-eINPUT$(cat)# 提取命令调试信息输出到 stderrCOMMAND$(echo$INPUT|jq-r.tool_input.command // )echoDEBUG: Checking command:$COMMAND2# 危险命令模式列表DANGEROUS_PATTERNS(rm -rf /rm -rf ~rm -rf \$HOME /dev/sdmkfs.:(){:|:};:# Fork bombchmod -R 777 /git push --force origin maingit push --force origin mastergit reset --hard originDROP DATABASEDROP TABLETRUNCATEcurl.*| sh# 危险的管道执行curl.*| bash)forpatternin${DANGEROUS_PATTERNS[]};doif[[$COMMAND*$pattern*]];thenechoBLOCKED:$pattern2catEOF { hookSpecificOutput: { hookEventName: PreToolUse, permissionDecision: deny, permissionDecisionReason: 拦截危险命令模式:$pattern } } EOFexit2fidoneecho{hookSpecificOutput:{hookEventName:PreToolUse,permissionDecision:allow}}exit0几个踩坑点想说一下。第一调试信息一定要输出到 stderr2别输出到 stdout否则会被当成 hook 返回值解析JSON 解析失败整条命令就静默放行了——这个坑我也躺过半天明明拦了却没生效排查半天才发现是 debug 日志污染了 stdout。第二set -e配合exit 2的语义exit 2 是阻断Claude Code 看到这个码就知道要拦。第三fork bomb 那个:(){:|:};:别在终端里手贱试写进模式列表就行。配置里挂上这个脚本{hooks:{PreToolUse:[{matcher:Bash,hooks:[{type:command,command:./.claude/hooks/block-dangerous.sh}]},{matcher:Write|Edit,hooks:[{type:command,command:./.claude/hooks/protect-files.sh}]}],PostToolUse:[{matcher:*,hooks:[{type:command,command:./.claude/hooks/audit-log.sh}]}]}}matcher用Bash只匹配命令执行Write|Edit匹配文件写入*是通配。PostToolUse审计日志事后留痕拦是一回事查是另一回事。PostToolUse 挂个审计日志所有工具调用都记一笔#!/bin/bash# .claude/hooks/audit-log.shINPUT$(cat)LOG_DIR${CLAUDE_PROJECT_DIR:-.}/.claude/logsmkdir-p$LOG_DIRLOG_FILE$LOG_DIR/audit-$(date%Y-%m-%d).logTIMESTAMP$(date-Iseconds)TOOL_NAME$(echo$INPUT|jq-r.tool_name // unknown)TOOL_INPUT$(echo$INPUT|jq-c.tool_input // {})echo[$TIMESTAMP]$TOOL_NAME:$TOOL_INPUT$LOG_FILEecho{}exit0CLAUDE_PROJECT_DIR是 Claude Code 注入的环境变量指向项目根。日志按天切分出事了一 grep 就能定位是哪条命令干的。我习惯把.claude/logs/加进.gitignore不然每天 diff 里全是日志噪音。Stop Hook测试质量门控这个我一开始没加后来吃亏。Claude 说我做完了就停结果测试是红的。Stop Hook 在 Claude 准备结束时触发可以 block 回去让它继续修#!/bin/bash# .claude/hooks/run-tests.shINPUT$(cat)# 防止无限循环检查 stop_hook_activeSTOP_ACTIVE$(echo$INPUT|jq-r.stop_hook_active // false)if[$STOP_ACTIVEtrue];thenexit0# 已经重试过一次这次让 Claude 停下来fiif[-n$CLAUDE_PROJECT_DIR];thencd$CLAUDE_PROJECT_DIRfiTEST_PASSEDtrueTEST_RESULTif[-fpackage.json]grep-qtestpackage.json;thenTEST_RESULT$(npmtest21)||TEST_PASSEDfalseelif[-fpyproject.toml]||[-fpytest.ini];thenTEST_RESULT$(pytest21)||TEST_PASSEDfalseelif[-fgo.mod];thenTEST_RESULT$(gotest./...21)||TEST_PASSEDfalseelseecho{hookSpecificOutput:{additionalContext:未检测到测试框架}}exit0fiif[$TEST_PASSEDtrue];thenecho{hookSpecificOutput:{additionalContext:所有测试通过}}elseTEST_ESCAPED$(echo$TEST_RESULT|head-50|jq-Rs.)catEOF { decision: block, reason: 测试失败请修复后再停止, hookSpecificOutput: { additionalContext:$TEST_ESCAPED} } EOFfiexit0这段有个必须强调的细节——stop_hook_active检查。Stop Hook block 之后 Claude 会继续干干完又触发 Stop又 block死循环。Claude Code 在第二次触发时会带stop_hook_active: true这时必须放行让它停。这个防死循环的逻辑漏了我机器风扇转了二十分钟才发现CPU 满载跑测试套件跑了一轮又一轮。顺便一提雷达鸭鸿蒙版上架那会儿华为审核反馈隐私协议字段缺失我后来给提审流程接了个 PreToolUse Hook 自动校验 manifest 里的权限声明和隐私政策一致性省了来回被打回的工夫。Hook 这东西一旦用顺会忍不住往各种流程上挂。写到这里回头看提示词和 Hooks 不是二选一是分工。提示词管该做什么意图层Hooks 管不能做什么执行层。把确定性的红线交给确定性的机制模型再聪明也越不过去——这才是 Harness 该干的事。那么问题来了当你的 Hook 脚本本身写错了谁来拦 Hook个人介绍雷达鸭 App 独立开发者10 年软件开发软件设计师、人工智能应用工程师专注鸿蒙 ArkTS 与 Web 前端正在探索 AI 自动化工程化。雷达鸭收录中国一人公司赚钱案例。MIT 声明本文代码示例基于《Claude Code 实战Harness 工程之道》黄佳著第 5 章改写遵循 MIT 协议可自由使用、修改、分发。

相关新闻