首页 › tutorials › Hooks造护栏:拦危险命令与自动收尾

Hooks 造护栏:拦危险命令与自动收尾

概述

本教程聚焦 Claude Code 的 PreToolUse / PostToolUse 钩子(Hooks),教你把它们组合成一套"开发护栏系统"——在工具执行前拦截危险命令(rm -rf、强制推送、改密钥文件),在工具执行后自动收尾(跑 Prettier、补 Lint、触发测试)。护栏会与 -p 打印模式的批量任务叠加,让 100 个文件的非交互迁移也能"踩着刹车"安全跑完。适合已经会用 Claude Code 做项目、但想给自动化流程上一道可编程安全阀的开发者。

前置条件


一、先厘清:Hooks 护栏 ≠ 权限系统

很多人把 Hooks 和 permissions(allow / ask / deny)混为一谈。它们是两层不同性质的安全机制,批量任务场景里需要配合使用。

维度 权限系统(permissions) Hooks 护栏
判断时机 命令执行前,静态匹配 命令执行前后,可运行脚本动态判断
能否读参数 只能按前缀/正则匹配命令字符串 可解析 stdin 的完整 JSON,看任意字段
能否反馈给模型 否(只有允许/拒绝二态) 是,可把原因回传给 Claude,让它换方法
能否改写内容 否 PostToolUse 可注入决策、补充上下文
适合场景 固定白名单(如放行 npm test) 基于内容判断、需要后处理、需要可观测

一句话总结:权限系统是"看门大爷查名单",Hooks 护栏是"带脚本的安全工程师做现场判断"。批量任务里,名单挡不住语义层面的危险(比如 git push --force 在白名单 git 之下被放行),这时就需要 Hooks 出场。

为什么要特别强调批量任务?因为 -p 非交互模式下,Claude 收不到你的实时确认。一次 claude -p "把 100 个文件里的 var 换成 let" 跑起来,它会连续调用几十上百次 Edit/Bash,任何一次手滑(比如顺手 rm 了构建产物)都没有人工把关的机会。Hooks 就是在这个时候替你盯着每一锤。


二、PreToolUse:执行前的硬拦截器

2.1 护栏的工作原理

PreToolUse 在每次工具调用真正执行之前触发。Claude Code 会通过 stdin 把一段 JSON 喂给你的脚本,结构大致如下:

{
  "session_id": "abc123",
  "transcript_path": "/home/you/.claude/projects/.../session.jsonl",
  "cwd": "/home/you/my-project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules && git push --force origin main",
    "description": "清理并推送"
  }
}

你的脚本拿到这段 JSON 后,可以用 jq 或任意语言解析,做判断,然后通过退出码告诉 Claude Code 怎么处理:

⚠️ 已知坑:在部分版本中,exit code 2 会让 Claude 直接停止整个会话(参见 GitHub issue #24327),而不是优雅地"换一种方法"。如果你希望拦截但让 Claude 继续干活,更可控的做法是输出 JSON 的 decision 字段(见第三节)。

2.2 实战脚本:拦危险命令

新建 scripts/guard-bash.sh:

#!/usr/bin/env bash
# PreToolUse 护栏:拦危险命令
set -euo pipefail

input=$(cat)                                    # 从 stdin 读取 JSON
tool_name=$(echo "$input" | jq -r '.tool_name')

# 只检查 Bash 工具
if [ "$tool_name" != "Bash" ]; then exit 0; fi

cmd=$(echo "$input" | jq -r '.tool_input.command')

# 危险模式黑名单(按需扩充)
dangerous_patterns=(
  'rm[[:space:]]+-rf?[[:space:]]+/($|[[:space:]])'   # rm -rf /
  'git[[:space:]]+push.*--force'                       # 强制推送
  'git[[:space:]]+push.*-f'                            # 强制推送简写
  '>[[:space:]]*\.env'                                 # 覆盖 .env
  'DROP[[:space:]]+TABLE'                              # 删表
)

for pattern in "${dangerous_patterns[@]}"; do
  if echo "$cmd" | grep -qiE "$pattern"; then
    # 写 stderr,exit 2 阻断
    echo "🛑 护栏拦截:命令命中危险模式 [$pattern]。" >&2
    echo "🛑 原命令:$cmd" >&2
    echo "🛑 如确需执行,请人工确认后临时禁用护栏。" >&2
    exit 2
  fi
done

exit 0

赋权:

chmod +x scripts/guard-bash.sh

2.3 写进 settings.json

在项目根目录的 .claude/settings.json 注册:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/guard-bash.sh" }
        ]
      }
    ]
  }
}

$CLAUDE_PROJECT_DIR 是 Claude Code 自动注入的项目根环境变量,比硬编码绝对路径更可移植。matcher 字段填工具名(Bash、Edit、Write、Read,或 MCP 工具 mcp__server__tool),留空表示匹配全部工具,也支持正则(如 Bash|Edit)。

2.4 预期效果

现在让 Claude 执行一个危险操作:

> 帮我把当前分支强制推到远程

Claude 会调用 Bash 跑 git push --force,但护栏触发,工具被阻断,stderr 回传。你会看到 Claude 收到反馈后主动改口:

🛑 护栏拦截:命令命中危险模式 [git push --force]。
我注意到强制推送被项目护栏拦截了。改用普通推送,或你确认后我再解除护栏重试。

这就是 Hooks 比权限系统强的地方:模型不仅知道被拦了,还知道为什么,能自己换路径。


三、PostToolUse:执行后的自动收尾器

3.1 用 JSON 输出做可控反馈

PostToolUse 在工具执行之后触发,stdin 里会多出 tool_response 字段。它最常见的用途是"自动后处理"。除了用退出码,新版 Hooks 支持脚本往 stdout 输出一段 JSON 来精细控制行为,关键字段:

{
  "decision": "block",
  "reason": "这个文件被护栏标记为禁止修改"
}

用 JSON 的 decision: block 代替 exit 2,不会触发"会话停止"的副作用,是更推荐的"软阻断"方式。

3.2 实战脚本:编辑后自动格式化 + 补 Lint

新建 scripts/auto-format.sh:

#!/usr/bin/env bash
# PostToolUse 收尾:对编辑过的文件跑 Prettier
set -euo pipefail

input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')

# 只在 Edit / Write 后触发
case "$tool_name" in
  Edit|Write|MultiEdit) ;;
  *) exit 0 ;;
esac

file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
[ -z "$file_path" ] && exit 0

# 只处理项目内的 JS/TS 文件
case "$file_path" in
  *.js|*.jsx|*.ts|*.tsx) ;;
  *) exit 0 ;;
esac

[ -f "$file_path" ] || exit 0

# 跑 Prettier(项目里有就跑)
if command -v npx >/dev/null 2>&1; then
  npx --no-install prettier --write "$file_path" >/dev/null 2>&1 || true
fi

# 把格式化结果作为上下文喂回 Claude(可选)
echo "$file_path" >&2   # 写日志
exit 0

注册:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/guard-bash.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/auto-format.sh" }]
      }
    ]
  }
}

3.3 预期效果

让 Claude 批量重命名函数:

> 把 src/ 下所有旧函数名 fetchUser 重命名为 getUser

Claude 每改完一个文件,护栏自动跑 Prettier 把它格式化。整个过程你不需要手动干预,代码风格始终统一——这正是批量任务最需要的"确定性收尾"。


四、组合 -p 批量模式:护栏在无人值守时怎么表现

把护栏和 -p 模式叠在一起,是本教程的核心目标。关键点:Hooks 在 -p 非交互模式下照常触发,行为和交互模式一致——这是护栏能"替你盯着"的前提。

4.1 跑一个批量迁移任务

claude -p "把 src/ 下所有 var 声明改成 let,逐文件修改" \
  --allowedTools "Edit Read Bash" \
  --output-format stream-json \
  > migration.log

过程中: - 每次 Claude 要执行 Edit,先过 PreToolUse(这里 Edit 没配护栏,放行) - Edit 执行完,自动过 PostToolUse,Prettier 跟上 - 万一某次 Claude 想 rm 清理中间产物,PreToolUse 立刻阻断,反馈进 JSON 流,Claude 收到后改用别的方法,任务不中断

4.2 解析 stream-json 看护栏触发

stream-json 输出里,每次工具调用和 hook 反馈都是一个 JSON 对象。事后用 jq 提取护栏拦截记录:

jq -r 'select(.type=="assistant") | .message.content[]?
        | select(.type=="text") | .text' migration.log

如果护栏拦截过,你能在日志里看到 Claude 那句"护栏拦截了我"的自述——这条记录就是护栏生效的可审计证据。


五、整体工作流

flowchart TD
    A[Claude 决定调用工具] --> B{PreToolUse 护栏}
    B -->|exit 0 / decision approve| C[工具正常执行]
    B -->|exit 2 / decision block| D[阻断 + stderr 回传]
    D --> E[Claude 收到反馈<br/>换一种方法]
    E --> A
    C --> F{PostToolUse 收尾}
    F --> G[自动跑 Prettier / Lint]
    F --> H[追加测试 / 通知]
    G --> I[任务继续]
    H --> I
    I --> J{还有更多工具调用?}
    J -->|是| A
    J -->|否| K[任务完成<br/>stream-json 收尾]

整条链路的关键在于:护栏不是"挡一次就完",而是每一次工具调用都过一遍。批量任务的可靠性,正是靠这种"每锤必检"换来的。


六、实战案例:给 Node 项目装"开发护栏套件"

把前面所有片段拼成一个完整可用的方案。项目结构:

my-app/
├── .claude/
│   └── settings.json
├── scripts/
│   ├── guard-bash.sh        # PreToolUse 拦危险命令
│   ├── guard-write.sh       # PreToolUse 护密钥文件
│   └── auto-format.sh       # PostToolUse 自动格式化
└── src/
    └── ...

6.1 加一个密钥文件护栏 guard-write.sh

#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
case "$tool_name" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac

file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
case "$file_path" in
  *.env|.env*|*secrets*|*credentials*|*.pem|*.key)
    # 用 JSON decision 软阻断,避免 exit 2 的会话停止副作用
    printf '{"decision":"block","reason":"护栏:禁止修改敏感文件 %s,请走专门的密钥管理流程"}\n' "$file_path"
    exit 0
    ;;
esac
exit 0

6.2 完整 settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/guard-bash.sh" }]
      },
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/guard-write.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/auto-format.sh" }]
      }
    ]
  }
}

6.3 跑一次带护栏的批量任务验证

# 模拟批量重构,护栏全程在场
claude -p "为 src/ 下所有导出函数补上 JSDoc 注释" \
  --output-format stream-json \
  > jsdoc.log ; echo "退出码: $?"

退出码非 0 时翻 jsdoc.log 看护栏记录,定位是哪一步被拦。这就是一套自洽、可审计、零人工干预的开发护栏。


七、常见问题与排查

Q1:护栏把正常命令也拦了(误报)怎么办? 先确认是脚本逻辑问题还是匹配太宽。临时调试:直接把一段 JSON 喂给脚本本地跑。

echo '{"tool_name":"Bash","tool_input":{"command":"npm test"}}' \
  | bash scripts/guard-bash.sh ; echo "exit=$?"

退出码 2 就是命中了规则。逐步收紧 dangerous_patterns 的正则即可。

Q2:exit code 2 之后任务直接停了,不是我想要的"换方法"。 改用 JSON 的 decision: block 软阻断(见 6.1)。它在阻止当前工具调用的同时,把 reason 回传给 Claude 让它调整策略,且不触发会话停止。

Q3:matcher 写了 Edit,为什么 Write 操作没触发? matcher 是精确匹配(或正则匹配)工具名,不会自动覆盖相邻工具。要同时覆盖就写 Edit|Write|MultiEdit。

Q4:脚本里有耗时操作,怕拖慢批量任务。 给 hook 加 timeout(毫秒),超时按放行/失败处理。2026 版还支持 async 标记的异步 hook,适合"发通知"这类不需要等返回的场景:

{ "type": "command", "command": "notify.sh", "timeout": 5000 }

Q5:能不能拦 MCP 工具? 能。MCP 工具名形如 mcp__服务器名__工具名,直接写进 matcher 即可,比如拦数据库写入工具 mcp__pg__execute。


八、2026 新特性速览

这些特性让"护栏"从本地脚本进化成可对接企业基础设施的安全层。


延伸阅读


训练营适用性评估

参考来源

  1. Hooks reference — Claude Code Docs — 官方钩子事件、配置 schema、JSON 输入输出与退出码(含 exit 2 阻断)权威参考
  2. Claude Code Hooks: PreToolUse & PostToolUse Tutorial — Paul's Development Blog — 拦 rm -rf、自动跑 Prettier、保护 .env 的确定性实战示例
  3. Automate actions with hooks — Claude Code Docs — 用 hooks 强制项目规范、自动化重复任务、对接外部系统的官方指南
  4. Claude Code Hooks — Input & Output Schemas (Gist) — 可直接照着实现的 hook 输入输出 JSON 字段速查表
  5. [BUG] PreToolUse hook exit code 2 causes Claude to stop — GitHub issue #24327 — exit 2 软/硬阻断行为差异的讨论,是选择 decision JSON 方案的关键依据

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