首页 › tutorials › 造工具级安全钩子建执行围栏

概述

CLAUDE.md 里写的"不要删数据库""不要改生产配置"只是建议,Claude 大多数时候会遵守,但一旦在 acceptEdits、bypassPermissions 或无头批量模式下高速运转,就可能在某个长链条里"顺手"执行了危险操作。本教程带你用 Claude Code 的工具级钩子(PreToolUse / PostToolUse)搭建一套硬性安全围栏——每一次工具调用在执行前都必须先过一道脚本关卡,命中危险规则就直接拦截并把理由回传给模型。

适用人群:已经会用 Claude Code 基础交互、想把它放进 CI / 批量 / 自治场景但又怕"翻车"的开发者与 DevOps 工程师。学完你将掌握:钩子拦截时机、settings.json 配置结构、if 匹配语法、stdin JSON 解析、permissionDecision 三态决策(allow/ask/deny),并能跑通一个"围栏内批量重构"的完整示例。

前置条件

关键认知:钩子与权限模式是两套独立机制。即便你用 --permission-mode bypassPermissions,PreToolUse 钩子依然会在工具执行前触发,deny 决策照样能拦下命令。这正是它适合做"最后一道防线"的原因。

第一步:分清三类钩子的拦截时机

Claude Code 的钩子按触发频率分三档,做安全围栏只用第一档:

类型 事件 触发频率 能否阻止动作
会话级 SessionStart / SessionEnd 每会话一次 否(已发生或不可逆)
轮次级 UserPromptSubmit / Stop 每轮一次 是
工具级 PreToolUse / PostToolUse 每次工具调用 PreToolUse 可阻止,PostToolUse 不能

围栏的核心是 PreToolUse:它在 Claude"决定调用某个工具"和"工具真正执行"之间插入一个检查点。返回 deny 就把这次调用挡回去;返回 allow 直接放行(跳过后续权限弹窗);返回 ask 转人工确认。PostToolUse 则在工具跑完后触发,做格式化、跑测试、记录审计日志——动作已经发生,拦不住,但能"擦屁股"。

注意区分:本教程聚焦工具级拦截,与"会话中枢"类教程讲的 SessionStart/Stop 生命周期钩子是不同层次,互不冲突,可叠加使用。

第二步:搭建项目骨架

新建一个测试目录并初始化钩子目录:

mkdir -p guardrail-demo/.claude/hooks guardrail-demo/src
cd guardrail-demo
echo 'DB_PASSWORD=s3cret' > .env          # 受保护文件
echo 'API_KEY=demo' > src/config.py        # 待重构文件

目录结构最终如下:

guardrail-demo/
├── .claude/
│   ├── settings.json          # 钩子注册表
│   └── hooks/
│       ├── block-destructive.sh   # 拦危险命令
│       ├── protect-files.sh       # 保护关键文件
│       └── auto-format.sh         # 编辑后自动格式化
├── src/config.py
└── .env

钩子脚本由 Claude Code 直接执行,必须带可执行权限,否则会被静默跳过:

chmod +x .claude/hooks/*.sh

第三步:注册钩子到 settings.json

在 .claude/settings.json 写入以下内容。注意 matcher 决定钩子作用于哪个工具,command 用 $CLAUDE_PROJECT_DIR 保证路径可移植(该变量由 Claude Code 在执行钩子时注入,指向项目根):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-destructive.sh" }
        ]
      },
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-format.sh" }
        ]
      }
    ]
  }
}

写完后用 /hooks 交互命令确认注册成功,应能看到三组钩子分别挂在 PreToolUse 与 PostToolUse 下。

进阶:if 字段用权限规则语法做前置过滤,不匹配时连进程都不启动,省 CPU。例如给 Bash 钩子加 "if": "Bash(rm *)",则只有 rm 开头的命令才会真正调用脚本。本教程为求通用先不写 if,让脚本内部用正则判断。

第四步:写 Bash 危险命令拦截器

PreToolUse 钩子启动时,Claude Code 把工具信息以 JSON 形式打到 stdin,结构是 {"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}, ...}。我们用 jq 取出命令再正则匹配。创建 .claude/hooks/block-destructive.sh:

#!/usr/bin/env bash
# PreToolUse on Bash —— 拦截破坏性命令
set -euo pipefail
CMD="$(jq -r '.tool_input.command // ""')"

# 高危规则集:递归删除 / 强推 / 覆盖设备 / 危险权限 / 数据库破坏 / fork 炸弹
PATTERN='rm[[:space:]]+-[a-zA-Z]*r|git[[:space:]]+push.*(-f|--force)|chmod[[:space:]]+777|>[[:space:]]*/dev/sd|dd[[:space:]].*of=|drop[[:space:]]+(table|database)|:[()]\{.*;\}'

if echo "$CMD" | grep -Eq "$PATTERN"; then
  REASON="⛔ 围栏拦截:命中破坏性操作规则。如确需执行,请人工运行或临时 /hooks 禁用本钩子。命中命令片段:$CMD"
  # 输出结构化决策(推荐方式:理由清晰、可区分 ask/deny)
  jq -n --arg r "$REASON" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
  exit 0
fi
exit 0   # 无输出 + exit 0 = 不干预,走正常权限流程

预期结果:当 Claude 试图执行 rm -rf src/legacy 时,命令不会执行,Claude 收到 deny 理由后会改用逐文件删除或停下来询问你。

最简写法(不依赖 JSON,适合快速上手):把决策那两行换成——把理由打到 stderr、exit 2。exit code 2 在 PreToolUse 上的语义就是"阻止这次调用,stderr 内容回传给 Claude":

echo "⛔ 危险命令被拦截:$CMD" >&2
exit 2

两种写法效果近似,但 JSON 方式多了 ask(转人工确认)和 allow(直接放行免弹窗)两个选项,围栏场景更推荐 JSON。

常见问题

第五步:写文件保护拦截器

危险命令之外,最常见的"翻车"是 Claude 改写了不该碰的文件:.env、生产配置、Terraform state、私钥、甚至 settings.json 自身。给 Edit/Write/MultiEdit 挂一个保护器。创建 .claude/hooks/protect-files.sh:

#!/usr/bin/env bash
# PreToolUse on Edit/Write/MultiEdit —— 保护关键文件
set -euo pipefail
# Edit/Write 用 file_path,NotebookEdit 用 notebook_path,兼容取值
TARGET="$(jq -r '.tool_input.file_path // .tool_input.notebook_path // ""')"

# 受保护路径:环境变量 / 密钥 / 生产配置 / tfstate / 钩子配置自身
PROTECTED='(\.env$|\.env\.[a-z]+$|secrets\.|\.prod\.ya?ml$|terraform/.*\.tfstate$|\.claude/settings\.json$|id_rsa$|pki/)'

if echo "$TARGET" | grep -Eq "$PROTECTED"; then
  REASON="⛔ 受保护文件:$TARGET 禁止由 Claude 直接改写。请走人工评审或专用发布流程。"
  jq -n --arg r "$REASON" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
  exit 0
fi
exit 0

预期结果:让 Claude "把 .env 里的密钥换成测试值"会被直接拒绝;而改 src/config.py 正常放行。

常见问题

第六步:写 PostToolUse 自动格式化器

围栏不只防破坏,还能"善后"。PostToolUse 在文件被编辑后触发,可读 tool_input.file_path 拿到刚改的文件,按扩展名跑对应格式化工具。创建 .claude/hooks/auto-format.sh:

#!/usr/bin/env bash
# PostToolUse on Edit/Write/MultiEdit —— 编辑后自动格式化
set -euo pipefail
FILE="$(jq -r '.tool_input.file_path // "")"
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0

case "$FILE" in
  *.py)        command -v black   >/dev/null 2>&1 && black   -q "$FILE" 2>/dev/null || true ;;
  *.ts|*.tsx|*.js|*.jsx|*.json|*.css)
               command -v prettier >/dev/null 2>&1 && prettier --write "$FILE" >/dev/null 2>&1 || true ;;
  *.go)        command -v gofmt  >/dev/null 2>&1 && gofmt  -w "$FILE" 2>/dev/null || true ;;
esac
exit 0

PostToolUse 即便 exit 2 也拦不住已发生的编辑(动作已完成),它的价值是副作用编排:格式化、跑单测、写审计日志、统计改动行数。若想"编辑后跑测试且失败就回滚提示",把 black 换成 pytest,并在失败时往 stderr 输出失败摘要——Claude 会读到并在下一轮自我修复。

第七步:在围栏内跑无头批量任务

围栏搭好,用 -p 无头模式验证它真的生效。先跑一个正常的批量重构:

claude -p "把 src/ 下所有 Python 文件里的全局变量加类型注解,保持逻辑不变" \
  --allowedTools "Edit,Read,Glob,Grep,Bash(black *)" \
  --permission-mode acceptEdits

尽管开了 acceptEdits(免编辑确认),但 PostToolUse 仍会触发 black 自动格式化,PreToolUse 仍在每次 Edit 前检查路径——围栏照常工作。

再故意触发拦截:

claude -p "清理项目:删除 src/legacy 整个目录" --permission-mode acceptEdits

预期结果:Claude 内部尝试构造 rm -rf src/legacy 时,block-destructive.sh 命中 rm -r 规则返回 deny。命令不执行,理由回传,Claude 看到后改用 Bash 逐个删除文件或 Glob+Bash(rm <file>) 安全方式,并在无法安全完成时停下来报告。

最后验证文件保护:

claude -p "把 .env 里的 DB_PASSWORD 改成 test123" --permission-mode acceptEdits
# 预期:protect-files.sh 命中 \.env$,deny,Claude 报告"受保护文件禁止改写"

这三条命令跑下来,围栏的"防破坏 / 防误改 / 善后格式化"三件事就都验证了。

实战案例:可一键复现的围栏套件

把上述三脚本 + settings.json 放进 .claude/,再写一个 verify.sh 自检脚本,方便团队任何人 clone 后一键验证围栏是否在线:

#!/usr/bin/env bash
# verify.sh —— 围栏自检
set -e
cd "$(dirname "$0")"
H=.claude/hooks

echo "[1] 危险命令拦截"
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | $H/block-destructive.sh \
  | jq -r '.hookSpecificOutput.permissionDecision'   # 期望输出 deny

echo "[2] 文件保护"
echo '{"tool_name":"Edit","tool_input":{"file_path":"/abs/.env"}}' | $H/protect-files.sh \
  | jq -r '.hookSpecificOutput.permissionDecision'   # 期望输出 deny

echo "[3] 正常放行"
echo '{"tool_name":"Edit","tool_input":{"file_path":"/abs/src/config.py"}}' | $H/protect-files.sh
echo "exit=$?"                                         # 期望 exit=0 且无 deny 输出

把它接进 CI:每次 PR 跑 bash verify.sh,围栏规则一旦被改坏立刻红灯。这样"钩子配置"本身也成了受版本控制、可测试的代码资产——而不是某个开发者机器上的隐式约定。

整体工作流程

flowchart TD
    A["Claude 决定调用工具"] --> B{"PreToolUse 钩子触发"}
    B -->|"matcher 匹配 Bash/Edit/..."| C["脚本读 stdin JSON<br/>取 tool_input"]
    C --> D{"命中危险规则?"}
    D -->|是| E["输出 permissionDecision: deny<br/>+ reason"]
    D -->|否| F["exit 0 放行<br/>可 allow 跳过弹窗"]
    E --> G["🚫 阻止本次调用<br/>reason 回传 Claude"]
    F --> H["✅ 执行工具"]
    H --> I{"PostToolUse 钩子"}
    I --> J["读 tool_response / file_path<br/>自动格式化 / 跑测试 / 审计日志"]
    J --> K["结果反馈给 Claude<br/>进入下一轮决策"]
    G --> L["Claude 收到拒绝理由<br/>改用安全方式或停下询问"]

延伸阅读

训练营适用性评估

参考来源

  1. Hooks reference — Claude Code Docs — 官方钩子权威参考,含 30+ 事件、JSON 输入输出、exit code 2 语义与 permissionDecision 决策字段
  2. Automate actions with hooks — Claude Code Docs — 官方快速上手指南,含 block-rm.sh 完整拦截示例与 if 匹配流程
  3. Claude Code Hooks: PreToolUse & PostToolUse Tutorial — paul-schick.com — 把钩子定位为"将 CLAUDE.md 变成强制规则"的执行层,配实战代码
  4. Claude Code Hooks: The Automation Layer Nobody Knows About — dev.to — settings.json 配置样例与 PreToolUse/PostToolUse/SessionStart 实践对比
  5. Chapter 5: Hooks — Claude Code Best Practices — 系统化的钩子章节,覆盖 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学通信知识星球海报-扫码加入