11 Hooks 系统
本节目标
-
理解 Hooks 与 CLAUDE.md 的本质区别:强制执行 vs 行为引导
-
掌握 9 种生命周期事件及其触发时机
-
区分 Command Hooks 和 Prompt Hooks 两种实现模式
-
能编写实际的 Hook 脚本:危险命令拦截、自动格式化、提交前检查
-
理解退出码机制(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:
-
一个 Bash 命令安全拦截器(至少拦截 5 种危险模式)
-
一个文件操作检查器(禁止修改特定目录)
-
一个敏感信息检测器(阻止提交包含 API key 的代码)
作业 2:实现完整的 PostToolUse 自动化工序
为以下操作编写 PostToolUse Hook:
-
TypeScript 文件修改后自动运行 tsc 类型检查
-
CSS 文件修改后自动运行 stylelint
-
任何文件修改后更新 TODO 注释的统计
作业 3:Hook 调试实战
-
故意创建一个会触发 exit 2 的危险命令
-
使用
claude --debug观察 Hook 的拦截过程 -
查看 stderr 输出是否正确显示
-
修复 Hook 中的任何问题,直到拦截行为完全符合预期
总结
Hooks 系统是 Claude Code 从"智能助手"跃升到"可控工具"的关键桥梁:
-
9 个生命周期事件覆盖了从会话启动到提交代码的完整链路
-
退出码机制(0=放行,2=阻止)给了你二进制级别的控制力
-
Command vs Prompt 两种模式让你在"精确性"和"编写成本"之间做出选择
-
与 CLAUDE.md 的分工:CLAUDE.md 引导 AI 的"思考",Hooks 控制 AI 的"手脚"
Hooks 配置得当,你会拥有一个既聪明又守规矩的 AI 开发伙伴。配置不当,它要么束缚 AI 的创造力,要么形同虚设。关键在于:只在真正需要确定性的场景使用 Hook,其他场景交给 CLAUDE.md 的自然语言引导。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)