<!-- title: Hooks造护栏：拦危险命令与自动收尾 -->

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

## 概述

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

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常输出），熟悉基本对话与工具调用
- 一台 macOS / Linux 环境（脚本以 Bash 为例，Windows 可用 Git Bash 或 WSL）
- 一个真实项目目录用于实战（Node 工程最佳，可演示自动格式化）
- 了解 `settings.json` 的基本结构（项目级 `.claude/settings.json` 或用户级 `~/.claude/settings.json`）

---

## 一、先厘清：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 喂给你的脚本，结构大致如下：

```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 0**：放行，工具正常执行
- **exit 2**：**阻断**（blocking error），工具不执行，stderr 内容会回传给 Claude 作为反馈
- **其他非零码**：非阻断错误，stderr 同样反馈给 Claude，但语义上属于"警告"

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

### 2.2 实战脚本：拦危险命令

新建 `scripts/guard-bash.sh`：

```bash
#!/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
```

赋权：

```bash
chmod +x scripts/guard-bash.sh
```

### 2.3 写进 settings.json

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

```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 来精细控制行为，关键字段：

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

- `decision`：`approve`（批准，仅部分事件生效）/ `block`（阻断并把 reason 回传）
- `hookSpecificOutput.additionalContext`：给 Claude 注入额外上下文（PostToolUse、UserPromptSubmit 常用）
- `systemMessage`：给用户展示一条系统提示

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

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

新建 `scripts/auto-format.sh`：

```bash
#!/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
```

注册：

```json
{
  "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 跑一个批量迁移任务

```bash
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` 提取护栏拦截记录：

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

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

---

## 五、整体工作流

```mermaid
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`

```bash
#!/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

```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 跑一次带护栏的批量任务验证

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

- 试图改 `.env`？→ guard-write.sh 软阻断，Claude 跳过
- 想顺手 `rm` 旧文件？→ guard-bash.sh 硬拦
- 每个文件改完？→ auto-format.sh 自动 Prettier

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

---

## 七、常见问题与排查

**Q1：护栏把正常命令也拦了（误报）怎么办？**
先确认是脚本逻辑问题还是匹配太宽。临时调试：直接把一段 JSON 喂给脚本本地跑。
```bash
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，适合"发通知"这类不需要等返回的场景：

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

**Q5：能不能拦 MCP 工具？**
能。MCP 工具名形如 `mcp__服务器名__工具名`，直接写进 matcher 即可，比如拦数据库写入工具 `mcp__pg__execute`。

---

## 八、2026 新特性速览

- **HTTP Hooks**：`type: "http"` 直接把事件 POST 到一个端点，免写脚本，适合接 Slack / 企业内部审批系统
- **Async Hooks**：不阻塞主流程，适合通知、日志这类"发了就行"的场景
- **Prompt Hooks**：针对 prompt 本身做拦截/改写，比 UserPromptSubmit 更聚焦
- **MCP Tool Hooks**：matcher 原生支持 MCP 工具名，外部工具也能纳入护栏体系

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

---

## 延伸阅读

- Claude Code 官方 Hooks 参考文档（事件、schema、退出码全表）
- Automate actions with hooks（官方快速上手，含更多示例）
- Claude Code SDK 文档（`-p` / `stream-json` / `--resume` 全参数）
- GitHub issue #24327（exit code 2 行为讨论，理解软/硬阻断差异）

---

## 训练营适用性评估

- **适合类型**：企业内训、安全合规导向的 AI 编程训练营、DevOps/SRE 自动化专场
- **不适合**：纯零基础入门营（建议先过 Round 1 安装与基础）
- **预计教学时长**：2.5–3 小时（含 1 小时讲解 + 1.5 小时动手装护栏套件）
- **配套练习**：让学员为自己的真实仓库设计 3 条护栏规则并验证拦截日志，作为结业作业
- **进阶衔接**：可与"权限配置入门""生命周期钩子造自动上下文引擎"组成 Hooks 三连讲，从静态名单 → 上下文注入 → 工具拦截形成完整安全闭环

## 参考来源

1. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 官方钩子事件、配置 schema、JSON 输入输出与退出码（含 exit 2 阻断）权威参考
2. [Claude Code Hooks: PreToolUse & PostToolUse Tutorial — Paul's Development Blog](https://paul-schick.com/posts/claude-code-hooks-pretooluse-posttooluse/) — 拦 `rm -rf`、自动跑 Prettier、保护 `.env` 的确定性实战示例
3. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — 用 hooks 强制项目规范、自动化重复任务、对接外部系统的官方指南
4. [Claude Code Hooks — Input & Output Schemas (Gist)](https://gist.github.com/FrancisBourre/50dca37124ecc43eaf08328cdcccdb34) — 可直接照着实现的 hook 输入输出 JSON 字段速查表
5. [\[BUG\] PreToolUse hook exit code 2 causes Claude to stop — GitHub issue #24327](https://github.com/anthropics/claude-code/issues/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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
