首页 › tutorials › 拦截式钩子造自动化安全门禁

学习目标:读完本文,你能用 Claude Code 的 PreToolUse/PostToolUse 拦截式钩子,搭出一套"危险命令必拦、密钥泄露必挡、写完文件自动格式化、全部操作留痕"的自动化安全门禁,并把它接到 -p 批量模式与 Setup 钩子上,让无人值守的批量任务也跑在护栏之内。

适用人群:已经用过 Claude Code 做过几个项目、想让它在 CI/批处理场景里"撒手跑"却不敢的开发者。假设你了解 settings.json 的基本结构,但不熟悉钩子的拦截机制。

一、为什么拦截式钩子是自动化的"最后一道闸"

当你在交互模式下用 Claude Code,每一次 rm -rf、每一次写入 .env、每一次 git push --force,你都能在终端里看见并手动放行或拒绝。可一旦切到 -p 批量模式——比如让它一夜之间重构 100 个文件——你就失去了这层人眼监督。模型一旦"自作主张"执行了危险命令,可能造成的损失往往要到事后才被发现。

Claude Code 的权限系统(permissions.allow/deny)能做静态规则匹配,比如"永远禁止 Bash(rm -rf:*)"。但真实项目里的危险操作常常是动态的:

这些场景正是拦截式钩子(PreToolUse / PostToolUse)的用武之地。与"上下文注入式"钩子(SessionStart、UserPromptSubmit,用于往对话里塞背景信息)不同,拦截式钩子的核心能力是在工具执行前/后做判断并返回决策——既能挡,也能放行,还能在放行后顺手补一刀(格式化、留痕、扫描)。

本文要造的"自动化安全门禁"由四道关卡组成,全部用钩子实现:

关卡 钩子事件 职责
命令护栏 PreToolUse (matcher=Bash) 拦 rm -rf /、git push -f、curl | sh 等危险命令
密钥门神 PreToolUse (matcher=Edit|Write) 写入前扫描 diff,发现疑似密钥则 deny
自动修复 PostToolUse (matcher=Edit|Write) 写完自动跑 prettier/eslint --fix
全量留痕 PostToolUse (matcher=*) 每次工具调用追加到审计日志
CI 预检 Setup (matcher=maintenance) -p --maintenance 模式下做一次性环境检查

二、前置条件

三、钩子的三种节奏:先想清楚在哪一道闸门拦截

在动手前,先建立一个心智模型。Claude Code 把钩子事件按触发频率分成三档,理解这三档是判断"用哪个事件"的前提:

flowchart TD
    A[会话开始] --> B[SessionStart 一次/会话]
    B --> C[你提交一条 prompt]
    C --> D[UserPromptSubmit 一次/轮]
    D --> E[智能体循环 每轮可能多次]
    E --> F{PreToolUse 工具执行前}
    F -->|deny| G[阻断 该工具不执行]
    F -->|放行| H[工具真正执行]
    H --> I[PostToolUse 执行后]
    I --> J[同一轮内继续调用工具?]
    J -->|是| F
    J -->|否| K[Stop 本轮结束]
    K --> L[你下一条 prompt]
    L --> D
    style G fill:#fdd,stroke:#c00
    style H fill:#dfd,stroke:#0a0

关键点:只有 PreToolUse 能在工具真正执行前把它拦下来。PostToolUse 是"事后"——文件已经写完了、命令已经跑完了,你能做的是格式化、留痕、告警,但无法阻止已发生的事。所以"挡"的活儿必须交给 PreToolUse,"补救"的活儿才交给 PostToolUse。这是整条门禁的设计基石。

钩子的配置统一写在 .claude/settings.json 的 hooks 字段下,三层嵌套:事件名 → matcher 组 → handler 列表。下面我们逐关搭建。

四、第一关:PreToolUse 命令护栏——拦住 rm -rf /

操作说明

先建钩子脚本。在项目根目录创建 .claude/hooks/block-dangerous.sh:

#!/usr/bin/env bash
# .claude/hooks/block-dangerous.sh
# 从 stdin 读取钩子输入 JSON,提取 Bash 命令,命中黑名单则 deny
set -euo pipefail
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# 危险命令黑名单:递归删根、强制推送到主分支、管道执行远程脚本
deny_patterns=(
  'rm[[:space:]]+-rf[[:space:]]+/?($|[[:space:]])'   # rm -rf / 或 rm -rf / ...
  'git[[:space:]]+push.*--force'                       # 强制推送
  'curl[[:space:]].*\|[[:space:]]*sh'                 # curl ... | sh
  '>[[:space:]]*/dev/sda'                              # 直接写裸设备
)

for pat in "${deny_patterns[@]}"; do
  if echo "$COMMAND" | grep -Eq "$pat"; then
    cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "命令命中安全门禁黑名单: $pat。请改用更安全的等价操作,例如限制作用域或分步确认。"
  }
}
EOF
    exit 0
  fi
done
exit 0   # 静默放行:exit 0 + 无输出 = 不干预,走正常权限流程

给它执行权限:chmod +x .claude/hooks/block-dangerous.sh。

决策返回的两种姿势:脚本通过 stdout 打印一段 JSON,里面 permissionDecision 设为 "deny" 即可阻断。"deny" 是硬挡(直接拒绝且把 reason 喂给模型);此外还有 "allow"(直接放行,绕过后续权限弹窗)。如果脚本什么都不输出、exit 0,表示"我不表态",工具调用会继续走默认权限流程——保持沉默不会批准它。另一个姿势是直接 exit 2,这也会阻断,但 reason 只能来自 stderr 文本,不如 JSON 灵活,本文统一用 JSON。

然后在 .claude/settings.json 注册它:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *) || Bash(git push *) || Bash(curl *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

注意 if 字段。它用权限规则语法对工具名+参数一起过滤,比 matcher(只匹配工具名)更细。Bash(rm *) 的意思是"只有当 Bash 命令的某个子命令以 rm 开头时,这个 handler 才真正 spawn"。

预期结果

if 的妙处在于省进程:模型跑 npm test 时,Bash(rm *) 不匹配,脚本根本不会启动。只有当命令形如 rm -rf dist 时脚本才会被唤起。这是官方文档反复强调的性能要点——if 是"最佳努力"过滤器,能过滤掉绝大多数无关调用。

你可以手动喂数据测试脚本逻辑,不必真去触发:

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
  | .claude/hooks/block-dangerous.sh

应输出一段含 "permissionDecision": "deny" 的 JSON。当模型在会话里真的想跑 rm -rf /tmp/build 时,工具调用会被阻断,模型会收到你写的中文 reason,并据此换一种写法重试。

常见问题与解决

五、第二关:PreToolUse 密钥门神——写入前扫描内容

静态 deny 规则挡不住"内容里的密钥",因为权限系统看的是命令/路径,看不到即将写入的字符。这一关用 PreToolUse 拦截 Edit/Write,把待写内容过一遍密钥正则。

操作说明

新建 .claude/hooks/secret-scan.sh:

#!/usr/bin/env bash
# .claude/hooks/secret-scan.sh
# 扫描 Edit/Write 的待写内容,命中常见密钥形态则 deny
set -euo pipefail
INPUT=$(cat)

# Write 的内容在 tool_input.content;Edit 的新内容在 tool_input.new_string
CONTENT=$(echo "$INPUT" | jq -r '
  .tool_input.content // .tool_input.new_string // ""')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // "?"')

# 常见密钥指纹:AWS Access Key、GitHub PAT、Google API Key、私钥头
patterns=(
  'AKIA[0-9A-Z]{16}'                # AWS Access Key ID
  'gh[pousr]_[A-Za-z0-9]{36}'       # GitHub fine-grained / classic PAT
  'AIza[0-9A-Za-z_-]{35}'           # Google API Key
  '-----BEGIN (RSA |EC |)PRIVATE KEY-----'
)

for pat in "${patterns[@]}"; do
  if echo "$CONTENT" | grep -Eq "$pat"; then
    cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "检测到疑似密钥 ($pat) 将写入 $FILE_PATH。请改用环境变量或密钥管理服务,不要把密钥硬编码进文件。"
  }
}
EOF
    exit 0
  fi
done
exit 0

chmod +x 后注册:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/secret-scan.sh"
          }
        ]
      }
    ]
  }
}

预期结果

当模型试图 Write 一个含 AKIAIOSFODNN7EXAMPLE 的配置文件时,写入会被拦在执行前,模型收到密钥告警。这一关的价值在批量场景里尤其明显:让模型重构 100 个文件时,它会遍历读取并改写,过程中很容易把某个 .env.example 的真实密钥"顺手"复制到代码里——门神会在那一刻喊停。

常见问题与解决

六、第三关:PostToolUse 自动修复——写完即格式化

"挡"是防守,"修"是进攻。模型写完文件后,代码风格未必符合团队规范。用 PostToolUse 在写入成功后自动跑格式化,既省了模型一次工具调用,又保证落盘的代码永远是干净的。

操作说明

新建 .claude/hooks/auto-format.sh:

#!/usr/bin/env bash
# .claude/hooks/auto-format.sh
# 写入成功后,对目标文件跑 prettier(若项目里有)
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

[ -z "$FILE_PATH" ] && exit 0
[ -f "$FILE_PATH" ] || exit 0

# 只处理 JS/TS,且项目装了 prettier 才跑
case "$FILE_PATH" in
  *.js|*.ts|*.jsx|*.tsx|*.json|*.css|*.md)
    if [ -x "node_modules/.bin/prettier" ]; then
      node_modules/.bin/prettier --write "$FILE_PATH" >/dev/null 2>&1 || true
    fi
    ;;
esac
exit 0

注册到 PostToolUse:

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

预期结果与注意

每次模型写完一个 .js 文件,prettier 立刻在该文件上跑一次。模型下次读这个文件时看到的就是格式化后的版本,避免"格式漂移"累积。

关键陷阱:PostToolUse 的脚本如果输出到 stderr 或非零退出,可能会把错误信息当成"工具执行异常"反馈给模型,导致模型困惑地"修复"一个根本不存在的问题。所以脚本结尾务必 exit 0,内部命令加 || true 吞掉错误。PostToolUse 永远不要 deny——它没有 permissionDecision 这回事,工具已经执行完了。

七、第四关:全量留痕——PostToolUse 审计日志

合规场景要求"模型动过什么,全记下来"。一个 matcher 为 * 的 PostToolUse 钩子,能把每次工具调用追加进日志文件。因为"记日志"不影响主流程,这里正好用上 async 字段——后台运行,不阻塞智能体循环。

操作说明

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "async": true,
            "command": "jq -c '{ts: now|todateiso8601, tool: .tool_name, input: .tool_input}' >> ${CLAUDE_PROJECT_DIR}/.claude/audit.jsonl"
          }
        ]
      }
    ]
  }
}

预期结果

.claude/audit.jsonl 会逐行累积形如 {"ts":"2026-07-01T08:14:22Z","tool":"Edit","input":{...}} 的记录。async: true 让写日志在后台进行,模型不会停下来等磁盘 I/O。把 audit.jsonl 加进 .gitignore,避免把审计日志本身提交进仓库。

async vs asyncRewake:普通 async 是"发了就忘",钩子结果不影响模型。asyncRewake: true 则在钩子以退出码 2 结束时唤醒模型,把 stderr 当系统提醒塞回去——适合"后台长任务失败时让模型知道"的场景,但写审计日志用不到。

八、第五关:Setup 钩子——给 -p 批量任务做一次性预检

前三关是"运行中"的护栏。但批量任务开始前,你还想做一次环境体检:依赖装好了吗?.env 没被误提交吗?测试能跑通吗?Setup 事件专为此设计——它只在 claude --init-only 或 -p 模式带 --init/--maintenance 时触发,做一次性的 CI/脚本准备。

操作说明

新建 .claude/hooks/preflight.sh:

#!/usr/bin/env bash
# .claude/hooks/preflight.sh
# 批量任务启动前的一次性体检
set -uo pipefail
fail=0

echo "[preflight] 检查依赖..." >&2
[ -d node_modules ] || { echo "✗ node_modules 缺失,先 npm ci" >&2; fail=1; }

echo "[preflight] 检查密钥是否误提交..." >&2
if git ls-files | grep -Eq '\.env$'; then
  echo "✗ .env 已被 git 跟踪,立即处理" >&2; fail=1
fi

echo "[preflight] 检查测试基线..." >&2
if ! npm test --silent >/dev/null 2>&1; then
  echo "✗ 测试基线未通过,先修红再开工" >&2; fail=1
fi

[ $fail -eq 0 ] && echo "[preflight] 通过" >&2
exit 0

注册:

{
  "hooks": {
    "Setup": [
      {
        "matcher": "maintenance",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/preflight.sh" }
        ]
      }
    ]
  }
}

预期结果

claude -p --maintenance "检查环境" 2>&1 | grep preflight

会看到三步体检的输出。--maintenance 模式下 Claude Code 跑完 Setup 钩子就做收尾,不进入完整智能体循环——非常适合"只体检不干活"的 CI 步骤。体检不过时,脚本的 stderr 会出现在日志里,CI 据此决定是否继续。

九、把五道关拼成一条 -p 批量流水线

现在把所有钩子合并进同一个 settings.json(同名事件下可以放多个 matcher 组),再写一个批量驱动脚本。完整配置:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh" }
        ]
      },
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/secret-scan.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/auto-format.sh" }
        ]
      },
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "async": true,
            "command": "jq -c '{ts: now|todateiso8601, tool: .tool_name, input: .tool_input}' >> ${CLAUDE_PROJECT_DIR}/.claude/audit.jsonl" }
        ]
      }
    ],
    "Setup": [
      {
        "matcher": "maintenance",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/preflight.sh" }
        ]
      }
    ]
  }
}

驱动脚本 .claude/run-batch.sh:先体检,再以 stream-json 输出跑批量任务,最后扫审计日志统计拦截次数。

#!/usr/bin/env bash
# .claude/run-batch.sh <prompt-file>
set -euo pipefail
PROMPT_FILE="${1:?用法: run-batch.sh <prompt-file>}"

echo "=== 1. 预检 ===" 
claude -p --maintenance "preflight" || { echo "预检失败,中止"; exit 1; }

echo "=== 2. 批量执行(护栏已激活)==="
claude -p "$(cat "$PROMPT_FILE")" \
      --output-format stream-json \
      --verbose \
  | tee .claude/run-stream.jsonl

echo "=== 3. 拦截统计 ==="
grep -c '"deny"' .claude/audit.jsonl 2>/dev/null \
  && echo "次危险操作被门禁拦截" \
  || echo "本次无拦截"

--output-format stream-json 把每个工具调用、每次拦截都吐成结构化 JSON 行,方便事后用 jq 复盘模型在批量任务里到底做了什么、被挡了几次。这就是一条"预检 → 护栏内批量 → 审计复盘"的完整闭环。

十、Mermaid:整条门禁的执行流

sequenceDiagram
    autonumber
    participant U as 你的脚本 run-batch.sh
    participant CC as Claude Code (-p)
    participant H as 钩子处理器
    participant FS as 文件系统

    U->>CC: claude -p --maintenance (预检)
    CC->>H: Setup 事件 → preflight.sh
    H-->>CC: 通过 / 失败
    alt 预检失败
        CC-->>U: 中止
    else 预检通过
        U->>CC: claude -p "批量重构" --output-format stream-json
        loop 每个工具调用
            CC->>H: PreToolUse (Bash?) → block-dangerous.sh
            alt 命中黑名单
                H-->>CC: deny + 中文理由
                CC->>CC: 模型据理由换写法
            else 放行
                H-->>CC: 静默 (exit 0)
                CC->>H: PreToolUse (Edit?) → secret-scan.sh
                alt 命中密钥
                    H-->>CC: deny
                else 放行
                    CC->>FS: 真正执行写入
                    CC->>H: PostToolUse → auto-format.sh
                    H->>FS: prettier --write
                    H-->>CC: (async) 审计日志追加
                end
            end
        end
        CC-->>U: stream-json 流(含每次拦截)
        U->>U: grep deny 统计拦截次数
    end

十一、常见问题汇总

十二、训练营适用性评估

维度 评估
适合的训练营类型 CI/CD 自动化营、DevSecOps 营、AI 工程化营。本教程的核心收益是"让 AI 无人值守也安全",正是这些方向的关键能力。
不太适合 纯入门营。需要先理解权限系统与会话生命周期,零基础建议先学"权限配置入门"。
预计教学时长 3–4 小时。其中 1 小时讲钩子三种节奏与决策返回机制,2 小时动手搭五道关,0.5–1 小时跑批量流水线并复盘审计日志。
学员产出 一个可提交进仓库的 .claude/ 目录(settings.json + 5 个钩子脚本 + run-batch.sh),能在自己的项目里直接复用。
进阶延伸 学完可接 prompt/agent 类型钩子(让另一个 Claude 实例做内容审查),或把 HTTP 钩子接到企业 SIEM 做集中式审计。

参考来源

  1. Hooks reference — Claude Code Docs — 官方钩子完整参考:事件清单、JSON 输入输出格式、permissionDecision 决策机制、if/matcher 语法、async/Setup 等,本文所有技术细节的权威出处。
  2. Automate actions with hooks — Claude Code Docs — 官方快速上手指南,含从零配置第一个钩子的分步示例。
  3. Claude Code Hooks: PreToolUse & PostToolUse Tutorial — paul-schick.com — 实战向博客,演示用 PreToolUse 拦 rm -rf、保护 .env、用 PostToolUse 接 Prettier 自动格式化。
  4. Claude Code Hooks (2026): Block Claude Reading .env + 30 Hook Events — morphllm.com — 2026 年版钩子全景,列出 30+ 钩子事件与"用 exit code 2 拦截 .env 读取"等防护配方。
  5. Claude Code Hooks: 15 Automations Every Developer Should Set Up — getbeam.dev — 15 个开箱即用的 PreToolUse/PostToolUse 自动化配置,可直接作为门禁配方库。

[!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学通信知识星球海报-扫码加入