Hooks 上篇:五种事件 + 实用模板 —— 让 AI 自动执行你的脚本
Skills 让 AI 学会了你的工作流,MCP 给 AI 装上了手脚。但还有一个问题没解决:你希望某些事在特定时机自动发生——AI 编辑文件后自动格式化、提交前自动跑测试、启动时自动加载环境变量。这些就是 Hooks 的领域。
Hook 是什么:给 Claude Code 装上触发器
一句话理解
Hook = "当 X 发生时,自动执行 Y 脚本" X = 五种事件之一(PreToolUse、PostToolUse、SessionStart...) Y = 你的 Shell 脚本(任何可执行文件) Claude Code 在关键节点自动触发你的脚本, 脚本的 exit code 决定后续行为(允许/拒绝/跳过)。Hook 和 Skill、MCP 的区别
Skill: 触发方式:你说的话匹配到 Skill 的 trigger 做什么:把一套提示词注入 AI 上下文 谁决定的:AI 判断是否触发 MCP Tool: 触发方式:AI 判断需要调用某个 Tool 做什么:执行一个外部操作 谁决定的:AI 判断是否调用 Hook: 触发方式:Claude Code 的内部事件自动触发 做什么:执行你的脚本,影响 Claude Code 的行为 谁决定的:你在配置中写死了触发条件,AI 无法跳过关键差异:Hook 不由 AI 控制。你定义的事件发生时,Hook 必然触发,AI 无法绕过。这就是 Hooks 适合做"安全检查"的原因——你不能指望 AI 自己审查自己。
五种事件逐详解
1. PreToolUse —— 工具调用前拦截
触发时机:AI 即将执行某个 Tool(Edit、Write、Bash 等)之前 参数: - tool_name: 即将被调用的 Tool 名称 - tool_input: 即将传给 Tool 的参数 - session_id: 当前会话 ID Hook 脚本的 exit code 含义: 0 → 允许执行(放行) 1 → 拒绝执行(阻止 Tool 调用) 2 → 跳过本次 Hook 的结果(但允许执行)——较少用 适用场景: - 拦截危险命令(rm -rf、git push --force) - 编辑前自动检查文件是否在白名单中 - 特定 Tool 调用前注入额外逻辑示例:拦截危险命令
#!/bin/bash# hooks/pre-tool-use.sh# PreToolUse Hook:拦截危险操作TOOL_NAME="$1"TOOL_INPUT="$2"# 只拦截 Bash 命令if["$TOOL_NAME"!="Bash"];thenexit0# 允许执行fi# 黑名单:匹配到就拒绝DANGEROUS_PATTERNS=("rm -rf /""git push --force""git push -f""DROP TABLE""DROP DATABASE""> /dev/sda")forpatternin"${DANGEROUS_PATTERNS[@]}";doifecho"$TOOL_INPUT"|grep-qi"$pattern";thenecho"🛑 Hook 拦截:检测到危险操作 '$pattern'。已阻止执行。"echo" 如果你确定要执行,请手动运行此命令。"exit1# 拒绝执行fidone# 高风险操作但可接受:发出警告RISKY_PATTERNS=("rm -rf""sudo ""chmod 777")forpatternin"${RISKY_PATTERNS[@]}";doifecho"$TOOL_INPUT"|grep-qi"$pattern";thenecho"⚠️ 警告:检测到高风险操作 '$pattern'。请确认命令正确。"fidoneexit0# 允许执行2. PostToolUse —— 工具调用后处理
触发时机:AI 执行完某个 Tool 之后 参数: - tool_name: 刚执行完的 Tool 名称 - tool_input: Tool 的参数 - tool_output: Tool 的输出结果 - exit_code: Tool 的执行结果(0=成功) 适用场景: - 编辑代码后自动格式化 - 修改文件后自动运行相关测试 - 部署后自动做健康检查 - Git 提交后自动推送到备份仓库示例:编辑后自动格式化 + 测试提醒
#!/bin/bash# hooks/post-tool-use.sh# PostToolUse Hook:编辑后自动格式化 + 测试提醒TOOL_NAME="$1"TOOL_INPUT="$2"TOOL_EXIT_CODE="$4"# 只在 Edit 或 Write 成功后触发if["$TOOL_NAME"!="Edit"]&&["$TOOL_NAME"!="Write"];thenexit0fiif["$TOOL_EXIT_CODE"!="0"];thenexit0# 编辑失败了,不处理fi# 从 Tool input 中提取被编辑的文件路径FILE_PATH=$(echo"$TOOL_INPUT"|grep-o'"file_path": "[^"]*"'|cut-d'"'-f4)if[-z"$FILE_PATH"];thenexit0fiecho"🔧 Hook: 检测到文件修改:$FILE_PATH"# Python 文件 → 自动格式化ifecho"$FILE_PATH"|grep-q'\.py$';thenifcommand-vblack&>/dev/null;thenblack"$FILE_PATH"2>/dev/nullecho" ✅ Black 格式化完成"fifi# TypeScript/JavaScript 文件 → 自动格式化ifecho"$FILE_PATH"|grep-q'\.\(ts\|tsx\|js\|jsx\)$';thenifcommand-vprettier&>/dev/null;thenprettier--write"$FILE_PATH"2>/dev/nullecho" ✅ Prettier 格式化完成"fifi# 如果文件在 src/ 或 lib/ 目录下,提醒运行测试ifecho"$FILE_PATH"|grep-q'src/\|lib/';thenecho" 💡 提示:源文件已修改,建议运行测试: npm test 或 pytest"fiexit03. SessionStart —— 会话启动初始化
触发时机:Claude Code 每次启动新会话时 参数: - session_id: 新会话的 ID - project_dir: 当前项目根目录 适用场景: - 自动加载 .env 环境变量 - 检查项目依赖是否已安装 - 展示项目状态(Git 分支、未提交文件、上次部署时间) - 预热 MCP Server 连接示例:启动时加载环境变量 + 项目状态摘要
#!/bin/bash# hooks/session-start.sh# SessionStart Hook:加载环境变量 + 展示项目状态PROJECT_DIR="$2"echo"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"echo"🚀 Claude Code 会话已启动"echo"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"# 1. 加载 .env 文件(如果存在)if[-f"$PROJECT_DIR/.env"];then# 安全地加载环境变量(不覆盖已存在的变量)set-asource"$PROJECT_DIR/.env"set+aecho"✅ 已加载环境变量 (.env)"fi# 2. 展示 Git 状态if[-d"$PROJECT_DIR/.git"];thenBRANCH=$(git-C"$PROJECT_DIR"branch --show-current)UNTRACKED=$(git-C"$PROJECT_DIR"status--short|wc-l|tr-d' ')echo"📂 Git 分支:$BRANCH| 未提交文件:$UNTRACKED"fi# 3. 检查 Python 依赖if[-f"$PROJECT_DIR/requirements.txt"];thenif!python-c"import fastapi"2>/dev/null;thenecho"⚠️ Python 依赖未安装,运行: pip install -r requirements.txt"fifi# 4. 检查 Node 依赖if[-f"$PROJECT_DIR/package.json"];thenif[!-d"$PROJECT_DIR/node_modules"];thenecho"⚠️ Node 依赖未安装,运行: npm install"fifiecho""exit04. PreCompaction —— 上下文压缩前保存
触发时机:Claude Code 即将压缩上下文(清理旧信息以释放 Token)之前 参数: - session_id: 当前会话 ID - context_size: 当前上下文大小(Token 数) - project_dir: 项目目录 适用场景: - 在上下文被清理前,自动保存重要信息到 Memory - 归档本次会话的关键决策和发现 - 保存当前任务的进度,防止压缩后"失忆"示例:压缩前自动归档关键信息
#!/bin/bash# hooks/pre-compaction.sh# PreCompaction Hook:压缩前归档关键决策SESSION_ID="$1"CONTEXT_SIZE="$2"PROJECT_DIR="$3"echo"📦 Hook: 上下文压缩前自动归档..."# 从 Claude Code 的会话日志中提取关键信息# (Claude Code 会在调用此 Hook 时传入最近的决策摘要)DECISIONS_FILE="$PROJECT_DIR/.claude/memory/decisions-$(date+%Y-%m-%d).md"# 如果上下文已经很大(超过 150K Token),额外提醒if["$CONTEXT_SIZE"-gt150000];thenecho"⚠️ 上下文已达${CONTEXT_SIZE}Token,建议 /compact 或 /clear"fiecho"✅ 关键信息已归档。压缩后 AI 不会丢失之前的重要决策。"exit05. Notification —— 事件通知推送
触发时机:Claude Code 中的特定通知事件发生时 参数: - event_type: 通知类型(task_complete, error, permission_denied 等) - message: 通知内容 - session_id: 会话 ID 适用场景: - 长时间任务完成后发送桌面通知 - 错误发生时发送 Slack 消息 - AI 被拒绝执行某个操作时记录日志 - 集成外部监控系统示例:长时间任务完成时发送通知
#!/bin/bash# hooks/notification.sh# Notification Hook:任务完成/出错时通知你EVENT_TYPE="$1"MESSAGE="$2"case"$EVENT_TYPE"intask_complete)# macOS 桌面通知osascript-e"display notification\"$MESSAGE\"with title\"Claude Code 任务完成\""# 也可以用 terminal-notifier:# terminal-notifier -title "Claude Code" -message "$MESSAGE";;error)echo"🔴 错误:$MESSAGE">&2# 发送到 Slack(如果你配置了 Slack webhook)# curl -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"Claude Code Error: $MESSAGE\"}";;permission_denied)echo"🟡 AI 的操作被拒绝:$MESSAGE"# 记录到日志文件echo"[$(date)] Permission Denied:$MESSAGE">>~/.claude/logs/denied.log;;esacexit0五种事件总结
| 事件 | 触发时机 | 核心用途 | exit code 0 | exit code 1 |
|---|---|---|---|---|
| PreToolUse | Tool 调用前 | 拦截、安全检查 | 放行 | 阻止执行 |
| PostToolUse | Tool 调用后 | 自动格式化、提醒 | 正常 | 标记失败 |
| SessionStart | 会话启动 | 初始化环境、加载配置 | 正常 | 启动失败 |
| PreCompaction | 上下文压缩前 | 保存关键信息 | 正常 | 阻止压缩 |
| Notification | 通知事件 | 桌面通知、告警 | 正常 | 忽略 |
实战模板库:四个开箱即用的 Hook
模板一:自动格式化(PostToolUse)
上面已有完整代码。核心逻辑:检测到 Edit/Write 成功 → 判断文件类型 → 自动运行对应格式化工具。
模板二:Git 提交前检查(PreToolUse)
#!/bin/bash# PreToolUse Hook:Git 提交前检查TOOL_NAME="$1"TOOL_INPUT="$2"# 只拦截 git commit 命令if["$TOOL_NAME"!="Bash"];thenexit0fiif!echo"$TOOL_INPUT"|grep-q"git commit";thenexit0fi# 检查是否有未解决的合并冲突ifgitdiff--name-only --diff-filter=U|grep-q.;thenecho"🛑 提交被阻止:存在未解决的合并冲突。"gitdiff--name-only --diff-filter=Uexit1fi# 检查 .env 文件是否在暂存区(防止密钥提交)ifgitdiff--cached--name-only|grep-q'\.env$';thenecho"🛑 提交被阻止:.env 文件在暂存区。请从暂存区移除。"echo" 运行: git reset HEAD .env"exit1fi# 检查是否有调试代码(console.log / print 等)ifgitdiff--cached|grep-q'+.*console\.log\|+.*print(';thenecho"⚠️ 警告:暂存的代码中包含调试语句(console.log / print)。"echo" 建议清理后提交。"# 注意:这里只用警告,不阻止提交(exit 0)fiecho"✅ 提交前检查通过。"exit0模板三:环境变量注入(SessionStart)
上面已有完整代码。核心逻辑:启动时 source .env,检查依赖安装状态。
模板四:危险文件保护(PreToolUse)
#!/bin/bash# PreToolUse Hook:保护关键文件不被 AI 修改TOOL_NAME="$1"TOOL_INPUT="$2"# 只拦截 Edit 和 Writeif["$TOOL_NAME"!="Edit"]&&["$TOOL_NAME"!="Write"];thenexit0fi# 受保护的文件列表PROTECTED_FILES=(".env"".env.production""credentials.json""secrets.yml""prod-database.yml")# 从 Tool input 中提取文件路径FILE_PATH=$(echo"$TOOL_INPUT"|grep-o'"file_path": "[^"]*"'|cut-d'"'-f4)forprotectedin"${PROTECTED_FILES[@]}";doifecho"$FILE_PATH"|grep-q"$protected";thenecho"🛑 Hook 拦截:$FILE_PATH是受保护文件,AI 不能直接修改。"echo" 请手动编辑此文件。"exit1fidoneexit0如何配置 Hook
配置文件位置
// ~/.claude/settings.json 或项目 .claude/settings.json{"hooks":{"PreToolUse":[{"matcher":"Bash",// 可选:只匹配特定 Tool"command":"bash .claude/hooks/pre-tool-use.sh"}],"PostToolUse":[{"matcher":"Edit|Write",// 支持正则"command":"bash .claude/hooks/post-tool-use.sh"}],"SessionStart":[{"command":"bash .claude/hooks/session-start.sh"}],"PreCompaction":[{"command":"bash .claude/hooks/pre-compaction.sh"}],"Notification":[{"matcher":"task_complete|error",// 只监听特定事件"command":"bash .claude/hooks/notification.sh"}]}}Matcher 字段说明
"matcher": "" → 不写或空:匹配所有 Tool/事件 "matcher": "Edit" → 只匹配 Edit 事件 "matcher": "Edit|Write" → 匹配 Edit 或 Write(正则) "matcher": "Bash" → 只匹配 Bash 命令 对于 PreToolUse / PostToolUse:matcher 匹配的是 tool_name 对于 Notification:matcher 匹配的是 event_type 对于 SessionStart / PreCompaction:通常不需要 matcherHook 调试三招
第一招:加日志
#!/bin/bash# 在 Hook 脚本开头加日志LOG_FILE="$HOME/.claude/logs/hook-debug.log"echo"==========$(date)==========">>"$LOG_FILE"echo"Event:$1">>"$LOG_FILE"echo"Input:$2">>"$LOG_FILE"echo"Args:$@">>"$LOG_FILE"# 你的 Hook 逻辑...# 退出码也记录EXIT_CODE=$?echo"Exit:$EXIT_CODE">>"$LOG_FILE"exit$EXIT_CODE然后tail -f ~/.claude/logs/hook-debug.log实时看 Hook 的执行情况。
第二招:Dry-run 模式
# 在 Hook 脚本里加一个 dry-run 开关DRY_RUN=false# 调试时设为 trueif["$DRY_RUN"=true];thenecho"[DRY RUN] 会执行:$@"exit0fi第三招:手动模拟
# Hook 脚本本质上是 Shell 脚本,可以直接手动调用测试:bash.claude/hooks/pre-tool-use.sh"Bash"'{"command": "rm -rf /test"}'# 预期输出:🛑 Hook 拦截# 预期 exit code:1常见问题
Q:Hook 脚本执行失败会影响 AI 吗?
A:取决于 Hook 的 exit code。exit 0 放行,exit 1 阻止。但如果 Hook 脚本本身崩溃(非零 exit code 但不是 1),Claude Code 通常按 exit 0 处理(放行),同时记录错误。所以不要在 Hook 里写可能崩溃的代码。
Q:多个 Hook 的执行顺序?
A:同一事件的多个 Hook,按 settings.json 中定义的顺序依次执行。如果某个 Hook 返回 exit 1(PreToolUse),后续 Hook 不会再执行(因为 Tool 已经被阻止了)。
Q:Hook 执行太慢怎么办?
A:Hooks 会阻塞 Claude Code 的主流程。SessionStart 可以慢一点无所谓,但 PreToolUse/PostToolUse 需要快(< 2 秒)。不要在 Hook 里做网络请求或大量 IO。
延伸阅读
- Claude Code Hooks 官方文档
- Claude Code Settings 文档