跳转到内容

Hooks 事件系统

Hooks 是在特定事件发生时自动运行的脚本,类似于 Git Hooks。你可以用它们来实现安全防护、代码质量检查、日志记录等自动化工作流。

安全防护

阻止危险命令,防止敏感文件泄露。

代码质量

自动格式化、lint 检查、运行测试。

上下文注入

在 Claude 处理请求前自动注入项目上下文。

日志审计

记录所有工具调用,追踪变更历史。


Hooks 通过事件系统工作。当 Claude Code 中发生特定事件时,对应的 Hook 脚本会自动触发。

事件触发时机可阻止?典型用途
SessionStart会话开始或恢复初始化、加载开发上下文
UserPromptSubmit用户提交提示,Claude 处理前上下文注入、提示验证
PreToolUse工具调用执行前安全验证、输入修改
PermissionRequest权限对话框出现时自定义审批逻辑
PostToolUse工具成功完成后格式化、日志记录
PostToolUseFailure工具调用失败后错误日志、恢复操作
NotificationClaude 发送通知声音提醒、自定义通知
SubagentStart子 Agent 启动子 Agent 初始化
SubagentStop子 Agent 完成子 Agent 清理
StopClaude 完成响应后续操作、继续循环
TeammateIdle团队 Agent 即将空闲团队协调、质量把关
TaskCompleted任务标记完成强制完成标准
ConfigChange配置文件在会话中更改是(策略除外)企业审计、阻止未授权变更
WorktreeCreate创建 worktree自定义 VCS 设置
WorktreeRemove移除 worktree清理 VCS 状态
PreCompact上下文压缩前压缩前保存状态
SessionEnd会话终止清理、日志记录
用户输入消息
┌──────────────────┐
│ UserPromptSubmit │ ← 添加上下文(如 git status)
└──────────────────┘
Claude 决定运行工具(如 Edit)
┌──────────────────┐
│ PreToolUse │ ← 安全检查
└──────────────────┘
▼(如果允许)
工具执行
┌──────────────────┐
│ PostToolUse │ ← 自动格式化
└──────────────────┘

Claude Code 支持两种 Hook 执行模型:

  • Claude 阻塞等待 Hook 完成
  • 退出码和标准输出立即可用于反馈
  • 适用场景:关键验证(安全检查、类型检查、阻止操作)
  • 配置:省略 async 或设置 async: false
Hook 用途执行模式原因
代码格式化 (Prettier, Black)异步外观变更,无需反馈
Lint 自动修复 (eslint —fix)异步非关键改进
类型检查 (tsc, mypy)同步错误必须阻止后续操作
安全验证同步必须阻止危险操作
日志/指标异步纯副作用,无需反馈
通知 (Slack, 邮件)异步用户提醒,非阻塞
测试执行同步结果影响下一步操作
Git 上下文注入同步处理前丰富提示内容

并非所有场景都需要 AI。选择合适的工具:

任务类型最佳工具原因示例
确定性的Bash 脚本快速、可预测、不消耗 token创建分支、获取 PR 评论
基于模式的Bash + 正则对已知模式可靠检查密钥、验证格式
需要理解的AI Agent需要判断力代码审查、架构决策
上下文依赖的AI Agent需要理解能力”是否符合需求?”

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/security-check.sh",
"timeout": 5000
}
]
}
]
}
}
字段说明
matcher正则模式,过滤 Hook 触发条件(工具名称等)
typeHook 类型:"command""http""prompt""agent"
command要运行的 Shell 命令(command 类型)
promptLLM 评估的提示文本(prompt/agent 类型)。使用 $ARGUMENTS 作为 Hook 输入 JSON 的占位符
timeout最大执行时间(秒)。默认:command 600s,prompt 30s,agent 60s
model用于评估的模型(prompt/agent 类型)。默认为快速模型
async如为 true,在后台运行不阻塞(仅 command 类型)
statusMessageHook 运行时显示的自定义加载消息
once如为 true,每个会话仅运行一次(仅限 skills)

运行 Shell 命令。通过 stdin 接收 JSON,通过 stdout 返回 JSON。最常用的类型。

{
"type": "command",
"command": ".claude/hooks/security-check.sh",
"timeout": 5000
}

Hook 通过 stdin 接收 JSON,包含通用字段和事件特定字段:

{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "git status"
}
}

Hook 通过退出码和可选的 JSON 标准输出来传达结果。

通用 JSON 字段

字段默认值说明
continuetrue如果为 false,Claude 停止所有处理
stopReasoncontinuefalse 时显示给用户的消息
suppressOutputfalse如果为 true,在详细模式中隐藏输出
systemMessage显示给用户的警告消息

事件特定控制

  • PreToolUse:使用 hookSpecificOutput 包含 permissionDecision(allow/deny/ask)、permissionDecisionReasonupdatedInputadditionalContext
  • PostToolUse, Stop, SubagentStop, UserPromptSubmit, ConfigChange:使用顶级 decision: "block"reason
  • TeammateIdle, TaskCompleted:仅退出码 2(无 JSON 决策控制)
  • PermissionRequest:使用 hookSpecificOutput 包含 decision.behavior(allow/deny)

PreToolUse 阻止示例

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Hook 阻止了危险命令"
}
}

PreToolUse 上下文注入

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "当前 git 分支: feature/auth。3 个未提交的文件。"
}
}
退出码含义结果
0成功允许操作,解析标准输出中的 JSON
2阻止错误阻止操作(对于可阻止事件),stderr 反馈给 Claude
其他非阻止错误stderr 在详细模式中显示,继续执行

.claude/hooks/security-blocker.sh
#!/bin/bash
# 阻止危险命令
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# 危险模式列表
DANGEROUS_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"rm -rf *"
"sudo rm"
"git push --force origin main"
"git push -f origin main"
"npm publish"
"> /dev/sda"
)
# 检查命令是否匹配危险模式
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if [[ "$COMMAND" == *"$pattern"* ]]; then
echo "BLOCKED: 检测到危险命令: $pattern" >&2
exit 2
fi
done
exit 0
.claude/hooks/auto-format.sh
#!/bin/bash
# 编辑后自动格式化代码
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# 仅对 Edit/Write 操作运行
if [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then
exit 0
fi
# 获取文件路径
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
# 无文件路径则跳过
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
# 对支持的文件运行 Prettier
if [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx|json|md|css|scss)$ ]]; then
npx prettier --write "$FILE_PATH" 2>/dev/null
fi
exit 0

模板 3:UserPromptSubmit 上下文注入

Section titled “模板 3:UserPromptSubmit 上下文注入”
.claude/hooks/git-context.sh
#!/bin/bash
# 为每个提示添加 git 上下文
# 获取 git 信息
BRANCH=$(git branch --show-current 2>/dev/null || echo "不是 git 仓库")
LAST_COMMIT=$(git log -1 --format='%h %s' 2>/dev/null || echo "无提交")
STAGED=$(git diff --cached --stat 2>/dev/null | tail -1 || echo "")
UNSTAGED=$(git diff --stat 2>/dev/null | tail -1 || echo "")
# 输出带上下文的 JSON
cat << EOF
{
"hookSpecificOutput": {
"additionalContext": "[Git] 分支: $BRANCH | 最新: $LAST_COMMIT | 暂存: $STAGED | 未暂存: $UNSTAGED"
}
}
EOF
exit 0
.claude/hooks/notification.sh
#!/bin/bash
# 在通知时播放声音 (macOS)
INPUT=$(cat)
TITLE=$(echo "$INPUT" | jq -r '.title // ""')
MESSAGE=$(echo "$INPUT" | jq -r '.message // ""')
# 根据内容决定声音
if [[ "$TITLE" == *"error"* ]] || [[ "$MESSAGE" == *"failed"* ]]; then
SOUND="/System/Library/Sounds/Basso.aiff"
elif [[ "$TITLE" == *"complete"* ]] || [[ "$MESSAGE" == *"success"* ]]; then
SOUND="/System/Library/Sounds/Hero.aiff"
else
SOUND="/System/Library/Sounds/Pop.aiff"
fi
afplay "$SOUND" 2>/dev/null &
exit 0
Terminal window
# security-check.ps1 - 阻止危险命令
$inputJson = [Console]::In.ReadToEnd() | ConvertFrom-Json
$command = $inputJson.tool_input.command
$dangerousPatterns = @(
"rm -rf /",
"rm -rf ~",
"Remove-Item -Recurse -Force C:\",
"git push --force origin main",
"git push -f origin main",
"npm publish"
)
foreach ($pattern in $dangerousPatterns) {
if ($command -like "*$pattern*") {
Write-Error "BLOCKED: 检测到危险命令: $pattern"
exit 2
}
}
exit 0

Windows settings.json 配置

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "powershell -ExecutionPolicy Bypass -File .claude/hooks/security-check.ps1",
"timeout": 5000
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "powershell -ExecutionPolicy Bypass -File .claude/hooks/auto-format.ps1",
"timeout": 10000
}
]
}
]
}
}

安全 Hooks 是保护系统的关键防线。

.claude/hooks/comprehensive-security.sh
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# === 关键阻止 (Exit 2) ===
# 文件系统破坏
[[ "$COMMAND" =~ rm.*-rf.*[/~] ]] && { echo "BLOCKED: 递归删除根目录/主目录" >&2; exit 2; }
# 磁盘操作
[[ "$COMMAND" =~ ">/dev/sd" ]] && { echo "BLOCKED: 直接写入磁盘" >&2; exit 2; }
[[ "$COMMAND" =~ "dd if=" ]] && { echo "BLOCKED: dd 命令" >&2; exit 2; }
# 受保护分支的 Git 强制操作
[[ "$COMMAND" =~ "git push".*"-f".*"(main|master)" ]] && { echo "BLOCKED: 强制推送到 main" >&2; exit 2; }
[[ "$COMMAND" =~ "git push --force".*"(main|master)" ]] && { echo "BLOCKED: 强制推送到 main" >&2; exit 2; }
# 包发布
[[ "$COMMAND" =~ "npm publish" ]] && { echo "BLOCKED: npm publish" >&2; exit 2; }
# 特权操作
[[ "$COMMAND" =~ ^sudo ]] && { echo "BLOCKED: sudo 命令" >&2; exit 2; }
# === 警告 (Exit 0 但记录日志) ===
[[ "$COMMAND" =~ "rm -rf" ]] && echo "WARNING: 检测到递归删除" >&2
exit 0
Terminal window
# 测试被阻止的命令
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | \
.claude/hooks/security-blocker.sh
echo "退出码: $?" # 应为 2
# 测试安全命令
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | \
.claude/hooks/security-blocker.sh
echo "退出码: $?" # 应为 0

一种高级模式是使用更强大的模型作为安全门,而非仅依赖静态规则匹配:

Terminal window
# .claude/hooks/opus-security-gate.sh(概念性)
# PreToolUse Hook - 路由到 Opus 进行安全筛查
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# 快速通道:已知安全工具跳过检查
[[ "$TOOL" == "Read" || "$TOOL" == "Grep" || "$TOOL" == "Glob" ]] && exit 0
# 路由到 Opus 进行安全分析
VERDICT=$(echo "$INPUT" | claude --model opus --print \
"分析此工具调用的安全风险。安全则回复 SAFE,否则 BLOCKED:原因")
[[ "$VERDICT" == SAFE* ]] && exit 0
echo "BLOCKED by security gate: $VERDICT" >&2
exit 2

保护敏感文件需要多层防护方法:

┌────────────────────────────────────────────┐
│ 文件保护架构 │
├────────────────────────────────────────────┤
│ │
│ 第 1 层: permissions.deny (内置) │
│ ───────────────────────── │
│ • 内置于 settings.json │
│ • 无需 Hook │
│ • 即时阻止所有工具访问 │
│ • 适用于: 绝对禁止的文件 │
│ │
│ 第 2 层: 模式匹配 (Hook) │
│ ───────────────────── │
│ • 使用 .agentignore 模式的 PreToolUse Hook │
│ • 支持 gitignore 风格语法 │
│ • 适用于: 敏感文件类别 │
│ │
│ 第 3 层: 绕过检测 (Hook) │
│ ───────────────────── │
│ • 检测变量展开 ($VAR) │
│ • 检测命令替换 $(cmd) │
│ • 适用于: 防御复杂攻击 │
│ │
└────────────────────────────────────────────┘
{
"permissions": {
"deny": [
".env",
".env.local",
".env.production",
"**/*.key",
"**/*.pem",
"credentials.json",
".aws/credentials"
]
}
}

优点:即时阻止,无需 Hook 缺点:无自定义逻辑,无法记录尝试

  1. 配置 settings.json

    {
    "permissions": {
    "deny": [".env", "*.key", "*.pem"]
    },
    "hooks": {
    "PreToolUse": [
    {
    "matcher": "Read|Write|Edit",
    "hooks": [
    {
    "type": "command",
    "command": ".claude/hooks/file-guard.sh",
    "timeout": 2000
    }
    ]
    }
    ]
    }
    }
  2. 创建 .agentignore

    .env*
    config/secrets/
    **/*.key
    **/*.pem
    credentials.json
  3. 复制 Hook 模板

    Terminal window
    cp examples/hooks/bash/file-guard.sh .claude/hooks/
    chmod +x .claude/hooks/file-guard.sh
  4. 测试保护

    Terminal window
    # 测试直接访问
    echo '{"tool_name":"Read","tool_input":{"file_path":".env"}}' | \
    .claude/hooks/file-guard.sh
    # 应显示 "File access blocked"
    # 测试绕过尝试
    echo '{"tool_name":"Read","tool_input":{"file_path":"$HOME/.env"}}' | \
    .claude/hooks/file-guard.sh
    # 应显示 "Variable expansion detected"

当 Hook 集合增长时,不要配置数十个独立 Hook,而是使用单一调度器根据文件类型、工具和上下文智能路由事件。

.claude/hooks/dispatch.sh
#!/bin/bash
# 所有 PostToolUse Hook 的单一入口点
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // ""')
HOOKS_DIR="$(dirname "$0")/handlers"
# 按文件扩展名路由
case "$FILE_PATH" in
*.ts|*.tsx)
[[ -x "$HOOKS_DIR/typescript.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/typescript.sh"
;;
*.py)
[[ -x "$HOOKS_DIR/python.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/python.sh"
;;
*.rs)
[[ -x "$HOOKS_DIR/rust.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/rust.sh"
;;
*.sql|*.prisma)
[[ -x "$HOOKS_DIR/database.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/database.sh"
;;
esac
# 按工具路由(始终运行,与文件类型无关)
case "$TOOL_NAME" in
Bash)
[[ -x "$HOOKS_DIR/security.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/security.sh"
;;
Write)
[[ -x "$HOOKS_DIR/new-file.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/new-file.sh"
;;
esac
exit 0

settings.json 配置(极简):

{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write|Bash",
"hooks": [{
"type": "command",
"command": ".claude/hooks/dispatch.sh"
}]
}]
}
}

Handler 目录结构

.claude/hooks/
├── dispatch.sh # 单一入口点
└── handlers/
├── typescript.sh # ESLint + tsc(.ts/.tsx)
├── python.sh # Ruff + mypy(.py)
├── rust.sh # cargo clippy(.rs)
├── database.sh # Schema 验证(.sql/.prisma)
├── security.sh # 阻止危险 Bash 命令
└── new-file.sh # Write 时检查命名规范
.claude/hooks/activity-logger.sh
#!/bin/bash
# 将所有工具使用记录到 JSONL 文件
INPUT=$(cat)
LOG_DIR="$HOME/.claude/logs"
LOG_FILE="$LOG_DIR/activity-$(date +%Y-%m-%d).jsonl"
# 创建日志目录
mkdir -p "$LOG_DIR"
# 清理旧日志(保留 7 天)
find "$LOG_DIR" -name "activity-*.jsonl" -mtime +7 -delete
# 提取工具信息
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# 创建日志条目
LOG_ENTRY=$(jq -n \
--arg timestamp "$TIMESTAMP" \
--arg tool "$TOOL_NAME" \
--arg session "$SESSION_ID" \
'{timestamp: $timestamp, tool: $tool, session: $session}')
# 追加到日志
echo "$LOG_ENTRY" >> "$LOG_FILE"
exit 0
.claude/hooks/lint-gate.sh
#!/bin/bash
# 代码变更后运行 linter
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# 仅在 Edit/Write 后运行
if [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then
exit 0
fi
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
# 仅 lint TypeScript/JavaScript
if [[ ! "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]]; then
exit 0
fi
# 运行 ESLint
LINT_OUTPUT=$(npx eslint "$FILE_PATH" 2>&1)
LINT_EXIT=$?
if [[ $LINT_EXIT -ne 0 ]]; then
cat << EOF
{
"systemMessage": "Lint errors found in $FILE_PATH:\n$LINT_OUTPUT"
}
EOF
fi
exit 0

将多个验证 Hook 串联起来,在代码变更后立即捕获问题:

Edit/Write → 类型检查 → Lint → 测试 → 通知 Claude
↓ ↓ ↓ ↓
file.ts tsc check eslint jest file.test.ts

三阶段流水线配置

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/typecheck-on-save.sh",
"timeout": 5000
},
{
"type": "command",
"command": ".claude/hooks/lint-gate.sh",
"timeout": 5000
},
{
"type": "command",
"command": ".claude/hooks/test-on-change.sh",
"timeout": 10000
}
]
}
]
}
}

测试文件检测逻辑

源文件测试文件模式
auth.tsauth.test.ts__tests__/auth.test.ts
utils.pyutils_test.pytest_utils.py
main.gomain_test.go

性能考虑

项目规模流水线时间可接受?
小型 (<100 文件)约 1-2 秒/编辑
中型 (100-1000 文件)约 2-5 秒/编辑是(增量编译)
大型 (1000+ 文件)约 5-10 秒/编辑考虑异步或跳过测试

优化策略

  1. 对 lint/格式化使用 async: true(外观检查)
  2. 保持类型检查同步(错误必须阻止)
  3. 仅运行变更文件的测试,跳过完整测试套件
  4. 使用增量编译(tsc --incremental

使用 Stop 事件在 Claude Code 结束时显示全面的会话统计信息:

{
"hooks": {
"SessionEnd": [{
"hooks": [{
"type": "command",
"command": "~/.claude/hooks/session-summary.sh"
}]
}]
}
}

示例输出

═══ Session Summary ═══════════════════
ID: abc-123-def-456
Name: Security hardening v3.26
Branch: main
Duration: Wall 1h 34m | Active 14m 24s
Tool Calls: 47 (OK 45 / ERR 2)
Read: 12 Bash: 10 Edit: 8 Write: 6
Grep: 5 Glob: 4 WebSearch: 2
Model Usage Reqs Input Output
claude-sonnet-4-5 42 493.9K 2.5K
claude-haiku-4-5 5 12.4K 46
Cache: 1.2M read / 45.3K created
Est. Cost: $0.74
═══════════════════════════════════════

先测试再部署

使用 echo 管道 JSON 到 Hook 脚本进行测试,确认退出码正确后再在 Claude Code 中使用。

合理设置超时

为每个 Hook 设置合理的超时时间。安全检查 2-5 秒,测试 10 秒,格式化 5 秒。

使用调度器模式

当 Hook 数量增多时,使用单一调度器而非在 settings.json 中配置大量独立 Hook。

区分同步和异步

关键验证用同步,非关键操作用异步,平衡安全性和性能。