Skip to content

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)"
          }
        ]
      }
    ]
  }
}

三层嵌套含义:

  1. Hook Event — 选择要响应的生命周期事件(如 PreToolUseStop
  2. Matcher Group — 过滤何时触发(如"仅 Bash 工具")
  3. Hook Handler — 匹配时执行的命令、HTTP 端点、Prompt 或 Agent

四、Hook 事件全览

截至 2026 年 4 月,Claude Code 提供以下 Hook 事件,分为三种触发节奏:

4.1 会话级别(每会话一次)

事件触发时机
SessionStart会话开始或恢复时
SessionEnd会话终止时

4.2 轮次级别(每轮一次)

事件触发时机
UserPromptSubmit用户提交 prompt、Claude 处理之前
StopClaude 完成响应时
StopFailure轮次因 API 错误结束时(输出和退出码被忽略)

4.3 工具调用级别(每次工具调用)

事件触发时机
PreToolUse工具调用执行之前,可阻止
PermissionRequest权限对话框弹出时
PermissionDenied工具调用被自动模式分类器拒绝时
PostToolUse工具调用成功之后
PostToolUseFailure工具调用失败之后

4.4 其他事件

事件触发时机
NotificationClaude Code 发送通知时
SubagentStart子代理生成时
SubagentStop子代理完成时
TaskCreated通过 TaskCreate 创建任务时
TaskCompleted任务被标记为完成时
TeammateIdleAgent Team 中的队友即将空闲时
InstructionsLoadedCLAUDE.md 或 .claude/rules/*.md 文件加载到上下文时
ConfigChange配置文件在会话中变更时
CwdChanged工作目录变更时(如执行 cd
FileChanged被监视的文件在磁盘上变更时
WorktreeCreate创建 worktree 时
WorktreeRemove移除 worktree 时
PreCompact上下文压缩之前
PostCompact上下文压缩之后
ElicitationMCP 服务器请求用户输入时
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 命令
asynctrue 时在后台运行,不阻塞
asyncRewaketrue 时后台运行并在退出码 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"]
}
字段必需说明
urlPOST 请求目标 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 文件路径
cwdhook 调用时的当前工作目录
permission_mode当前权限模式
hook_event_name触发的事件名
tool_name工具名(工具事件特有)
tool_input工具输入参数(工具事件特有)

在子代理中运行时,还会额外包含 agent_idagent_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"
  }
}
字段默认值说明
continuetruefalse 时 Claude 完全停止处理
stopReasoncontinuefalse 时显示给用户的消息
suppressOutputfalsetrue 时从调试日志中省略 stdout
systemMessage显示给用户的警告信息

7.5 Decision Control 模式

不同事件使用不同的决策模式:

事件决策模式关键字段
Stop, PostToolUse, ConfigChange顶层 decisiondecision: "block", reason
PreToolUsehookSpecificOutputpermissionDecision (allow/deny/ask/defer)
PermissionRequesthookSpecificOutputdecision.behavior (allow/deny)
PermissionDeniedhookSpecificOutputretry: 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 -rfgit 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
fi

8.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"
---
  • 仅在组件活跃时运行
  • 组件完成后自动清理
  • 对于子代理,Stop hooks 自动转换为 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_MSSessionEnd 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 环境变量,仅在三种事件中可用:SessionStartCwdChangedFileChanged

用法一:直接写入 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.jsoncommand 字段中直接引用

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 0

9.4 once 字段

仅在 Skill frontmatter 中有效,设为 true 时 hook 在当前会话只执行一次,执行后自动移除。

9.5 多 Hook 并行执行

当多个 hook 匹配时,它们并行执行。相同的 hook 命令会自动去重(命令字符串相同的 command hook、URL 相同的 HTTP hook)。

9.6 输出大小限制

注入到上下文中的 hook 输出(additionalContextsystemMessage 或纯 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 代为修改

十二、调试与排障

常见问题排查清单

  1. Hook 不触发 — 检查 matcher 是否正确,先移除 matcher 确认基础 hook 可加载
  2. JSON 解析失败 — 确保 stdout 只输出 JSON 对象(Shell profile 打印的文本会干扰)
  3. 脚本不可执行 — 确保 chmod +x 且 shebang 正确(如 #!/bin/bash
  4. 依赖缺失 — 验证脚本中调用的外部命令(如 jqnode)在 $PATH 中可用
  5. 路径错误 — 项目级 hook 相对于项目根目录,用户级 hook 相对于 ~/.claude/
  6. 配置未生效 — Claude Code 有文件监视器自动重载,如仍无效则重启 Claude Code

禁用 Hooks

  • 移除 hook:从 settings JSON 中删除对应条目
  • 临时禁用所有 hooks:在 settings 中设置 "disableAllHooks": true
  • 无法单独禁用某个 hook(保留配置的同时禁用)

安全注意事项

  • Hooks 以用户权限执行任意 Shell 命令
  • 对设置文件的直接编辑会被文件监视器检测
  • PreToolUse hooks 的决策优先于权限模式 — 即使在 bypassPermissions 模式下,hook 返回 deny 仍会阻止工具调用
  • Hooks 是控制层,不能直接调用 Claude Code 工具或 / 命令

十三、最佳实践

  1. 从简单开始 — 先用一个 PostToolUse 格式化 hook 验证机制可行,再逐步扩展
  2. 使用 exit 2 而非 exit 1 — 只有 exit 2 会阻止操作,exit 1 仅产生非阻止性错误
  3. 先去掉 matcher 调试 — 如果 hook 不工作,先移除 matcher 确认基础功能正常
  4. $CLAUDE_PROJECT_DIR — 引用项目脚本时使用环境变量,确保路径正确
  5. JSON 输出要纯净 — stdout 中只包含 JSON 对象,避免 Shell profile 干扰
  6. 合理使用 Prompt Hook — 需要判断力的场景用 prompt hook,需要确定性的用 command hook
  7. 设置合理超时 — command 默认 600s,prompt 默认 30s,agent 默认 60s,按需调整
  8. 注意幂等性 — 多个 hook 并行执行且自动去重,确保脚本可安全重复运行
  9. 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 事件可获取的字段
UserPromptSubmitsession_id, prompt(用户输入文本), transcript_path, cwd
Stopsession_id, last_assistant_message(Claude 最终回复文本), stop_hook_active, transcript_path
PostToolUsesession_id, tool_name, tool_input, tool_response, transcript_path
SessionStartsession_id, source(startup/resume/clear), model
SessionEndsession_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 服务需要 jqcurl需要后端接收文件
对性能影响极小(异步 HTTP)小(可设 async)无(仅退出时)
数据完整度每个事件独立上报每个事件独立上报完整对话含所有细节

注意事项

  1. UserPromptSubmit 的 stdout 会被注入到 Claude 的上下文中,所以上报脚本不要向 stdout 输出任何内容,否则 Claude 会"看到"它
  2. PostToolUse 事件频率很高(每次工具调用都触发),建议设置 async: true 避免影响响应速度
  3. Stop 事件可能因 Stop hook 循环触发,检查 stop_hook_active 字段避免重复上报
  4. SessionEnd hook 默认超时仅 1.5 秒,上报大文件时注意设置 timeout
  5. Hook 以用户权限运行,确保上报脚本中不会泄露敏感信息(如 API Key)

官方文档: