本节目标

  1. 理解 Hooks 与 CLAUDE.md 的本质区别:强制执行 vs 行为引导

  2. 掌握 9 种生命周期事件及其触发时机

  3. 区分 Command Hooks 和 Prompt Hooks 两种实现模式

  4. 能编写实际的 Hook 脚本:危险命令拦截、自动格式化、提交前检查

  5. 理解退出码机制(0=放行, 2=阻止)及 Hook 调试方法


核心知识点

Hooks vs CLAUDE.md:强制 vs 建议

这是理解 Hooks 系统的第一原则:

维度 CLAUDE.md Hooks
作用方式 自然语言指令,AI 自行理解执行 脚本代码,系统强制执行
可靠性 AI 可能忽略或遗忘(概率性) 100% 执行(确定性)
适用场景 编码风格、技术偏好、命名约定 安全检查、格式强校验、操作拦截
编写语言 Markdown 自然语言 Bash/Python/Node.js 可执行脚本
更新方式 编辑 .md 文件 编辑 settings.json + 部署脚本
典型例子 "使用 camelCase 命名" "禁止执行 rm -rf /"

一句话总结:CLAUDE.md 引导 AI 怎么想,Hooks 控制 AI 能做什么。

9 种生命周期事件

Claude Code v2.1.x 提供了 9 个 Hook 挂载点,覆盖 AI 操作的完整生命周期:

SessionStart ──→ 工具调用循环 ──→ Stop
                    │
                    ├── PreToolUse
                    ├── PostToolUse
                    ├── PreCompact
                    └── Notification
                    
独立事件:
UserPromptSubmit ── 用户每次提交提示词
PreCommit ── git commit 之前
PostCommit ── git commit 之后

1. PreToolUse (工具调用前)

最高频使用的 Hook。在 Claude Code 执行任何工具(Write、Edit、Bash 等)之前触发。可以拦截危险操作。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "node ~/.claude/hooks/block-dangerous-cmd.mjs",
        "description": "拦截危险的 Bash 命令"
      }
    ]
  }
}

2. PostToolUse (工具调用后)

工具执行完成后触发,常用于自动格式化:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "npx prettier --write \"$FILE_PATH\"",
        "description": "自动格式化刚写入的文件"
      }
    ]
  }
}

3. SessionStart (会话启动)

Claude Code 启动时触发,用于环境检查:

{
  "hooks": {
    "SessionStart": [
      {
        "command": "node -e \"const v=process.version.slice(1).split('.')[0]; if(+v<20) { console.error('需要 Node.js >= 20'); process.exit(2) }\"",
        "description": "检查 Node.js 版本"
      }
    ]
  }
}

4. Stop (会话结束)

Claude Code 退出前触发,用于最终验证或清理:

{
  "hooks": {
    "Stop": [
      {
        "command": "npm run type-check && npm run lint",
        "description": "退出前运行类型检查和 lint"
      }
    ]
  }
}

5. PreCompact (上下文压缩前)

当对话历史过长,Claude Code 需要压缩上下文时触发。可用于保存重要信息:

{
  "hooks": {
    "PreCompact": [
      {
        "command": "node .claude/hooks/save-context-summary.mjs",
        "description": "压缩前保存对话摘要"
      }
    ]
  }
}

6. Notification (通知事件)

用于外部状态更新(如桌面通知、IM 推送):

{
  "hooks": {
    "Notification": [
      {
        "command": "node .claude/hooks/send-notify.mjs",
        "description": "发送桌面通知"
      }
    ]
  }
}

7. UserPromptSubmit (用户提交提示词前)

每次用户按回车提交消息前触发。可用于输入预处理或审查:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "command": "node .claude/hooks/preprocess-prompt.mjs",
        "description": "预处理用户提示词"
      }
    ]
  }
}

8. PreCommit (提交前)

Claude Code 执行 git commit 前触发:

{
  "hooks": {
    "PreCommit": [
      {
        "command": "npm test",
        "description": "提交前运行测试"
      }
    ]
  }
}

9. PostCommit (提交后)

git commit 完成后触发,可用于推送或通知:

{
  "hooks": {
    "PostCommit": [
      {
        "command": "git push origin HEAD",
        "description": "提交后自动推送"
      }
    ]
  }
}

Command Hooks vs Prompt Hooks

两种 Hook 实现模式:

Command Hooks (命令式)

最常用,直接执行可执行脚本:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "prettier --write \"$CLAUDE_TOOL_INPUT_FILE\"",
        "description": "代码格式化"
      }
    ]
  }
}

环境变量:

  • $CLAUDE_TOOL_NAME: 工具名称 (Write, Edit, Bash 等)

  • $CLAUDE_TOOL_INPUT_FILE: 操作的文件路径

  • $CLAUDE_TOOL_INPUT: 工具输入的 JSON 字符串(通过 stdin 传入)

  • $CLAUDE_SESSION_ID: 当前会话 ID

Prompt Hooks (提示词注入式)

将内容注入到 AI 的上下文中,用于在不打断工作流的情况下提供提醒:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "type": "prompt",
        "prompt": "在执行此命令前,请确认它不会修改 /etc 目录下的任何文件。如果是,请先向用户确认。",
        "description": "Bash 命令安全提醒"
      }
    ]
  }
}

两种模式的对比:

维度 Command Hook Prompt Hook
能否阻止操作 能 (exit 2) 不能直接阻止(只能提醒)
精确性 确定性逻辑判断 依赖 AI 理解提醒内容
编写成本 需要写脚本 写一段提示词即可
适用场景 明确规则(禁止、格式化) 提醒性规则(注意、考虑)

退出码机制

Hook 脚本的退出码决定了操作是否被允许:

退出码 含义 行为
0 通过/允许 操作正常执行
1 非阻塞错误 记录为 warning,操作继续
2 阻止 操作被拦截,向用户展示错误信息
其他 未知状态 视为错误,但操作可能继续

stdout 的消息会作为 info 返回给 AI,stderr 的消息会作为 error 返回。

Hook 调试方法

调试 Hook 时使用 Claude Code 的内置诊断:

# 启动调试模式,显示所有 Hook 执行过程
claude --debug
​
# 查看最近的 Hook 执行日志
claude --doctor hooks
​
# 临时禁用所有 Hook
claude --bare
​
# 检查配置文件语法
claude --doctor config

在 Hook 脚本中加调试输出:

// .claude/hooks/block-dangerous.mjs
const input = JSON.parse(process.stdin.read() || '{}');
​
// debug 输出到 stderr 会出现在 --debug 日志中
console.error('[DEBUG] 输入参数:', JSON.stringify(input, null, 2));
​
const dangerous = ['rm -rf /', 'git push --force origin main', ':(){ :|:& };:'];
const command = input.tool_input?.command || '';
​
if (dangerous.some(d => command.includes(d))) {
  console.error('[Hook] 阻止危险命令:', command);
  process.exit(2);
}
process.exit(0);

实操步骤

步骤 1:创建危险命令拦截 Hook

mkdir -p ~/.claude/hooks
// ~/.claude/hooks/block-dangerous.mjs
import { readFileSync } from 'fs';
​
// 从 stdin 读取工具输入
const chunks = [];
process.stdin.on('data', chunk => chunks.push(chunk));
process.stdin.on('end', () => {
  const input = JSON.parse(Buffer.concat(chunks).toString() || '{}');
  const command = input.tool_input?.command || '';
​
  const blocked = [
    { pattern: /rm\s+-rf\s+\//, reason: 'rm -rf / 是毁灭性操作' },
    { pattern: /git\s+push\s+--force.*origin\s+(main|master)/, reason: '禁止强制推送到主分支' },
    { pattern: />\s*\/etc\//, reason: '禁止覆盖 /etc 系统配置文件' },
    { pattern: /curl.*\|\s*(ba)?sh/, reason: '禁止管道执行远程脚本' },
  ];
​
  for (const block of blocked) {
    if (block.pattern.test(command)) {
      console.error(`[BLOCKED] ${block.reason}`);
      console.error(`命令: ${command}`);
      process.exit(2);
    }
  }
  process.exit(0);
});

配置到 settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "node ~/.claude/hooks/block-dangerous.mjs",
        "description": "拦截危险 Bash 命令"
      }
    ]
  }
}

步骤 2:创建自动格式化 Hook

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "prettier --write \"$CLAUDE_TOOL_INPUT_FILE\" 2>/dev/null || true",
        "description": "自动 Prettier 格式化"
      }
    ]
  }
}

步骤 3:创建提交前检查 Hook

# .claude/hooks/pre-commit-check.sh
#!/bin/bash
set -e
​
echo "=== 提交前检查 ==="
​
# 1. 类型检查
echo "[1/3] TypeScript 类型检查..."
npx tsc --noEmit || { echo "类型检查失败,请修复后重试"; exit 2; }
​
# 2. Lint 检查
echo "[2/3] ESLint 检查..."
npx eslint . --max-warnings 0 || { echo "ESLint 检查未通过"; exit 2; }
​
# 3. 测试
echo "[3/3] 运行测试..."
npm test || { echo "测试未通过"; exit 2; }
​
echo "=== 所有检查通过 ==="
{
  "hooks": {
    "PreCommit": [
      {
        "command": "bash .claude/hooks/pre-commit-check.sh",
        "description": "提交前运行类型检查、Lint、测试"
      }
    ]
  }
}

避坑指南

坑 1:Hook 脚本没有执行权限

Window/macOS/Linux 的文件权限设置不同。如果 Hook 脚本无法执行:

# 确保脚本可执行
chmod +x .claude/hooks/*.sh
chmod +x ~/.claude/hooks/*.mjs
​
# 或者使用解释器调用
# 在 command 中使用 "bash script.sh" 而不是 "./script.sh"

坑 2:Command Hook 的 stdin 读取超时

Node.js 脚本从 stdin 读取输入时,如果等待过久会导致 Hook 超时。必须在读取完输入后立即处理:

// 错误:可能永远等不到 end 事件
process.stdin.on('data', chunk => data += chunk);
// 缺少 end 事件的监听
​
// 正确:设置超时和完整的流处理
const timeout = setTimeout(() => {
  console.error('[Hook] stdin 读取超时,默认放行');
  process.exit(0);
}, 5000);
​
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => {
  clearTimeout(timeout);
  // 处理 data
  process.exit(0);
});

坑 3:PostToolUse 在出错时也会触发

即使工具执行失败,PostToolUse Hook 仍然会触发。需要在脚本中判断操作是否成功:

const input = JSON.parse(process.stdin.read() || '{}');
if (input.tool_output?.error) {
  // 工具执行失败,跳过格式化
  process.exit(0);
}
// 正常执行格式化

坑 4:PreToolUse 中 exit 2 的提示信息被吞

当 exit 2 阻止操作时,stderr 的信息会显示给用户,但 stdout 的信息可能被丢弃。确保关键错误信息写入 stderr:

# 错误:用户看不到这条消息
echo "操作被阻止"  # stdout,可能被丢弃
exit 2
​
# 正确:写入 stderr
echo "操作被阻止: 检测到危险命令" >&2
exit 2

课后作业

作业 1:设计你的安全 Hook 集合

针对你的工作场景,设计至少 3 个 PreToolUse Hook:

  1. 一个 Bash 命令安全拦截器(至少拦截 5 种危险模式)

  2. 一个文件操作检查器(禁止修改特定目录)

  3. 一个敏感信息检测器(阻止提交包含 API key 的代码)

作业 2:实现完整的 PostToolUse 自动化工序

为以下操作编写 PostToolUse Hook:

  1. TypeScript 文件修改后自动运行 tsc 类型检查

  2. CSS 文件修改后自动运行 stylelint

  3. 任何文件修改后更新 TODO 注释的统计

作业 3:Hook 调试实战

  1. 故意创建一个会触发 exit 2 的危险命令

  2. 使用 claude --debug 观察 Hook 的拦截过程

  3. 查看 stderr 输出是否正确显示

  4. 修复 Hook 中的任何问题,直到拦截行为完全符合预期


总结

Hooks 系统是 Claude Code 从"智能助手"跃升到"可控工具"的关键桥梁:

  • 9 个生命周期事件覆盖了从会话启动到提交代码的完整链路

  • 退出码机制(0=放行,2=阻止)给了你二进制级别的控制力

  • Command vs Prompt 两种模式让你在"精确性"和"编写成本"之间做出选择

  • 与 CLAUDE.md 的分工:CLAUDE.md 引导 AI 的"思考",Hooks 控制 AI 的"手脚"

Hooks 配置得当,你会拥有一个既聪明又守规矩的 AI 开发伙伴。配置不当,它要么束缚 AI 的创造力,要么形同虚设。关键在于:只在真正需要确定性的场景使用 Hook,其他场景交给 CLAUDE.md 的自然语言引导。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐