---
title: "Prompt 会忘，Hook 不会：Claude Code 自动化守卫完全实战"
author: deletexiumu
pubDatetime: 2026-04-12T20:00:00+08:00
featured: false
draft: false
tags:
  - Claude Code
  - 教程
  - AI 编程
description: "从 rm -rf 误删到 CI 自动化，Claude Code Hooks 让 AI 的每次操作都可控。涵盖 4 种实现类型、6 类核心事件、PermissionDenied 等 2026 新特性，附完整可运行脚本。"
---


![封面：Prompt 会忘，Hook 不会](/blog/claude-code-hooks-automation-guard/cover.png)

你有没有经历过这种事：让 Claude Code 帮你重构一个模块，它干得挺好，但中间突然执行了一个 `rm -rf build/`——把你还没提交的临时文件一起删了。或者你让它修一个 bug，它改完代码顺手来了一句 `git push --force origin main`，把同事刚合进去的 PR 覆盖了。

你事后复盘，发现这些操作你其实事先就能预见。`rm -rf` 在这个项目里不该碰 build 目录（里面有手动放的资源文件），force push 到 main 在你们团队永远不应该发生。但 Claude Code 不知道这些规则。你在 CLAUDE.md（项目根目录下的 Claude Code 规则文件）里写过一次，它大部分时候会遵守，但偶尔还是会忘——LLM 的本质就是概率性的，不是确定性的。

还有一类更隐蔽的问题：Claude Code 改了三个文件，每个都改对了，但没跑测试。你 review 完觉得没问题就提交了，结果 CI 挂了——某个改动引入了一个类型错误，本地跑一次 `pytest` 就能发现，但谁也没跑。

每次都在 prompt 里重复一遍"别碰 build 目录"、"改完记得跑测试"、"不要 force push"，第三次你就烦了。

Hooks 解决的就是这个问题：**在 Claude Code 做任何事之前或之后，你的脚本先跑一遍**。规则你定，执行它来。和 prompt 指令不同，Hook 是确定性的——它不会忘，不会偷懒，不会"这次觉得改动太小就跳过了"。

## 一、Hooks 系统全景

理解 Hooks 需要分清两个维度，很多人一上来就把它们搅在一起：

- **"什么时候触发"** —— 事件类型
- **"用什么来处理"** —— 实现类型

### 1.1 最常用的 6 个事件类型

官方定义了 20 多个事件（SessionStart、InstructionsLoaded、SubagentStart/Stop、FileChanged、ConfigChange 等等，输入 `/hooks` 命令可以看全部）。日常开发最常用的是以下 6 个：

| 事件类型 | 触发时机 | 能做什么 | 注意事项 |
|---------|---------|---------|---------|
| **PreToolUse** | 工具调用**前** | 阻止、修改输入、defer | 唯一能阻止操作的时机 |
| **PostToolUse** | 工具调用**后** | 检查结果、触发副作用 | 操作已执行，无法撤销 |
| **Stop** | Claude 每轮响应结束时 | 阻止 Claude 结束当前轮次、触发每轮记忆提取 | **每轮都触发** |
| **SessionEnd** | 会话结束时 | 通知、清理、存档 | 整个会话只触发一次 |
| **TaskCompleted** | 后台任务完成时 | 通知 | Agent Teams 场景专用 |
| **PermissionDenied** | Auto Mode（Claude Code 的自动执行模式）内置分类器拒绝时 | 重试或降级 | 2026 年 4 月新增 |

**Stop 和 SessionEnd 是最容易搞混的一对**。Stop 在 Claude 每轮响应结束后都触发——一个 10 轮对话会触发 10 次 Stop。SessionEnd 只在会话真正结束时触发一次。所以做 Telegram 通知要用 SessionEnd，做记忆提取（每轮都需要存一次上下文）才用 Stop。

### 1.2 四种实现类型

| 类型 | 执行方式 | 适用场景 | 性能 |
|------|---------|---------|------|
| **command** | Shell 脚本 | 本地验证、自动化 | ~10-100ms |
| **http** | POST 到外部 URL | 团队审批服务（**必须配置 `allowedEnvVars`** 才能传 token） | 取决于网络 |
| **prompt** | Haiku 单次 LLM 调用 | 自然语言判断 | ~0.5-1s |
| **agent** | 带 Read/Grep/Glob 的子代理 | 需要检查文件才能决策 | ~5-30s |

大多数场景用 command 就够了。prompt 和 agent 类型有延迟，不适合做守卫——你不会希望每次 Bash 命令前都等一秒。

### 1.3 通信协议：command hook 必须知道的三件事

**第一件：exit code 的含义**

```
exit 0    = 允许操作继续
exit 2    = 阻止操作（blocking）
exit 1/3+ = 非阻止警告（warn but continue）
```

**是 exit 2，不是 exit 1**。这是最常见的错误。很多人写 `exit 1` 以为能拦住操作，结果只是一个警告，Claude 照做不误。

另外，exit 2 在不同事件里行为不同：PreToolUse 中阻止工具调用；PostToolUse 中只能向 Claude 抛错（因为操作已经执行了，你拦不住）；Stop 中可以阻止 Claude 结束当前轮次。

**第二件：JSON 返回结构（以 PreToolUse 为例）**

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "原因说明（展示给用户）",
    "additionalContext": "注入给 Claude 上下文的信息（不展示给用户）"
  }
}
```

如果你只需要简单的允许/阻止，直接 `exit 0` 或 `exit 2` 就行。但如果要注入原因或上下文信息，就必须输出这个 JSON。注意 `hookEventName` 字段不能省。

不同事件的返回结构略有差异：PostToolUse 和 Stop 的顶层是 `decision` + `reason`，`hookSpecificOutput` 嵌套在内（见场景 B 和场景 C 的脚本）。PreToolUse 是唯一支持 `permissionDecision`（allow/deny/ask/defer）的事件。

**第三件：stdin 输入解析**

Hook 的输入是 JSON，通过 stdin 传入。因为 stdin 只能读一次，建议先存到变量：

> **前置依赖**：以下所有 Shell 脚本均使用 `jq` 解析 JSON。如果你的机器还没有安装：`brew install jq`（macOS）或 `sudo apt install jq`（Ubuntu/Debian）。

```bash
INPUT=$(cat -)
TOOL=$(jq -r '.tool_name' <<< "$INPUT")
FILE=$(jq -r '.tool_input.file_path' <<< "$INPUT")
CMD=$(jq -r '.tool_input.command' <<< "$INPUT")
```

### 1.4 三层嵌套配置 + 四个作用域

配置结构是三层嵌套——事件类型、过滤条件、处理器列表：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/absolute/path/to/guard.sh",
            "timeout": 10,
            "statusMessage": "安全检查中..."
          }
        ]
      }
    ]
  }
}
```

**Matcher 必须配置**。不配或留空意味着所有工具都会触发这个 Hook——Edit 触发、Write 触发、Grep 也触发，性能直接拉垮。

Matcher 支持三种模式：
- `"Bash"` —— 精确匹配
- `"Write|Edit"` —— 管道分隔多工具
- `"mcp__memory__.*"` —— 正则匹配某个 MCP 服务器的所有工具（MCP 是 Claude Code 的外部工具扩展协议，`mcp__服务器名__工具名` 是其命名格式）

**四个作用域**（从组织到个人）：

| 位置 | 用途 | 是否提交 Git |
|------|------|-------------|
| Managed policy | 组织级，管理员强制（用户不可覆盖） | 管理员控制 |
| `~/.claude/settings.json` | 全局，所有项目生效 | 否 |
| `.claude/settings.json` | **项目共享，团队规范放这里** | 是 |
| `.claude/settings.local.json` | 项目私有，个人偏好 | 否（gitignore） |

注意：对于 hooks 配置，所有作用域的 hooks 会**叠加执行**（都会跑），而不是高优先级覆盖低优先级。"优先级"主要影响 permissions 等其他配置项。

## 二、三大经典场景实战

### 场景 A：守卫类——拦截危险命令

最直接的需求：阻止 Claude Code 在自动模式下执行破坏性命令。

**脚本**（保存到 `~/.claude/hooks/guard-dangerous-commands.sh`）：

```bash
#!/bin/bash
# 拦截已知危险命令
# 用法：PreToolUse → Bash 工具触发

INPUT=$(cat -)
CMD=$(jq -r '.tool_input.command' <<< "$INPUT")

# 危险命令模式列表
DANGEROUS_PATTERNS=(
  "rm -rf /"
  "rm -rf ~"
  "git push.*--force"
  "git push.*-f "
  "DROP TABLE"
  "DROP DATABASE"
  "kubectl delete"
  "docker system prune -a"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$CMD" | grep -qiE "$pattern"; then
    jq -n --arg reason "命令匹配危险模式: $pattern" '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: $reason
      }
    }'
    exit 2
  fi
done

exit 0
```

别忘了给执行权限：`chmod +x ~/.claude/hooks/guard-dangerous-commands.sh`

**对应的 settings.json 配置**：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/guard-dangerous-commands.sh",
            "timeout": 10,
            "statusMessage": "检查危险命令..."
          }
        ]
      }
    ]
  }
}
```

**踩坑提醒**：正则写太宽会误杀。比如 `rm -rf` 不带路径限定，会把 `rm -rf ./node_modules`（完全安全的清理操作）也拦住。建议先在宽松模式下跑几天，看日志里有没有误报，再逐步收紧。

### 场景 B：测试触发类——改了代码就自动跑测试

改完代码不跑测试，是 AI 辅助编程最常见的坑。用 PostToolUse 可以做到"改 + 测"原子化。

**脚本**（保存到 `~/.claude/hooks/post-edit-test.sh`）：

```bash
#!/bin/bash
# 文件修改后自动运行相关测试
# 用法：PostToolUse → Write|Edit 触发

INPUT=$(cat -)
FILE=$(jq -r '.tool_input.file_path' <<< "$INPUT")

# 只处理源代码文件
case "$FILE" in
  *.py)
    # Python：找对应的测试文件
    # 注意：此处假设了 src/ 和 tests/ 目录分离结构（src/foo.py → tests/test_foo.py）
    # 如果你的测试文件在同级目录或其他位置，请修改这里的路径替换逻辑
    TEST_FILE="${FILE/src\//tests/test_}"
    if [ -f "$TEST_FILE" ]; then
      RESULT=$(python -m pytest "$TEST_FILE" --tb=short 2>&1)
      EXIT_CODE=$?
    else
      RESULT="未找到对应测试文件: $TEST_FILE"
      EXIT_CODE=0
    fi
    ;;
  *.ts|*.tsx)
    # TypeScript：用 vitest 跑相关文件
    TEST_FILE="${FILE%.ts}.test.ts"
    [ ! -f "$TEST_FILE" ] && TEST_FILE="${FILE%.tsx}.test.tsx"
    if [ -f "$TEST_FILE" ]; then
      RESULT=$(npx vitest run "$TEST_FILE" 2>&1)
      EXIT_CODE=$?
    else
      RESULT="未找到对应测试文件"
      EXIT_CODE=0
    fi
    ;;
  *)
    exit 0  # 非源代码文件，跳过
    ;;
esac

# 将测试结果注入 Claude 上下文
jq -n --arg ctx "$RESULT" '{
  hookSpecificOutput: {
    hookEventName: "PostToolUse",
    additionalContext: $ctx
  }
}'

# 测试失败时 exit 2 让 Claude 知道有问题
[ $EXIT_CODE -ne 0 ] && exit 2
exit 0
```

`chmod +x ~/.claude/hooks/post-edit-test.sh`

**配置**：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/post-edit-test.sh",
            "timeout": 120,
            "statusMessage": "运行关联测试..."
          }
        ]
      }
    ]
  }
}
```

这里有两个关键设计决策值得说明：

**为什么测试结果要走 `additionalContext` 而不是直接 echo？** Hook 的 stdout 输出必须是合法的 JSON 结构，Claude 只解析 `hookSpecificOutput` 里的字段。直接 echo 一段测试日志，Claude 不会读到——它只看结构化的 JSON。把测试结果放在 `additionalContext` 里，Claude 会在下一轮对话中看到这段信息，如果测试失败，它通常会自动尝试修复。

**测试文件找不到时为什么 `exit 0` 而不是 `exit 2`？** 不是每个源代码文件都有对应的测试文件——新建的文件、配置文件、工具脚本都可能没有。如果找不到测试就 `exit 2` 报错，Claude 会把"缺少测试文件"当成代码错误来处理，陷入无意义的修复循环。正确做法是静默跳过（`exit 0`），让 Claude 继续正常工作。如果你希望强制所有源文件都有测试覆盖，可以把 `exit 0` 改成 `exit 2` 加一条 `additionalContext` 提示"请为此文件创建测试"。

**踩坑提醒**：默认超时是 600 秒，但如果你的测试套件很慢，可以单独配置 `timeout`。另外，并行任务（比如 Claude 连续修改多个文件）会同时触发多个 Hook 实例。如果测试涉及数据库或端口占用，可以用 `flock` 加文件锁避免竞态：在脚本开头加 `exec 200>/tmp/hook-test.lock; flock -n 200 || exit 0`，抢不到锁的实例直接跳过。

### 场景 C：状态通知类——会话结束发 Telegram

长任务跑完了你可能已经切出去干别的了。用 SessionEnd Hook 在会话结束时发一条 Telegram 消息 + 播放提示音。

**为什么是 SessionEnd 而不是 Stop**：Stop 每轮响应结束都触发。一个 10 轮对话发 10 条 Telegram 消息，你的手机会炸。

**脚本**（保存到 `~/.claude/hooks/session-end-notify.sh`）：

```bash
#!/bin/bash
# 会话结束后发 Telegram 通知 + 提示音
# 用法：SessionEnd 触发

INPUT=$(cat -)
SESSION_ID=$(jq -r '.session_id' <<< "$INPUT")

# Telegram 通知（token 和 chat_id 通过环境变量传入）
if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
  curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
    -d "chat_id=${TELEGRAM_CHAT_ID}" \
    -d "text=Claude Code 会话完成: ${SESSION_ID:0:8}..." \
    > /dev/null 2>&1
fi

# macOS 提示音
if [ "$(uname)" = "Darwin" ]; then
  afplay /System/Library/Sounds/Glass.aiff &
fi

exit 0
```

`chmod +x ~/.claude/hooks/session-end-notify.sh`

**配置**：

```json
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/session-end-notify.sh",
            "timeout": 15
          }
        ]
      }
    ]
  }
}
```

Telegram Bot Token 和 Chat ID 通过环境变量传入（在你的 shell profile 里 `export TELEGRAM_BOT_TOKEN=xxx`），不要硬编码在脚本里。

**多会话并行时的通知区分**：如果你同时开了三个终端跑不同任务，三个会话结束时都会触发通知。Session ID 前 8 位足以区分不同任务。更进一步的做法是在通知消息里加上当前工作目录（从 stdin JSON 的 `cwd` 字段读取），这样你一看消息就知道是哪个项目跑完了。

**为什么 `afplay` 要加 `&` 放后台？** 提示音播放大约需要 1-2 秒，如果不放后台，Hook 会等提示音播放完才退出。虽然 SessionEnd 没有后续操作受影响，但养成习惯——非关键的副作用（声音、通知）都放后台，避免拖慢 Hook 执行。

**Stop Hook 的正确用途**：每轮记忆提取。MindStudio 社区有一个方案：用 Stop Hook 在每轮响应后调用 Claude API（Haiku 模型，成本低于 $0.01/次），从会话记录中提取结构化知识点（代码模式、错误解法、架构决策），写入 Obsidian 知识库。下次开启新会话时，Claude 通过 CLAUDE.md 引用这个知识库，就能"记住"之前学到的东西。这种场景每轮都需要执行，所以用 Stop 而不是 SessionEnd。

## 三、April 2026 新特性

v2.1.89 带来了两个重要的 Hook 新能力。

### 3.1 PermissionDenied Hook：分类器拒绝后的自动重试

Auto Mode 有一个内置分类器，会拦截它认为"超出权限"的操作。以前被拦了就只能等用户手动授权。现在你可以用 PermissionDenied Hook 实现自动降级。

一个实际例子：Claude 想执行 `git push origin main`，分类器拦截了（受保护分支）。Hook 检查被拒绝的原因，发现是分支名问题，返回 `retry: true` 加一条提示"请 push 到 feature 分支"。Claude 收到后重试 `git push origin feature/my-feature`，成功。

**脚本**（保存到 `~/.claude/hooks/permission-retry.sh`）：

```bash
#!/bin/bash
# PermissionDenied 自动降级重试
INPUT=$(cat -)
TOOL=$(jq -r '.tool_name' <<< "$INPUT")
REASON=$(jq -r '.denial_reason // "unknown"' <<< "$INPUT")

# 只对 Bash 工具的权限拒绝做处理
if [ "$TOOL" = "Bash" ]; then
  jq -n --arg reason "操作被权限分类器拒绝（原因: $REASON），请尝试更安全的替代方式" '{
    hookSpecificOutput: {
      hookEventName: "PermissionDenied",
      retry: true,
      permissionDecisionReason: $reason
    }
  }'
  exit 0
fi

exit 0
```

`chmod +x ~/.claude/hooks/permission-retry.sh`

**配置**：

```json
{
  "hooks": {
    "PermissionDenied": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/permission-retry.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

**什么时候该 retry，什么时候让用户确认**：如果被拒操作有明确的安全替代方案（push main → push feature），可以 retry。如果是删除生产数据这种不可逆操作，不要自动 retry——让用户自己决定。

### 3.2 PreToolUse Defer：暂停等待外部审批

这是 Hooks 系统里最强的流程控制能力。Hook 不立刻返回 allow/deny，而是"暂停"整个 Claude Code 进程，等外部系统（审批流、CI 门禁、人工确认）回信号后再继续。

**完整工作流程**：

```
① Claude Code 执行到高风险操作（如数据库写入）
② PreToolUse Hook 返回 permissionDecision: "defer"
③ Claude Code 进程暂停，输出 stop_reason: "tool_deferred"
④ 外部系统读取 deferred_tool_use（包含操作详情：id、name、input）
⑤ 外部系统发送审批请求（企业微信/Slack/钉钉）
⑥ 审批人确认
⑦ 调用 claude -p --resume <session-id> 恢复执行
⑧ 同一个 Hook 再次触发 → 这次返回 allow 或 deny
```

第 ⑦ 步的 `claude -p --resume <session-id>` 是关键——defer 只在 headless 模式（`-p` 参数）下有效。如果你在普通交互终端（直接输入 `claude` 启动的模式）里配置了 defer，效果等同于 `ask`——Claude Code 会直接在终端弹出权限确认提示，不会暂停等待外部审批。defer 的设计场景是 CI/CD 流水线或自动化脚本，不是日常对话。

**脚本示例**（保存到 `~/.claude/hooks/defer-approval.sh`）：

```bash
#!/bin/bash
# PreToolUse defer：数据库写操作需要外部审批
# 用法：PreToolUse → Bash 工具触发（headless 模式下）

INPUT=$(cat -)
CMD=$(jq -r '.tool_input.command' <<< "$INPUT")
SESSION_ID=$(jq -r '.session_id' <<< "$INPUT")

# 检查是否有审批标记文件（第二次触发时存在 = 已审批）
APPROVAL_FILE="/tmp/claude-approval-${SESSION_ID}"
if [ -f "$APPROVAL_FILE" ]; then
  rm -f "$APPROVAL_FILE"
  exit 0  # 已审批，放行
fi

# 检查是否包含数据库写操作
if echo "$CMD" | grep -qiE "(INSERT INTO|UPDATE .* SET|DELETE FROM|DROP |ALTER TABLE)"; then
  # 发送审批请求到企业微信 webhook
  if [ -n "$WECHAT_WEBHOOK_URL" ]; then
    curl -s "$WECHAT_WEBHOOK_URL" \
      -H 'Content-Type: application/json' \
      -d "$(jq -n --arg cmd "$CMD" --arg sid_short "${SESSION_ID:0:8}" --arg sid_full "$SESSION_ID" --arg af "$APPROVAL_FILE" '{
        msgtype: "text",
        text: {content: "Claude Code 审批请求\n会话: \($sid_short)...\n操作: \($cmd)\n审批通过请执行:\ntouch \($af) && claude -p --resume \($sid_full)"}
      }')" > /dev/null 2>&1
  fi

  # 注意：审批标记文件由审批人手动创建，不在这里 touch
  # 审批人确认后执行: touch /tmp/claude-approval-<session-id> && claude -p --resume <session-id>

  # 返回 defer
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "defer",
      permissionDecisionReason: "数据库写操作需要审批，已发送审批请求"
    }
  }'
  exit 0
fi

exit 0
```

`chmod +x ~/.claude/hooks/defer-approval.sh`

**配置**：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/defer-approval.sh",
            "timeout": 15,
            "statusMessage": "检查是否需要审批..."
          }
        ]
      }
    ]
  }
}
```

审批人确认后执行两步操作：先 `touch /tmp/claude-approval-<session-id>` 创建审批标记，再 `claude -p --resume <session-id>` 恢复执行。同一个 Hook 再次触发——这次检测到审批标记文件，直接 `exit 0` 放行。标记文件必须由审批人（或审批系统的 callback）创建，脚本本身不能自己 touch，否则 defer 就变成了"发通知就自动通过"。

**典型场景**：

- **生产环境操作审批**：数据库写操作 → 发企业微信审批 → 负责人点通过 → 继续执行
- **CI 门禁**：每次部署前等 CI 检查完成
- **人在回路（HITL）**：高风险操作强制等人确认

这个能力让 Claude Code 从"本地开发工具"升级成了可以嵌入企业流程的自动化节点。

## 四、我的实际 Hook 配置

给一个开箱即用的起点。以下是我日常在用的 4 个 Hook 的完整配置。实际用的 PostToolUse 是 lint（比测试轻量，适合每次编辑后触发），场景 B 里的测试触发是完整版，读者可以按需选择。

**目录结构**：

```
~/.claude/
├── settings.json
└── hooks/
    ├── guard-dangerous-commands.sh   # 危险命令拦截
    ├── post-edit-test.sh             # 修改后自动运行测试（即场景 B 的脚本）
    ├── session-end-notify.sh         # 会话结束通知
    └── permission-retry.sh           # 权限拒绝重试
```

**完整 `~/.claude/settings.json`**：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/guard-dangerous-commands.sh",
            "timeout": 10,
            "statusMessage": "检查危险命令..."
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/post-edit-test.sh",
            "timeout": 30,
            "statusMessage": "运行测试..."
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/session-end-notify.sh",
            "timeout": 15
          }
        ]
      }
    ],
    "PermissionDenied": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/permission-retry.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

**踩坑汇总**：

1. **脚本必须 `chmod +x`**，否则 Hook 静默失败，不报错，你以为配好了其实没生效
2. **settings.json 配置中路径用 `$HOME` 或绝对路径**，不要用 `~`（JSON 配置由 Claude Code 进程解析，部分环境不展开波浪号，导致找不到脚本却不报错。终端里手动执行 `chmod +x` 等命令用 `~/` 没问题）
3. **PostToolUse 并行竞态**：多个文件同时修改会同时触发多个 Hook 实例，如果涉及数据库或端口，加锁文件（`flock`）避免冲突
4. **prompt/agent 类型有延迟**：不适合 PreToolUse 守卫场景，守卫就用 command，快且确定

## 五、Hooks 和微软 Governance Toolkit

如果你在企业里推 AI 编程工具，可能听说过微软刚开源的 Agent Governance Toolkit（AGT）。它和 Hooks 做的事有重叠，但定位完全不同。

| 维度 | Claude Code 原生 Hooks | 微软 AGT |
|------|----------------------|----------|
| 适用范围 | 仅 Claude Code | 20+ Agent 框架通用 |
| 配置方式 | JSON + Shell 脚本 | Python/TS/.NET/Rust/Go SDK |
| 合规支持 | 自定义规则 | OWASP Agentic Top 10 内置覆盖 |
| 审计日志 | 需自建 | 内置结构化日志 |
| 性能开销 | ~10-100ms（脚本启动） | < 0.1ms（内核级） |
| 适合场景 | 个人 / 小团队 | 多框架、企业合规 |

AGT 在红队测试中做到了 0% 策略违反率，而纯 prompt 层面的安全指令（"请遵守规则"）有 26.67% 的违反率。这个数据说明一件事：**语言指令无法替代程序性执行层**——这也是 Hooks 存在的根本原因。

**什么时候需要 AGT**：如果你的团队同时在用 Claude Code、LangChain、AutoGen 等多种 Agent 框架，需要一套统一的策略覆盖所有工具；或者要满足 SOC2/GDPR 等合规审计要求，需要内置的结构化审计日志和 OWASP Agentic Top 10 的书面合规证明——这些是 Hooks 做不到的，得上 AGT。

**什么时候 Hooks 够用**：个人开发者、小团队（5 人以下），只用 Claude Code，规则简单明确（拦截危险命令、自动跑测试、通知），不需要跨框架策略——Shell 脚本 + JSON 配置完全够用，不用引入额外的 SDK 依赖。

核心结论：两者不冲突。Hooks 是 Claude Code 的"最后一道本地墙"，AGT 是跨工具的"统一策略层"。企业场景两层都要，个人场景 Hooks 够用。

## 结尾：Hooks 是你和 AI 的协作契约

不配 Hooks 的 Claude Code 是一个黑盒 Agent——它能做什么、不能做什么，全靠它自己判断。配了 Hooks 的 Claude Code 是一个可信任的开发伙伴——边界你定，执行它来。区别不在能力，在于你是否定义了规则。

**现在就能做的三件事**：

- **5 分钟**：把本文场景 A 的守卫脚本复制过去，加上 `chmod +x`，拦住 `rm -rf` 和 `--force push`
- **1 小时**：配置场景 B 的 PostToolUse 测试触发，让"改代码"变成"改 + 测"原子操作
- **生产级**：接入 defer + 外部审批，把关键操作纳入团队工作流

---

**参考来源**

[Claude Code Hooks 官方文档](https://docs.anthropic.com/en/docs/claude-code/hooks)

[微软 Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)

[MindStudio: Self-Evolving Claude Code Memory with Obsidian Hooks](https://www.mindstudio.ai/blog/self-evolving-claude-code-memory-obsidian-hooks)

---

## 相关阅读

- [Claude Code 安全三道防线：从权限模式到 Hook 兜底](/posts/claude-code-safety-three-defenses/) — 讲权限控制原则，本文是具体 Hook 实现层的完整进阶
- [同样的话跟 Claude Code 说了八遍，是时候让它自己记住了](/posts/claude-code-memory-rules/) — CLAUDE.md 规则配置，是 Hooks 的"声明性"补充手段
- [Claude Code 安装后必做的 9 项设置](/posts/claude-code-essential-settings/) — 环境初始化，Hook 配置在此基础上添加
