首页 › tutorials › 用Hooks造确定性护栏

概述

Claude Code 本质是概率性的——同一提示跑两次结果可能不同。但 Hooks(钩子) 是其中罕见的确定性机制:由 harness 在固定生命周期点强制执行 shell 命令,模型无法绕过。本教程带你用 PreToolUse、PostToolUse、UserPromptSubmit、Stop 四类钩子构建一套"自动拦截危险命令 + 自动格式化 + 自动注入上下文 + 完工通知"的护栏系统。适合已熟悉 Claude Code 日常用法、想把它放进团队/生产流水线的开发者。

前置条件


第一步:理解 Hooks 的执行模型与四类核心钩子

操作说明

Hooks 不是提示词,也不是 Skill,而是由 harness 在工具调用生命周期里插入的 shell 命令。关键在于:它们由 harness 执行,模型自己改不了、也跳不过。这正是它比"在 CLAUDE.md 里写一句『别删数据库』"可靠得多的根本原因。

四类最常用的钩子事件:

钩子事件 触发时机 典型用途 能否阻断流程
PreToolUse 工具执行前 拦截危险命令、保护敏感文件、二次确认 能(deny/block)
PostToolUse 工具执行后 自动格式化、跑 lint、记录日志 否(只做副作用)
UserPromptSubmit 用户提交提示词后、送模型前 注入上下文、拦截敏感提问 能(exit 2)
Stop 模型结束本轮回复 完工通知、自动跑测试、归档 能(让模型继续)

每个钩子执行时,harness 会通过 stdin 喂一段 JSON,包含 session_id、transcript_path、cwd、hook_event_name,以及工具相关的 tool_name / tool_input / tool_response。你的脚本读这段 JSON 做判断,再通过退出码或 stdout 的 JSON 反向控制 harness。

预期结果

输入 /hooks 回车,能看到一个交互式管理界面,列出当前生效的所有钩子及其来源(user / project / local)。这就是你后续所有配置的"真值表"。

常见问题


第二步:PreToolUse——硬拦截危险命令与敏感文件

操作说明

这是整个护栏系统的心脏。在项目根目录创建 .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bash .claude/hooks/guard-bash.sh" }
        ]
      },
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          { "type": "command", "command": "bash .claude/hooks/guard-write.sh" }
        ]
      }
    ]
  }
}

matcher 是正则,对工具名做匹配,多条 matcher 之间是或关系。注意 Write|Edit|MultiEdit 必须用一条正则覆盖三种写入工具,漏掉 MultiEdit 是最常见的配置漏洞。

创建 .claude/hooks/guard-bash.sh:

#!/usr/bin/env bash
# 读取 harness 注入的 JSON
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')

# 1. 拦截递归删除根目录/家目录
if echo "$cmd" | grep -qE 'rm\s+-rf?\s+(/|~|\$HOME|\*)'; then
  echo "⛔ BLOCKED: 检测到递归删除根/家目录命令: $cmd" >&2
  exit 2
fi

# 2. 拦截强制推送主分支
if echo "$cmd" | grep -qE 'git\s+push.*--force.*main|git\s+push.*-f.*master'; then
  echo "⛔ BLOCKED: 禁止强制推送主分支" >&2
  exit 2
fi

# 3. 拦截管道执行远程脚本
if echo "$cmd" | grep -qE 'curl.*\|\s*(sh|bash)|wget.*\|\s*(sh|bash)'; then
  echo "⛔ BLOCKED: 禁止管道执行远程脚本" >&2
  exit 2
fi

# 放行
exit 0

控制协议要记牢两条:退出码 2 = 阻断工具并把 stderr 回喂给模型(模型会"看到"被拦并改道);退出码 0 = 放行。退出码 2 是最常用、最可靠的阻断方式,几乎所有版本都兼容。

预期结果

当模型尝试 rm -rf /,你会看到红色拦截信息,工具根本不会执行,模型随后会自行道歉并换方案。日志里看不到任何 rm 真实执行痕迹。

常见问题


第三步:PostToolUse——每次写入后自动格式化

操作说明

PreToolUse 负责"不让坏事发生",PostToolUse 负责"让好事自动发生"。配置:

"PostToolUse": [
  {
    "matcher": "Write|Edit|MultiEdit",
    "hooks": [
      { "type": "command", "command": "bash .claude/hooks/auto-format.sh" }
    ]
  }
]

.claude/hooks/auto-format.sh:

#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // ""')
[ -z "$file" ] || [ ! -f "$file" ] && exit 0

case "$file" in
  *.js|*.ts|*.jsx|*.tsx|*.json|*.css)
    npx --no-install prettier --write "$file" 2>/dev/null ;;
  *.py)
    python -m black "$file" 2>/dev/null
    python -m isort "$file" 2>/dev/null ;;
  *.go)
    gofmt -w "$file" 2>/dev/null ;;
esac
exit 0

PostToolUse 的 stdin 还多了 tool_response,你可以据此判断工具是否成功。注意钩子始终在项目根目录执行(cwd 固定),所以脚本里用相对路径引用工具时要小心,引用文件本身则用 stdin 给的绝对 file_path。

预期结果

模型每次 Edit 完一个文件,下一次 Read 或 git diff 时,代码已经自动按 Prettier/Black 规范排好版。你不再需要手动 npx prettier --write .,也不用在 CLAUDE.md 里反复叮嘱"写完要格式化"——它每次都执行,零遗漏。

常见问题


第四步:UserPromptSubmit——提交时注入上下文与拦截

操作说明

这是最被低估的钩子。它在用户回车之后、送给模型之前触发,能在不修改用户输入的前提下追加上下文,或拦截不合规提问。

"UserPromptSubmit": [
  {
    "hooks": [
      { "type": "command", "command": "bash .claude/hooks/enrich-prompt.sh" }
    ]
  }
]

.claude/hooks/enrich-prompt.sh:

#!/usr/bin/env bash
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt // ""')

# 拦截:提示词里带真实密钥
if echo "$prompt" | grep -qE 'sk-ant-[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16}'; then
  echo "⛔ 检测到提示词含疑似 API 密钥,已拒绝提交。请先脱敏。" >&2
  exit 2
fi

# 注入:当前 git 分支与未提交文件数,让模型始终知晓上下文
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')

cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "[护栏注入] 当前分支: $branch | 未提交改动: $dirty 个文件。修改前请确认是否需要先 stash。"
  }
}
EOF
exit 0

这里用到了 JSON stdout 控制:返回 hookSpecificOutput.additionalContext 会被 harness 追加到这一轮上下文里,用户看不到但模型看得到。退出码 2 则拦截整次提交,stderr 提示给用户。

预期结果

你问"帮我把这个函数重构一下",模型在动手前会自己先确认分支、先提示有未提交改动——因为它每次都被注入了 git 状态。你不写一行 CLAUDE.md 就获得了"团队级上下文感知"。

常见问题


第五步:Stop——完工通知与自动收尾质检

操作说明

Stop 在模型结束本轮、把控制权交还用户时触发。最常见的两种用法:桌面通知(长任务跑完提醒)和自动跑测试(每次完工自检)。

"Stop": [
  {
    "hooks": [
      { "type": "command", "command": "bash .claude/hooks/on-stop.sh" }
    ]
  }
]

.claude/hooks/on-stop.sh:

#!/usr/bin/env bash
input=$(cat)
# Stop hook 若返回 {"decision":"block","reason":"..."},模型会被迫继续,适合"没跑测试不许停"
# 这里只做通知,不阻断

os=$(uname -s)
if [ "$os" = "Darwin" ]; then
  osascript -e 'display notification "Claude 已完成本轮任务" with title "Claude Code" sound name "Glass"'
else
  # Linux: 通用终端响铃 + 可选 libnotify
  printf '\a'
  command -v notify-send >/dev/null && notify-send "Claude Code" "已完成本轮任务"
fi
exit 0

进阶用法:如果想让"未通过测试就不许停",可在脚本末尾返回:

if ! npm test --silent >/dev/null 2>&1; then
  echo '{"decision":"block","reason":"测试未通过,请修复后再结束。"}'
  # 模型会继续工作直到测试绿
fi

注意 Stop 的 block 是强约束但有风险——若测试环境本身坏掉,模型会陷入无限循环。务必设超时与重试上限。

预期结果

长任务(重构、批量改造)跑完,桌面弹出通知;如果你启用了 block 模式,模型在测试转绿前根本"停不下来",相当于一个免费的 QA 把关人。

常见问题


实战案例:构建一套"零信任"护栏套件

把上面四个钩子组装成一个可分发的项目级护栏套件,目录结构:

.claude/
├── settings.json          # 钩子注册
└── hooks/
    ├── guard-bash.sh      # PreToolUse: 拦危险命令
    ├── guard-write.sh     # PreToolUse: 保护 .env/密钥
    ├── auto-format.sh     # PostToolUse: 自动格式化
    ├── enrich-prompt.sh   # UserPromptSubmit: 注入+拦截
    └── on-stop.sh         # Stop: 通知+质检

guard-write.sh 补上文件保护:

#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // ""')

# 禁止写入/覆盖敏感文件
case "$file" in
  *.env|.env*|*secrets*|*credentials*|id_rsa|*.pem)
    echo "⛔ BLOCKED: 禁止直接修改敏感文件: $file" >&2
    exit 2 ;;
esac

# 禁止写入项目根之外的路径(防越权)
proj_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
case "$file" in
  "$proj_root"/*) exit 0 ;;   # 在项目内,放行
  *) echo "⛔ BLOCKED: 禁止写入项目目录之外: $file" >&2; exit 2 ;;
esac

完整的 settings.json:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "bash .claude/hooks/guard-bash.sh" }] },
      { "matcher": "Write|Edit|MultiEdit",
        "hooks": [{ "type": "command", "command": "bash .claude/hooks/guard-write.sh" }] }
    ],
    "PostToolUse": [
      { "matcher": "Write|Edit|MultiEdit",
        "hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format.sh" }] }
    ],
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command", "command": "bash .claude/hooks/enrich-prompt.sh" }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "bash .claude/hooks/on-stop.sh" }] }
    ]
  }
}

部署后效果:任何 teammate cd 进这个项目,Claude Code 立刻"穿上护甲"——危险命令挡掉、敏感文件锁死、代码自动排版、提示词自带 git 上下文、完工自动通知。整套配置随仓库走,无需逐人培训。

验收清单


工作流程图

flowchart TD
    U[用户输入提示词] --> UPS{UserPromptSubmit<br/>钩子}
    UPS -->|exit 2 拦截| UR[拒绝并提示用户]
    UPS -->|exit 0 + 注入 ctx| M[送入模型]
    M -->|决定调用工具| PTU{PreToolUse<br/>钩子}
    PTU -->|exit 2 阻断| BLK[工具不执行<br/>原因回喂模型]
    PTU -->|exit 0 放行| RUN[工具真实执行]
    RUN --> PTU2{PostToolUse<br/>钩子}
    PTU2 -->|格式化/lint| SIDE[副作用: 改文件/记录]
    SIDE --> M2[模型继续推理]
    M2 -->|本轮结束| STOP{Stop 钩子}
    STOP -->|通知| NTF[桌面/终端通知]
    STOP -->|decision block| M2B[强制继续: 未过测试不许停]
    STOP -->|放行| DONE[交还控制权给用户]

整条链路的关键在于:模型永远在"被护栏包围"的沙箱里工作。PreToolUse 是入口闸门,PostToolUse 是出口美化,UserPromptSubmit 是输入增强,Stop 是出口质检——四者闭环,构成确定性护栏。


关键避坑要点(来自生产环境踩坑)

  1. 退出码语义优先于 JSON:exit 2 跨版本最稳,能用退出码解决的就别上 JSON。需要细粒度控制(如 permissionDecision: ask 走二次确认弹窗)才用 JSON stdout。
  2. matcher 是正则、大小写敏感:Bash 不是 bash,Write|Edit|MultiEdit 漏 MultiEdit 等于漏一半写入。
  3. 钩子静默失败最致命:缺 jq、脚本没 chmod +x、路径错——harness 不会报错,钩子直接"消失"。务必 /hooks 里逐条确认,并给每个脚本加 set -euo pipefail 之外的宽松默认放行(catch-all exit 0),避免护栏自身把正常工作流卡死。
  4. 三层 settings 不要打架:用户级 ~/.claude/settings.json 放通用偏好,项目级放团队护栏,.claude/settings.local.json 放个人实验。项目级会覆盖而非合并同名 matcher,部署前理清优先级。
  5. 拦而不教等于没拦:每次 exit 2 都在 stderr 给出为什么 + 怎么改,模型才能优雅降级,而不是硬刚死循环。

延伸阅读


训练营适用性评估


参考来源

  1. Claude Code Hooks: PreToolUse & PostToolUse Tutorial — Paul Schick — 用 PreToolUse 阻断 rm -rf、PostToolUse 接 Prettier、保护 .env 的确定性护栏实战,本文拦截与格式化脚本的主要范式来源。
  2. Intercept and control agent behavior with hooks — Claude Code 官方文档 — 钩子生命周期事件、matcher 正则语法、stdin JSON 输入与退出码/JSON 控制协议的权威定义。
  3. Claude Code Hooks: 12 Production Configs I Run Daily — Bruce — 12 个生产级钩子配置与 4 个静默失效故障模式,本文"避坑要点"与静默失败排查的主要依据。
  4. Claude Code Hooks Guide: PreToolUse, PostToolUse, Stop — AI Productivity — 各钩子事件的 matcher 配置与 Stop 钩子通知/质检用法的图解指南。
  5. Claude Code Hooks: Complete Guide with 20+ Examples — AIorg — 20+ 可复制粘贴的钩子配置,覆盖 lint、测试、安全扫描等团队场景,可作为护栏套件的扩展素材。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入