首页 › tutorials › 双钩护栏:拦截与保存即格式化

概述

本教程带你掌握 Claude Code 钩子系统中最核心的两个工具事件——PreToolUse 与 PostToolUse,构建一条确定性的代码质量护栏:前者在工具执行前拦截危险命令、保护敏感文件、改写入参;后者在工具执行后自动格式化、自动 Lint、自动跑测试。读完即可在一个项目里跑通"保存即格式化 + 危险命令拦截"的完整闭环。适合已用 Claude Code 跑过至少一个项目、想把"靠 LLM 自觉"升级为"铁律执行"的开发者与工程负责人。

前置条件

一、为什么需要钩子:从"建议"到"铁律"

Claude Code 会遵循 CLAUDE.md 里的约定——但只是"大概率遵循"。当规则是"永远不要删 .env""每次写完文件都要跑 Prettier"时,"大概率"就不够了。

钩子(Hooks)是用户定义的事件处理器,在工具调用的特定生命周期点确定性地执行。关键区别在于:

机制 性质 是否每次执行
CLAUDE.md 建议(suggestion) 否,LLM 偶尔跳过
Hooks 强制(enforcement) 是,无例外
Skills 上下文相关专业知识 仅在匹配时激活

一句话判断标准:违反会造成破坏 → 用钩子;违反只是次优 → 用 CLAUDE.md。

二、读懂双钩生命周期

一次工具调用的完整链路如下,PreToolUse 和 PostToolUse 正好夹住"工具执行"这个不可逆动作:

flowchart LR
    A[用户提示] --> B[Claude 生成工具调用]
    B --> C["PreToolUse 钩子<br/>可拦截/改写/放行"]
    C -->|放行| D[工具执行<br/>产生副作用]
    D --> E["PostToolUse 钩子<br/>可格式化/Lint/测试"]
    E --> F[Claude 继续下一步]
    C -->|拦截 exit 2| G[取消调用<br/>错误反馈给 Claude]

关键时序事实:PreToolUse 钩子在任何权限模式检查之前触发。也就是说,即便会话以 bypassPermissions(跳过所有权限)模式运行,permissionDecision: "deny" 依然能拦住调用。这让你能把"谁都无法绕过"的策略焊死在钩子里。

三、退出码契约:钩子与 Claude Code 的"对讲机"

钩子脚本通过 stdin(收 JSON)、stdout/stderr、退出码与 Claude Code 通信。其中退出码是核心控制信号:

退出码 含义 适用场景
0 无异议,动作正常进行 PreToolUse 下不等于批准,正常权限流程仍生效
2 阻断动作;stderr 会被当作反馈喂给 Claude 让 Claude 调整策略后重试
其他非 0 动作照常进行,仅记一条 hook error 提示 非阻断性警告

需要更精细控制时,退出 0 并向 stdout 打印结构化 JSON。例如 PreToolUse 拒绝并给出理由:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "请用 rg 替代 grep 以提升性能"
  }
}

permissionDecision 取值:allow(跳过交互式权限提示)、deny(取消调用并把理由回传 Claude)、ask(照常弹权限对话框)、defer(仅非交互 -p 模式可用,保留调用供 SDK 包装器后续处理)。

四、第一步:第一个 PostToolUse——保存即格式化

这是回报最快的一个钩子:Claude 每写/改一个文件,自动跑 Prettier。

把以下内容写入项目根目录的 .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true",
            "statusMessage": "Formatting with Prettier..."
          }
        ]
      }
    ]
  }
}

操作说明:matcher: "Edit|Write" 让钩子只对文件编辑类工具触发(不对 Bash、Read 触发,避免无谓开销)。jq 从 stdin 的 JSON 中取出刚改的文件路径,管道给 Prettier。2>/dev/null || true 保证对 Prettier 不支持的文件类型(如 .md)不报错。

预期结果:让 Claude 改一个 .ts 文件,文件保存瞬间自动按 Prettier 规范重排;过程中底部状态栏显示自定义文字"Formatting with Prettier..."。

常见问题: - 钩子不触发?在会话里输入 /hooks,确认 PostToolUse 下出现该钩子;matcher 区分大小写,Edit|Write 必须精确。 - 看到 hook error?通常是 jq: command not found——装好 jq 即可。

五、第二步:PreToolUse 拦截危险命令

PreToolUse 是生产安全的头号事件。下面这个钩子拦截 rm -rf、git push --force、DROP TABLE 等。

新建 .claude/hooks/block-destructive.sh:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ -z "$COMMAND" ]; then exit 0; fi

DANGEROUS_PATTERNS=('rm -rf' 'git push --force' 'git push -f' 'DROP TABLE' 'git reset --hard')
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qi "$pattern"; then
    echo "Blocked: command matches dangerous pattern '$pattern'" >&2
    exit 2   # 退出码 2 = 阻断,stderr 反馈给 Claude
  fi
done
exit 0

赋予可执行权限,并在 settings.json 注册:

chmod +x .claude/hooks/block-destructive.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.sh", "timeout": 5 }
        ]
      }
    ]
  }
}

预期结果:让 Claude"清理临时目录"。原本它会执行 rm -rf /tmp/project-cache;钩子激活后,命令被拦截,Claude 收到错误反馈,自动改用更安全的 find /tmp/project-cache -type f -delete 重试——无需人工干预。

常见问题: - 钩子"坏了"、.env 照改不误?90% 是退出码写错:写了 exit 1(非阻断警告)而不是 exit 2(阻断)。这是最高频的踩坑点。 - 用 "$CLAUDE_PROJECT_DIR" 或绝对路径引用脚本,避免 command not found。要彻底绕开 shell 引号问题,可加 "args": [] 切到 exec 形式直接 spawn 脚本。

六、第三步:保护敏感文件

把 .env、lock 文件、CI 配置焊死,任何工具(含 Bash 里的 sed/echo >)都改不动。注意 matcher 用 Edit|Write|Bash 三管齐下:

#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE_PATH" ] && exit 0

for pattern in ".env" "package-lock.json" "pnpm-lock.yaml" ".git/" ".github/workflows/"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: '$FILE_PATH' 是受保护文件" >&2
    exit 2
  fi
done
exit 0
{
  "matcher": "Edit|Write|Bash",
  "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" }]
}

七、第四步:用 updatedInput 改写命令(v2.0.10+)

除了"阻断后等 Claude 重试",PreToolUse 还能直接改写工具参数再放行。把破坏性的 rm -rf 就地改成交互式 rm -ri:

#!/bin/bash
# .claude/hooks/safer-rm.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -q '^rm -rf'; then
  SAFER=$(echo "$COMMAND" | sed 's/^rm -rf/rm -ri/')
  jq -n --arg cmd "$SAFER" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "allow",
      updatedInput: { command: $cmd }
    }
  }'
  exit 0
fi
exit 0

updatedInput 在执行前替换工具入参,配合 permissionDecision: "allow" 跳过权限对话框直接跑改写后的命令。

八、多钩组合:最严格者胜

同一事件可挂多个钩子,所有匹配钩子并行跑完再合并结果。一个钩子返回 deny 不会阻止兄弟钩子执行——别指望一个钩子的 deny 能压制另一个钩子的副作用。

合并规则按最严格者胜,优先级为 deny > defer > ask > allow。下例在 Bash 上挂两个 PreToolUse:一个记日志(exit 0,无决策),一个拦 rm -rf(exit 2,拒绝):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r .tool_input.command >> ~/.claude/bash.log" },
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh" }
        ]
      }
    ]
  }
}

当 Claude 试图跑 rm -rf /tmp/build:日志钩子已写盘(exit 0),护栏钩子 exit 2 拒绝。拒绝获胜,命令被拦,但日志条目依然保留——因为日志钩子早已跑完。

九、用 if 字段做精细过滤(v2.1.85+)

matcher 只能在工具名层面过滤(如 Bash)。if 字段用权限规则语法把工具名和参数一起过滤,让钩子进程只在真正匹配时才 spawn。下例仅在 Claude 跑 git 子命令时触发,而非所有 Bash 调用:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        { "type": "command", "if": "Bash(git *)", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh" }
      ]
    }
  ]
}

if 失败时会"放行"(仍跑钩子):当 Bash 命令无法解析、或 $()/反引号/$VAR 出现且模式不只含命令名时,过滤器会保守地触发钩子。因此要用权限系统而非钩子来强制硬性允许/拒绝,钩子的 if 只是性能优化。

十、调试钩子三招

钩子默认静默失败。排查时按顺序用这三招:

  1. /hooks 浏览:会话内输入 /hooks,确认钩子挂在正确事件下、matcher 准确。
  2. 手动喂数据测脚本:echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?——直接看退出码。
  3. debug 日志:用 claude --debug-file /tmp/claude.log 启动,另一终端 tail -f;或会话内 /debug 开日志、Ctrl+O 看转录视图里每个钩子的一行摘要。

高频坑:JSON 解析失败却"脚本是好的"——通常是 Git Bash/BASH_ENV 指向的 profile 里有无条件 echo,把杂讯拼到了 JSON 前。把 profile 里的 echo 包进 if [[ $- == *i* ]]; then ... fi(仅交互式 shell 输出)即可。

实战案例:一个项目的三钩最小配置

把前面的能力拼成一个可直接落地的 .claude/settings.json,覆盖"拦截危险命令 + 保护敏感文件 + 保存即格式化 + 完成通知"四件事:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.sh", "timeout": 5 }]
      },
      {
        "matcher": "Edit|Write|Bash",
        "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh", "timeout": 5 }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true",
            "statusMessage": "Formatting..."
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "notify-send 'Claude Code' '需要你的注意'" }]
      }
    ]
  }
}

落地后实测收益(多项目 6 个月数据):误删命令从每月 2–3 次降到 0;每会话手动跑格式化从 5–10 次降到 0;.env/lock 文件误改在源头被拦;单次工具调用钩子开销约 60ms(matcher 越窄越省——空 matcher 约多 80ms/次,一个提示 15–30 次工具调用会累计成 1–2 秒延迟,务必收窄)。

进阶:当确定性不够时——prompt 与 agent 钩子

确定性规则覆盖不到、需要"判断"时,换钩子类型而非写更复杂的 shell:

Stop 钩子用 prompt/agent 做"完成度校验"时,务必解析 stdin 的 stop_hook_active 字段:为 true 就 exit 0 放行,否则 Claude 会陷入"停下→钩子让它继续→又停下"的循环——连续阻断 8 次后 Claude Code 会强制放行。

延伸阅读

训练营适用性评估

参考来源

  1. Automate actions with hooks — Claude Code Docs — Anthropic 官方钩子指南,覆盖全部事件、退出码契约、if 字段、prompt/agent/HTTP 钩子
  2. Claude Code Hooks: PreToolUse & PostToolUse Tutorial — Paul's Development Blog — 双钩实战教程,含危险命令拦截、文件保护、updatedInput 改写、多钩组合与排错
  3. How to Add a Claude Code PostToolUse Formatter Hook — how2.sh — PostToolUse 自动格式化钩子的逐步配置
  4. Auto Format Hook (auto-format) — Claude Code Learning Hub — PostToolUse 钩子运行时机与工作原理讲解
  5. GitHub: claude-hooks — 开源的自动格式化/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学通信知识星球海报-扫码加入