主题
Claude Code Hooks 详解
一、什么是 Hooks?
Hooks 是 Claude Code 中用户自定义的 Shell 命令、HTTP 端点、LLM Prompt 或 Agent,在 Claude Code 生命周期的特定节点自动执行。它们提供对 Claude Code 行为的确定性控制,确保某些动作始终发生,而不是依赖 LLM 自行选择执行。
核心价值
| 场景 | 说明 |
|---|---|
| 消除重复操作 | 每次文件变更后自动运行格式化工具,无需手动执行 |
| 强制执行规则 | 阻止危险命令、校验文件路径、强制命名规范 |
| 注入动态上下文 | 会话启动时自动加载 git 状态、TODO 列表、Sprint 信息 |
| 安全审计 | 拦截敏感操作、扫描 secrets、记录操作日志 |
与其他扩展机制的区别
| 机制 | 特点 |
|---|---|
| Hooks | 确定性控制层,每次都执行,不依赖 LLM 判断 |
| Skills | 给 Claude 额外的指令和可执行命令 |
| Subagents | 在隔离上下文中运行任务 |
| Plugins | 打包扩展,跨项目共享 |
二、配置位置与作用域
Hooks 定义在 JSON 设置文件中,不同位置决定不同作用域:
| 位置 | 作用域 | 可否共享 |
|---|---|---|
~/.claude/settings.json | 所有项目(全局) | 否,仅本机 |
.claude/settings.json | 单个项目 | 是,可提交到仓库 |
.claude/settings.local.json | 单个项目 | 否,已 gitignore |
| 受管策略设置 | 组织级别 | 是,管理员控制 |
Plugin hooks/hooks.json | 启用插件时 | 是,与插件打包 |
| Skill / Agent frontmatter | 组件活跃时 | 是,定义在组件文件中 |
优先级:项目级设置优先于用户级设置。管理员可通过
allowManagedHooksOnly禁止用户级和项目级 hooks。
三、配置结构(三层嵌套)
json
{
"hooks": {
"事件名称(Hook Event)": [
{
"matcher": "匹配模式(Matcher Group)",
"hooks": [
{
"type": "command",
"command": "脚本路径或命令(Hook Handler)"
}
]
}
]
}
}三层嵌套含义:
- Hook Event — 选择要响应的生命周期事件(如
PreToolUse、Stop) - Matcher Group — 过滤何时触发(如"仅 Bash 工具")
- Hook Handler — 匹配时执行的命令、HTTP 端点、Prompt 或 Agent
四、Hook 事件全览
截至 2026 年 4 月,Claude Code 提供以下 Hook 事件,分为三种触发节奏:
4.1 会话级别(每会话一次)
| 事件 | 触发时机 |
|---|---|
SessionStart | 会话开始或恢复时 |
SessionEnd | 会话终止时 |
4.2 轮次级别(每轮一次)
| 事件 | 触发时机 |
|---|---|
UserPromptSubmit | 用户提交 prompt、Claude 处理之前 |
Stop | Claude 完成响应时 |
StopFailure | 轮次因 API 错误结束时(输出和退出码被忽略) |
4.3 工具调用级别(每次工具调用)
| 事件 | 触发时机 |
|---|---|
PreToolUse | 工具调用执行之前,可阻止 |
PermissionRequest | 权限对话框弹出时 |
PermissionDenied | 工具调用被自动模式分类器拒绝时 |
PostToolUse | 工具调用成功之后 |
PostToolUseFailure | 工具调用失败之后 |
4.4 其他事件
| 事件 | 触发时机 |
|---|---|
Notification | Claude Code 发送通知时 |
SubagentStart | 子代理生成时 |
SubagentStop | 子代理完成时 |
TaskCreated | 通过 TaskCreate 创建任务时 |
TaskCompleted | 任务被标记为完成时 |
TeammateIdle | Agent Team 中的队友即将空闲时 |
InstructionsLoaded | CLAUDE.md 或 .claude/rules/*.md 文件加载到上下文时 |
ConfigChange | 配置文件在会话中变更时 |
CwdChanged | 工作目录变更时(如执行 cd) |
FileChanged | 被监视的文件在磁盘上变更时 |
WorktreeCreate | 创建 worktree 时 |
WorktreeRemove | 移除 worktree 时 |
PreCompact | 上下文压缩之前 |
PostCompact | 上下文压缩之后 |
Elicitation | MCP 服务器请求用户输入时 |
ElicitationResult | 用户响应 MCP elicitation 之后 |
五、四种 Hook 类型
5.1 Command Hook(命令钩子)
最常用的类型。运行一个 Shell 命令,通过 stdin 接收 JSON 输入,通过 stdout/stderr 和退出码通信。
json
{
"type": "command",
"command": "bash .claude/hooks/format-on-save.sh",
"timeout": 600
}| 字段 | 必需 | 说明 |
|---|---|---|
command | 是 | 要执行的 Shell 命令 |
async | 否 | true 时在后台运行,不阻塞 |
asyncRewake | 否 | true 时后台运行并在退出码 2 时唤醒 Claude |
shell | 否 | "bash"(默认)或 "powershell" |
timeout | 否 | 超时秒数,默认 600 |
5.2 HTTP Hook(HTTP 钩子)
将事件数据以 JSON 形式 POST 到指定 URL。
json
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30,
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}| 字段 | 必需 | 说明 |
|---|---|---|
url | 是 | POST 请求目标 URL |
headers | 否 | 额外 HTTP 头,支持 $VAR 环境变量插值 |
allowedEnvVars | 否 | 允许插值的环境变量名列表 |
5.3 Prompt Hook(提示钩子)
将 prompt 和 hook 输入数据发送给 Claude 模型(默认 Haiku),由模型返回 yes/no 的 JSON 决策。适合需要判断力而非确定性规则的场景。
json
{
"type": "prompt",
"prompt": "Does this command look safe to execute? Only allow read-only operations. Here is the hook input: $ARGUMENTS",
"timeout": 30
}5.4 Agent Hook(代理钩子,实验性)
生成一个子代理,可使用 Read、Grep、Glob 等工具来验证条件后返回决策。
json
{
"type": "agent",
"prompt": "Review the file being written and check for security issues. $ARGUMENTS",
"timeout": 60
}| 字段 | 必需 | 说明 |
|---|---|---|
prompt | 是 | 发送给模型的提示文本,$ARGUMENTS 为 hook 输入的占位符 |
model | 否 | 使用的模型,默认为快速模型 |
六、Matcher 匹配规则
matcher 字段控制 hook 何时触发:
| Matcher 值 | 匹配方式 | 示例 |
|---|---|---|
"*", "", 或省略 | 匹配所有 | 对事件的每次发生都触发 |
仅字母、数字、_、` | ` | 精确匹配或 ` |
| 包含其他字符 | JavaScript 正则表达式 | ^Notebook / mcp__memory__.* |
不同事件的 Matcher 匹配对象
| 事件 | Matcher 过滤对象 | 示例 |
|---|---|---|
PreToolUse / PostToolUse 等 | 工具名 | Bash, `Edit |
SessionStart | 会话启动方式 | startup, resume, clear |
SessionEnd | 会话结束原因 | clear, resume, logout |
Notification | 通知类型 | permission_prompt, idle_prompt |
SubagentStart / SubagentStop | 代理类型 | Bash, Explore, Plan |
PreCompact / PostCompact | 压缩触发方式 | manual, auto |
FileChanged | 字面文件名 | `.envrc |
if 条件(更精细的过滤)
在单个 hook handler 上设置 if 字段,使用权限规则语法进行更细粒度的匹配:
json
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": ".claude/hooks/block-rm.sh"
}
]
}"Bash(git *)"— 匹配所有 git 子命令"Edit(*.ts)"— 仅匹配 TypeScript 文件编辑
匹配 MCP 工具
MCP 工具名遵循 mcp__<server>__<tool> 命名模式:
mcp__memory__create_entities— Memory 服务器的创建实体工具mcp__memory__.*— 匹配 Memory 服务器的所有工具mcp__.*__write.*— 匹配所有服务器中以 write 开头的工具
七、输入与输出
7.1 输入(stdin JSON)
所有事件都会收到以下公共字段:
json
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}| 字段 | 说明 |
|---|---|
session_id | 当前会话标识 |
transcript_path | 对话 JSON 文件路径 |
cwd | hook 调用时的当前工作目录 |
permission_mode | 当前权限模式 |
hook_event_name | 触发的事件名 |
tool_name | 工具名(工具事件特有) |
tool_input | 工具输入参数(工具事件特有) |
在子代理中运行时,还会额外包含 agent_id 和 agent_type 字段。
7.2 退出码
| 退出码 | 含义 |
|---|---|
| 0 | 成功,Claude Code 解析 stdout 中的 JSON 输出 |
| 2 | 阻止性错误,stderr 文本反馈给 Claude 作为错误信息 |
| 其他 | 非阻止性错误,显示提示后继续执行 |
重要:exit code 1 是非阻止性的。如果 hook 要强制执行策略,必须使用
exit 2。
7.3 Exit Code 2 在各事件中的行为
| 事件 | 能否阻止 | Exit 2 的效果 |
|---|---|---|
PreToolUse | 是 | 阻止工具调用 |
PermissionRequest | 是 | 拒绝权限 |
UserPromptSubmit | 是 | 阻止 prompt 处理并清除 prompt |
Stop | 是 | 阻止 Claude 停止,继续对话 |
SubagentStop | 是 | 阻止子代理停止 |
TaskCreated | 是 | 回滚任务创建 |
PreCompact | 是 | 阻止压缩 |
PostToolUse | 否 | 显示 stderr 给 Claude(工具已执行) |
Notification | 否 | 仅显示 stderr 给用户 |
7.4 JSON 输出
exit 0 时可通过 stdout 输出 JSON 实现更精细的控制:
json
{
"continue": true,
"stopReason": "Build failed",
"suppressOutput": false,
"systemMessage": "Warning message",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked"
}
}| 字段 | 默认值 | 说明 |
|---|---|---|
continue | true | false 时 Claude 完全停止处理 |
stopReason | 无 | continue 为 false 时显示给用户的消息 |
suppressOutput | false | true 时从调试日志中省略 stdout |
systemMessage | 无 | 显示给用户的警告信息 |
7.5 Decision Control 模式
不同事件使用不同的决策模式:
| 事件 | 决策模式 | 关键字段 |
|---|---|---|
Stop, PostToolUse, ConfigChange 等 | 顶层 decision | decision: "block", reason |
PreToolUse | hookSpecificOutput | permissionDecision (allow/deny/ask/defer) |
PermissionRequest | hookSpecificOutput | decision.behavior (allow/deny) |
PermissionDenied | hookSpecificOutput | retry: true 告知模型可重试 |
八、实战示例
8.1 自动格式化(最常用)
每次 Claude 写入或编辑文件后自动运行 Prettier:
json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}8.2 阻止危险命令
使用 PreToolUse 阻止 rm -rf 和 git push --force:
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
}
]
}
]
}
}.claude/hooks/block-rm.sh:
bash
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'
else
exit 0
fi8.3 会话启动注入上下文
json
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "git status --short && echo '---' && cat TODO.md"
}
]
}
]
}
}每次会话启动时,Claude 自动获知当前 git 状态和 TODO 列表。
8.4 自动审批安全命令
json
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Bash(npm test*)",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}'"
}
]
}
]
}
}8.5 Stop Hook — 确保任务完成
json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Review whether the task is complete. If all requirements are met, respond with 'complete'. If work remains, respond with 'continue' and specify what still needs to be done."
}
]
}
]
}
}8.6 用户 Prompt 提交时注入 Sprint 上下文
json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "cat ./current-sprint-context.md"
}
]
}
]
}
}8.7 PostToolUse 触发 Lint 检查
json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "node ~/.claude/hooks/lint-on-edit.js"
}
]
}
]
}
}8.8 PreToolUse 扫描 Secrets
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "node ~/.claude/hooks/secret-scan.js"
}
]
}
]
}
}九、高级特性
9.1 异步 Hook(后台运行)
通过 async: true 让 hook 在后台运行而不阻塞 Claude:
json
{
"type": "command",
"command": "bash .claude/hooks/long-running-check.sh",
"async": true
}asyncRewake: true 可以在后台 hook 以退出码 2 结束时唤醒 Claude,让它对后台检测到的问题做出反应。
9.2 Hooks in Skills & Agents(技能和代理中的 Hooks)
Hooks 可以直接定义在 Skill 和 Subagent 的 YAML frontmatter 中,作用域限定于组件的生命周期:
yaml
---
name: secure-operations
description: Perform operations with security checks
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/security-check.sh"
---- 仅在组件活跃时运行
- 组件完成后自动清理
- 对于子代理,
Stophooks 自动转换为SubagentStop
9.3 Hook 脚本中的环境变量(详细)
当 Claude Code 执行一个 hook 脚本时,脚本运行在当前工作目录中,继承 Claude Code 的进程环境。以下是脚本中可以使用的所有环境变量的详细分类。
9.3.1 Claude Code 为 Hook 专门注入的环境变量
这些变量仅在 hook 执行上下文中可用,由 Claude Code 在启动 hook 进程时自动设置:
| 变量 | 可用范围 | 说明 |
|---|---|---|
$CLAUDE_PROJECT_DIR | 所有 hook | 项目根目录的绝对路径(即 Claude Code 启动时的目录)。引用项目内脚本时应使用此变量,确保无论当前工作目录如何变化,路径始终正确。路径含空格时需用引号包裹:"$CLAUDE_PROJECT_DIR"/.claude/hooks/my-hook.sh |
$CLAUDE_CODE_REMOTE | 所有 hook | 当 Claude Code 运行在远程/Web 环境(云端会话)时设为 "true";在本地 CLI 环境中不设置或为空。可用于根据执行环境分支 hook 逻辑 |
$CLAUDE_ENV_FILE | 仅 SessionStart、CwdChanged、FileChanged | 指向一个文件路径,hook 脚本可向其中写入 export VAR=value 语句。写入的变量将在本会话后续所有 Bash 命令中生效。用于持久化虚拟环境激活、nvm 版本切换等环境配置 |
9.3.2 Plugin Hook 专用环境变量
这些变量仅在 Plugin 提供的 hook 中可用:
| 变量 | 说明 |
|---|---|
${CLAUDE_PLUGIN_ROOT} | 插件的安装目录的绝对路径。用于引用与插件打包在一起的脚本文件。每次插件更新后此路径会变化 |
${CLAUDE_PLUGIN_DATA} | 插件的持久化数据目录。用于存储依赖和状态,在插件更新后仍保留 |
9.3.3 继承的标准 Shell 环境变量
Hook 脚本继承当前 Shell 环境中的所有标准环境变量,包括但不限于:
| 变量 | 说明 |
|---|---|
$HOME | 用户主目录 |
$USER | 当前用户名 |
$PATH | 可执行文件搜索路径 |
$SHELL | 当前 Shell 路径 |
$PWD | 当前工作目录(hook 执行时为 Claude Code 的工作目录) |
$LANG / $LC_* | 语言和区域设置 |
$TERM | 终端类型 |
| 其他自定义变量 | 在 ~/.bashrc / ~/.zshrc 中设定的所有 export 变量 |
9.3.4 Claude Code 全局环境变量(hook 中也可读取)
这些变量在启动 Claude Code 之前设定,hook 脚本中也可以读取:
| 变量 | 说明 |
|---|---|
$CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS | SessionEnd hooks 的超时预算(毫秒),默认 1.5 秒 |
$CLAUDE_CODE_DEBUG_LOG_LEVEL | 设为 verbose 可看到更详细的 hook 匹配日志 |
$CLAUDE_CODE_SHELL | 覆盖自动检测的 Shell |
$CLAUDE_CODE_SHELL_PREFIX | 所有 Bash 命令的前缀包装命令 |
9.3.5 重要:hook 中不可用的变量
| 变量 | 说明 |
|---|---|
$CLAUDECODE | 此变量仅在 Claude Code 生成的 Bash 工具和 tmux 会话中设为 1,不在 hook 中设置。不能用它来判断是否在 hook 中运行 |
$CLAUDE_TOOL_INPUT | 历史文档中曾提及,但从未实际实现。工具输入数据通过 stdin JSON 传递,不是环境变量 |
$CLAUDE_EVENT_TYPE | 同上,已废弃。使用 stdin JSON 中的 hook_event_name 字段 |
$CLAUDE_TOOL_NAME | 同上,已废弃。使用 stdin JSON 中的 tool_name 字段 |
$CLAUDE_FILE_PATHS | 同上,已废弃。使用 stdin JSON 中的 tool_input 字段 |
关键设计原则:Claude Code hook 的输入数据全部通过 stdin 的 JSON 传入,而非环境变量。环境变量主要用于路径引用和环境检测,不用于传递事件数据。
9.3.6 $CLAUDE_ENV_FILE 使用详解
$CLAUDE_ENV_FILE 是最特殊的 hook 环境变量,仅在三种事件中可用:SessionStart、CwdChanged、FileChanged。
用法一:直接写入 export 语句
bash
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi
exit 0用法二:捕获 setup 命令产生的所有环境变化
bash
#!/bin/bash
ENV_BEFORE=$(export -p | sort)
# 运行会修改环境的 setup 命令
source ~/.nvm/nvm.sh
nvm use 20
if [ -n "$CLAUDE_ENV_FILE" ]; then
ENV_AFTER=$(export -p | sort)
comm -13 <(echo "$ENV_BEFORE") <(echo "$ENV_AFTER") >> "$CLAUDE_ENV_FILE"
fi
exit 0用法三:配合 direnv 在切换目录时自动加载环境
json
{
"hooks": {
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash >> \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}注意:使用
>>追加而非>覆写,以保留其他 hook 写入的变量。写入此文件的变量在本会话后续所有 Bash 命令中生效。
9.3.7 环境变量在 Hook 配置中的使用
环境变量不仅在脚本内部可用,还可以在 hooks.json 的 command 字段中直接引用:
json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
}
]
}
]
}
}对于 HTTP hook,环境变量可以插值到 headers 中,但必须在 allowedEnvVars 中显式声明:
json
{
"type": "http",
"url": "http://localhost:8080/hooks/validate",
"headers": {
"Authorization": "Bearer $MY_TOKEN",
"X-Project": "$PROJECT_NAME"
},
"allowedEnvVars": ["MY_TOKEN", "PROJECT_NAME"]
}未在 allowedEnvVars 中列出的变量引用会被替换为空字符串。
9.3.8 Prompt Hook 中的 $ARGUMENTS 占位符
Prompt hook 和 Agent hook 的 prompt 字段支持 $ARGUMENTS 占位符,它会被替换为 hook 的完整 JSON 输入数据:
json
{
"type": "prompt",
"prompt": "Evaluate whether this shell command is safe. Input: $ARGUMENTS"
}这不是环境变量,而是 Claude Code 在发送给模型之前做的字符串替换。
9.3.9 完整示例:在脚本中使用环境变量和 stdin JSON
bash
#!/bin/bash
# .claude/hooks/audit-tool-call.sh
# 同时使用环境变量和 stdin JSON
# 1. 通过环境变量获取项目路径
PROJECT="$CLAUDE_PROJECT_DIR"
LOG_DIR="$PROJECT/.claude/logs"
mkdir -p "$LOG_DIR"
# 2. 通过环境变量判断运行环境
if [ "$CLAUDE_CODE_REMOTE" = "true" ]; then
echo "Running in cloud environment" >&2
fi
# 3. 通过 stdin 读取 JSON 输入(这是获取事件数据的正确方式)
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
SESSION=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"')
# 4. 记录审计日志
echo "[$(date)] Event=$EVENT Tool=$TOOL_NAME Session=$SESSION CWD=$CWD" >> "$LOG_DIR/audit.log"
exit 09.4 once 字段
仅在 Skill frontmatter 中有效,设为 true 时 hook 在当前会话只执行一次,执行后自动移除。
9.5 多 Hook 并行执行
当多个 hook 匹配时,它们并行执行。相同的 hook 命令会自动去重(命令字符串相同的 command hook、URL 相同的 HTTP hook)。
9.6 输出大小限制
注入到上下文中的 hook 输出(additionalContext、systemMessage 或纯 stdout)上限为 10,000 字符。超出后保存到文件并替换为预览和文件路径。
十、Hook 解析流程
以 PreToolUse 为例,完整流程如下:
Claude 决定调用工具(如 Bash "rm -rf /tmp/build")
│
▼
① 事件触发:PreToolUse 事件触发
│
▼
② Matcher 检查:matcher "Bash" 匹配工具名 → 激活此 hook group
│
▼
③ if 条件检查:if "Bash(rm *)" 匹配子命令 → 生成 handler 进程
│
▼
④ Hook Handler 执行:脚本读取 stdin JSON,检查命令内容
│
▼
⑤ 返回决策:输出 JSON(如 permissionDecision: "deny")或 exit 2
│
▼
⑥ Claude Code 执行决策:阻止工具调用,向 Claude 展示原因十一、/hooks 菜单
在 Claude Code 中输入 /hooks 可打开只读的 hook 浏览器:
- 显示所有 hook 事件及其配置数量
- 可深入查看 matcher 和 handler 详情
- 显示 hook 来源标签:
User/Project/Local/Plugin/Session/Built-in - 只读:添加/修改/删除 hook 需编辑 settings JSON 或让 Claude 代为修改
十二、调试与排障
常见问题排查清单
- Hook 不触发 — 检查 matcher 是否正确,先移除 matcher 确认基础 hook 可加载
- JSON 解析失败 — 确保 stdout 只输出 JSON 对象(Shell profile 打印的文本会干扰)
- 脚本不可执行 — 确保
chmod +x且 shebang 正确(如#!/bin/bash) - 依赖缺失 — 验证脚本中调用的外部命令(如
jq、node)在$PATH中可用 - 路径错误 — 项目级 hook 相对于项目根目录,用户级 hook 相对于
~/.claude/ - 配置未生效 — Claude Code 有文件监视器自动重载,如仍无效则重启 Claude Code
禁用 Hooks
- 移除 hook:从 settings JSON 中删除对应条目
- 临时禁用所有 hooks:在 settings 中设置
"disableAllHooks": true - 无法单独禁用某个 hook(保留配置的同时禁用)
安全注意事项
- Hooks 以用户权限执行任意 Shell 命令
- 对设置文件的直接编辑会被文件监视器检测
- PreToolUse hooks 的决策优先于权限模式 — 即使在
bypassPermissions模式下,hook 返回deny仍会阻止工具调用 - Hooks 是控制层,不能直接调用 Claude Code 工具或
/命令
十三、最佳实践
- 从简单开始 — 先用一个 PostToolUse 格式化 hook 验证机制可行,再逐步扩展
- 使用
exit 2而非exit 1— 只有 exit 2 会阻止操作,exit 1 仅产生非阻止性错误 - 先去掉 matcher 调试 — 如果 hook 不工作,先移除 matcher 确认基础功能正常
- 用
$CLAUDE_PROJECT_DIR— 引用项目脚本时使用环境变量,确保路径正确 - JSON 输出要纯净 — stdout 中只包含 JSON 对象,避免 Shell profile 干扰
- 合理使用 Prompt Hook — 需要判断力的场景用 prompt hook,需要确定性的用 command hook
- 设置合理超时 — command 默认 600s,prompt 默认 30s,agent 默认 60s,按需调整
- 注意幂等性 — 多个 hook 并行执行且自动去重,确保脚本可安全重复运行
- PreToolUse 用于拦截,PostToolUse 用于后处理 — PostToolUse 无法撤销已执行的操作
十四、总结
Claude Code Hooks 是一套强大的确定性控制系统,覆盖了从会话启动到工具调用再到任务完成的完整生命周期。通过合理配置 hooks,你可以:
- 自动化代码格式化、lint 检查等重复工作
- 安全防护阻止危险操作和 secrets 泄露
- 上下文注入让 Claude 始终了解项目状态
- 质量保障确保任务完成前通过验证检查
- 工作流编排利用 Stop hook 实现多步骤自动化
Hooks 是 Claude Code 从"AI 辅助工具"升级为"AI 自动化工作流"的关键机制。
附录:实战案例 — 全链路对话上报
需求
在 Claude Code 执行过程中,将以下数据逐条上报到外部服务(如日志平台、数据库、HTTP API):
session_id- 用户的问题(原始 prompt)
- Claude 的回复(最终文本)
- 每次工具调用的详情(可选)
- 完整对话上下文(可选,通过 transcript 文件)
各事件可获取的关键字段
| Hook 事件 | 可获取的字段 |
|---|---|
UserPromptSubmit | session_id, prompt(用户输入文本), transcript_path, cwd |
Stop | session_id, last_assistant_message(Claude 最终回复文本), stop_hook_active, transcript_path |
PostToolUse | session_id, tool_name, tool_input, tool_response, transcript_path |
SessionStart | session_id, source(startup/resume/clear), model |
SessionEnd | session_id, exit_reason |
注意:Claude 内部的 system prompt(系统指令)不会暴露给 hook。但
transcript_path指向的 JSONL 文件包含完整的对话历史,包括所有用户消息、工具调用和 Claude 回复。
方案一:HTTP Hook(推荐,最简洁)
直接将事件数据 POST 到你的后端 API,无需编写脚本文件:
json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "http",
"url": "http://your-server:8080/api/hooks/user-prompt",
"timeout": 5,
"headers": {
"Authorization": "Bearer $REPORT_API_TOKEN",
"X-Project": "$CLAUDE_PROJECT_DIR"
},
"allowedEnvVars": ["REPORT_API_TOKEN", "CLAUDE_PROJECT_DIR"]
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "http",
"url": "http://your-server:8080/api/hooks/claude-response",
"timeout": 5,
"headers": {
"Authorization": "Bearer $REPORT_API_TOKEN"
},
"allowedEnvVars": ["REPORT_API_TOKEN"]
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://your-server:8080/api/hooks/tool-use",
"timeout": 5,
"headers": {
"Authorization": "Bearer $REPORT_API_TOKEN"
},
"allowedEnvVars": ["REPORT_API_TOKEN"]
}
]
}
]
}
}后端 API 收到的 POST body 就是 hook 的完整 JSON 输入,例如:
/api/hooks/user-prompt 收到:
json
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "UserPromptSubmit",
"prompt": "帮我写一个计算阶乘的函数"
}/api/hooks/claude-response 收到:
json
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "Stop",
"stop_hook_active": false,
"last_assistant_message": "我已经为你创建了一个阶乘函数..."
}方案二:Command Hook + Shell 脚本
如果你想本地记录或需要对数据做预处理后再上报:
.claude/settings.json 配置:
json
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/report.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/report.sh"
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/report.sh",
"async": true
}
]
}
]
}
}.claude/hooks/report.sh 脚本:
bash
#!/bin/bash
# 统一上报脚本:根据 hook_event_name 分发处理
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
CWD=$(echo "$INPUT" | jq -r '.cwd')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
LOG_DIR="$CLAUDE_PROJECT_DIR/.claude/logs"
mkdir -p "$LOG_DIR"
case "$EVENT" in
UserPromptSubmit)
PROMPT=$(echo "$INPUT" | jq -r '.prompt')
# 本地记录
jq -n \
--arg ts "$TIMESTAMP" \
--arg sid "$SESSION_ID" \
--arg event "$EVENT" \
--arg prompt "$PROMPT" \
--arg cwd "$CWD" \
'{timestamp: $ts, session_id: $sid, event: $event, user_prompt: $prompt, cwd: $cwd}' \
>> "$LOG_DIR/session-$SESSION_ID.jsonl"
# 上报到远程 API(异步,不阻塞)
curl -s -X POST "http://your-server:8080/api/report" \
-H "Content-Type: application/json" \
-d "$INPUT" \
> /dev/null 2>&1 &
;;
Stop)
RESPONSE=$(echo "$INPUT" | jq -r '.last_assistant_message')
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')
jq -n \
--arg ts "$TIMESTAMP" \
--arg sid "$SESSION_ID" \
--arg event "$EVENT" \
--arg response "$RESPONSE" \
--arg stop_active "$STOP_ACTIVE" \
'{timestamp: $ts, session_id: $sid, event: $event, claude_response: $response, stop_hook_active: $stop_active}' \
>> "$LOG_DIR/session-$SESSION_ID.jsonl"
curl -s -X POST "http://your-server:8080/api/report" \
-H "Content-Type: application/json" \
-d "$INPUT" \
> /dev/null 2>&1 &
;;
PostToolUse)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
jq -n \
--arg ts "$TIMESTAMP" \
--arg sid "$SESSION_ID" \
--arg event "$EVENT" \
--arg tool "$TOOL_NAME" \
--argjson tool_input "$(echo "$INPUT" | jq '.tool_input')" \
'{timestamp: $ts, session_id: $sid, event: $event, tool_name: $tool, tool_input: $tool_input}' \
>> "$LOG_DIR/session-$SESSION_ID.jsonl"
;;
esac
exit 0脚本创建后需要执行
chmod +x .claude/hooks/report.sh
方案三:解析 transcript 文件获取完整对话
如果需要在会话结束时一次性获取所有对话内容,可以用 SessionEnd hook 读取 transcript_path:
bash
#!/bin/bash
# .claude/hooks/export-session.sh
# 在会话结束时导出完整对话记录
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
if [ -f "$TRANSCRIPT" ]; then
# transcript 是 JSONL 格式,每行一个 JSON 对象
# 包含 type 为 "human"(用户)和 "assistant"(Claude)的消息
cp "$TRANSCRIPT" "$CLAUDE_PROJECT_DIR/.claude/logs/transcript-$SESSION_ID.jsonl"
# 或上传到远程存储
curl -s -X POST "http://your-server:8080/api/transcript" \
-H "Content-Type: application/jsonl" \
-d @"$TRANSCRIPT" \
> /dev/null 2>&1 &
fi
exit 0各方案对比
| 维度 | 方案一 (HTTP Hook) | 方案二 (Command + 脚本) | 方案三 (SessionEnd 导出) |
|---|---|---|---|
| 实时性 | 实时逐条上报 | 实时逐条上报 | 会话结束后一次性 |
| 复杂度 | 最低,无需写脚本 | 中等,需维护脚本 | 低,但数据有延迟 |
| 灵活性 | 后端处理 | 本地预处理 + 远程上报 | 拿到原始完整数据 |
| 依赖 | 需要后端 API 服务 | 需要 jq、curl | 需要后端接收文件 |
| 对性能影响 | 极小(异步 HTTP) | 小(可设 async) | 无(仅退出时) |
| 数据完整度 | 每个事件独立上报 | 每个事件独立上报 | 完整对话含所有细节 |
注意事项
UserPromptSubmit的 stdout 会被注入到 Claude 的上下文中,所以上报脚本不要向 stdout 输出任何内容,否则 Claude 会"看到"它PostToolUse事件频率很高(每次工具调用都触发),建议设置async: true避免影响响应速度Stop事件可能因 Stop hook 循环触发,检查stop_hook_active字段避免重复上报SessionEndhook 默认超时仅 1.5 秒,上报大文件时注意设置timeout- Hook 以用户权限运行,确保上报脚本中不会泄露敏感信息(如 API Key)
官方文档: