万字深度解析Claude Code的hook系统:让AI编程更智能、更可控|上篇—详解篇

❤️ 如果你也关注 AI 的发展现状,且对 AI 应用开发感兴趣,我会跟你分享大模型与 AI 领域的开源项目和应用,提供运行实例和实用教程,帮助你快速上手AI技术!也非常欢迎你通过公众号发消息加入我们!

❤️ 微信公众号|搜一搜:蚝油菜花


在AI编程助手的世界里,Claude Code以其强大的功能和灵活性脱颖而出。而其中最令人眼前一亮的功能之一,就是hook(钩子)系统。今天,我将深入探讨这个让 Claude Code 变得更加智能和可控的核心功能。

Hook这部分的内容高达两万字,篇幅较长,所以我将这一部分内容分为上下两篇,建议先收藏再慢慢消化!

  • 上篇:引入 hook 概念,一步步地跟随文章带你学会创建自己的 hook,循序渐进地深入了解 hook 系统。
  • 下篇:以具体的 hook 示例和解析为核心内容,了解 hook 的最佳实践方法,丰富你的实战经验,学会优化 hook。

🌟 如果你还不知道什么是 Claude Code,或者你还想知道怎么安装和快速上手,可以阅读前文:

什么是hook?

Claude Code 中的 hook 本质上是用户定义的 Shell 命令,它能够在 Claude Code 生命周期的各个"关键节点"自动执行。

用通俗的话来说:如果将 Claude Code 的工作流程比作一条繁忙的高速公路,那么 hook 就是沿途设置的智能交通信号灯,能够在预设的关键路口进行流量控制、安全检查和信息收集等操作。

为什么需要hook?

假设没有 hook,你想让 Claude Code 在每次编辑文件后都自动格式化代码,你就需要在每次对话时都主动提示它。但有了 hook,这个过程就变成了自动化的。

在 Claude Code 中,hook提供了确定性的控制机制,通过将规则和流程编码为应用程序级代码而非提示指令,hook 能够保证命令按预期执行,而不是依赖LLM的选择来运行它们。

Hook的常见应用场景

Hook 的应用场景非常灵活且广泛,以下是一些常见的应用示例:

应用场景 具体功能示例
智能通知系统 • 当Claude Code需要你的输入时,自动发送桌面通知 • 集成邮件提醒、Slack消息等
自动代码格式化 • 每次文件编辑后自动运行prettier格式化.ts文件 • 对.go文件运行gofmt • 自动修复markdown文件格式问题
日志记录与审计 • 跟踪和记录所有执行的命令 • 用于合规性检查和调试分析
代码质量反馈 • 当Claude Code生成不符合代码库约定的代码时提供自动反馈 • 集成代码检查工具和测试框架
安全与权限控制 • 阻止对生产文件或敏感目录的修改 • 实施自定义的安全策略

如何创建一个hook?

这里从最简单的例子开始,创建一个用于记录 Claude Code 运行命令的 hook。

步骤1:打开 hook 配置

在 Claude Code 中运行 /hooks 斜杠命令,选择创建 PreToolUse 类型的 hook。

🌟 小贴士PreToolUse表示"在工具使用之前",这个 hook 会在 Claude Code 执行任何工具之前被触发。除此之外,Claude Code 还支持创建多种不同的 hook 类型,我将在下一部分内容展开介绍。

步骤2:添加匹配器

选择 + Add new matcher... 并输入 Bash。这表示该 hook 只会在 Claude Code 要执行 Bash 工具时触发。

🌟 小贴士 :matcher 可以输入*来匹配所有工具。

步骤3:添加 hook 命令

选择 + Add new hook... 并输入以下命令:

bash 复制代码
jq -r '"\(.tool_input.command) - \(.tool_input.description // "No description")"' >> ~/.claude/bash-command-log.txt

命令解析

  1. jq -rjq 是一个强大的 JSON 处理工具。
  • -r 参数表示解析 JSON 数据后输出为字符串。
  1. jq 工具的过滤表达式:'"\(.tool_input.command) - \(.tool_input.description // "No description")'
  • \(.tool_input.command):提取 JSON 中的命令名称。
  • \(.tool_input.description // "No description"):提取命令描述,如果没有描述则展示"No description"。
  1. >> ~/.claude/bash-command-log.txt:将结果追加到用户主目录下的.claude/bash-command-log.txt文件中。

步骤4:保存配置

选择User settings作为存储位置,那么这个 hook 的配置将会被保存到用户级配置~/.claude/settings.json中,它将在全局生效并应用到所有项目。这里仅简单提及下,在后续章节我会详细介绍 hook 配置的存储位置和优先级。

步骤5:测试创建的hook

使用 Claude Code 运行简单的命令,比如 ls,当执行命令时,这个 hook 会被触发并执行以下操作:

  1. 接收 Claude Code 传递的 JSON 数据,例如:
json 复制代码
{
  "tool_input": {
    "command": "ls -la",
    "description": "列出当前目录的详细信息"
  }
}
  1. jq工具依照过滤表达式'"\(.tool_input.command) - \(.tool_input.description // "No description")',提取 JSON 数据中的关键信息并格式化为字符串,生成类似这样的日志记录:
plain 复制代码
ls -la - 列出当前目录的详细信息
  1. 追加到日志文件.claude/bash-command-log.txt,保存完整的命令执行历史记录.

执行命令后检查记录的日志文件:

bash 复制代码
cat ~/claude_commands.log

你应该能看到类似这样的记录:

plain 复制代码
ls -la - 列出当前目录的详细信息

Hook的类型

现在你已经成功创建了第一个 hook,相信你对 hook 已经有了一个初步的认识和理解。

下面让我们来了解 Claude Code 支持的所有 hook 类型。每种类型代表 Claude Code 会在不同的时机触发 hook,就像不同的"节点"一样。

调用工具类hook

PreToolUse - 调用工具前

触发时机:在 Claude Code 执行任何工具之前运行 hook。

常见匹配器(匹配条件)

  • Task - 子代理任务
  • Bash - Shell命令
  • Glob - 文件模式匹配
  • Grep - 内容搜索
  • Read - 文件读取
  • EditMultiEdit - 文件编辑
  • Write - 文件写入
  • WebFetchWebSearch - Web操作
  • * - 匹配任何工具,你也可以使用空字符串("")或留空 matcher。

常见应用场景:记录即将执行的操作、检查操作的权限、预处理工作

PostToolUse - 调用工具后

触发时机:在 Claude Code 执行工具之后运行 hook。

常见应用场景:清理临时文件、验证执行工具的结果是否正确、自动格式化刚刚修改的代码

实际例子

bash 复制代码
# 自动格式化刚刚修改的TypeScript文件
find . -name "*.ts" -newer /tmp/last_format -exec prettier --write {} \;
touch /tmp/last_format

用户交互类hook

UserPromptSubmit - 用户提交提示时

触发时机:当用户提交提示时(大模型开始处理之前)运行 hook。

常见应用场景:对用户的输入进行验证或预处理

Notification - 发送通知时

触发时机:当 Claude Code 发送通知时运行。

一般出现以下情况时会发送通知

  1. Claude需要权限使用工具:"Claude needs your permission to use Bash"
  2. 提示输入已空闲至少60秒:"Claude is waiting for your input"

会话生命周期类hook

SessionStart - 会话开始时

触发时机:当 Claude Code 启动新的会话或恢复会话时运行 hook。

匹配器(匹配条件/开始方式)

  • startup - 启动会话时运行
  • resume - 通过--resume--continue/resume命令恢复会话时运行
  • clear - 通过/clear清除会话记录时运行
  • compact - 自动或主动执行/compact命令压缩上下文时运行

常见应用场景:加载开发上下文(如现有问题或代码库的最近更改)、初始化环境

SessionEnd - 会话结束时

触发时机:当 Claude Code 会话结束时运行 hook。

匹配器(匹配条件/结束原因)

  • clear - 通过/clear清除会话记录时运行
  • logout - 用户注销时运行
  • prompt_input_exit - 当 Claude Code 正在等待用户输入的时候退出会话
  • other - 因其他情况以致退出会话时

常见应用场景:统计会话信息、保存会话记录

任务完成类hook

Stop - 主代理任务停止时

触发时机:当 Claude Code 主代理完成任务时运行 hook,如果是由于用户中断导致的停止,则不会运行。

SubagentStop - 子代理任务停止时

触发时机:当 Claude Code 通过 Task 工具调用相应的子代理完成任务时运行 hook。

系统事件类hook

PreCompact - 压缩上下文前处理

触发时机:当 Claude Code 即将执行压缩上下文操作之前运行 hook。

匹配器(匹配条件/压缩方式)

  • manual - 主动执行/compact命令,Claude Code 开始压缩上下文之前。
  • auto - Claude Code 自动压缩上下文之前。

如何选择合适的hook类型?

选择合适的 hook 类型就像选择在"合适的时机"做事:

  • 想在 Claude Code "动手"前做准备 → 选择 PreToolUse
  • 想在 Claude Code "完成工作"后收尾 → 选择 PostToolUse
  • 想在开始对话时进行初始化 → 选择 SessionStart
  • 想在结束对话时"整理环境" → 选择 SessionEnd
  • 想在收到通知时做额外处理 → 选择 Notification

详解 hook 配置

在前面已经介绍了如何创建一个 hook,下面将详细解读 hook 的配置方式。

Hook 配置文件的存储位置和优先级

Hook的配置文件可以存储在以下位置,不同的位置有着不同的优先级:

配置类型 配置文件位置 使用场景 应用举例
用户级配置 ~/.claude/settings.json • 所有项目中都使用的hook • 个人习惯和偏好设置 • 通用的工具和流程 • 自动格式化代码 • 记录操作日志
项目级配置 .claude/settings.json • 特定项目的特殊需求 • 团队协作的统一规范 • 项目特有的工作流程 • 特定的测试流程 • 项目特有的代码检查
本地项目配置 .claude/settings.local.json • 个人在项目中的特殊配置 • 不想影响团队其他成员的设置 • 本地调试配置 • 个人工作流定制

Hook配置遵循优先覆盖原则:本地项目配置具有最高优先级,可以覆盖其他所有配置;项目级配置具有中等优先级,可以覆盖用户级配置;用户级配置具有最低优先级,能够全局生效但会被其他配置覆盖。

Hook 的 JSON 格式配置

settings.json文件中,hook 的配置使用 JSON 格式表示,像是一个结构化的清单。Hook 按触发时机和匹配器进行组织,每个匹配器可以有多个 hook,这里举例一个 PreToolUse 类的 hook 配置:

json 复制代码
{
  "hooks": {                    // 所有hook配置的根节点
    "PreToolUse": [             // Hook事件类型(在工具使用前触发)
      {
        "matcher": "Bash",      // 匹配器(这里是"Bash",表示只匹配Bash命令)
        "hooks": [              // 具体要执行的hook列表
          {
            "type": "command",  // Hook 执行的任务类型(目前只支持"command")
            "command": "python3 $CLAUDE_PROJECT_DIR/.claude/hooks/my_script.py",  // Hook要执行的Shell命令
            "timeout": 30       // 超时时间(可选)
          }
        ]
      }
    ]
  }
}

逐项解释这个配置

  • "hooks" :所有hook配置的根节点
    • "PreToolUse":hook 的类型(触发时机)
    • "matcher" :hook 的匹配器(匹配条件)
      • "hooks" :具体要执行的 hook 列表
        • "type":hook 执行的任务类型(目前只支持"command")
        • "command":hook 要执行 Shell 命令,在 command 中可以使用系统的环境变量
        • "timeout":超时时间(秒),防止hook执行时间过长

Hook 的匹配条件---matcher

匹配器(matcher)是 hook 的"匹配条件",决定是否执行 hook。matcher字段需要填写匹配的工具名称,注意区分大小写:

匹配类型 示例 含义
精确匹配 "matcher": "Write" 只有当 Claude Code 要使用 Write 工具时才触发
正则表达式匹配 `"matcher": "Edit Write"`
正则表达式匹配 "matcher": "Notebook.*" 匹配所有以"Notebook"开头的工具
通配符匹配 "matcher": "*" Claude Code 使用任何工具都会触发

🌟 小贴士 :目前 matcher 字段仅适用于 PreToolUsePostToolUse。对于像 UserPromptSubmitNotificationStopSubagentStop 这样不需要匹配器的事件,您可以省略matcher字段,示例如下:

json 复制代码
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/prompt-validator.py"
          }
        ]
      }
    ]
  }
}

Hook 的标准输入(stdin)

Hook 通过标准输入(stdin)接收 Claude Code 提供的特殊信息(包含会话信息和 hook 事件特定数据的 JSON 数据),就像接收"参数"一样,传递信息的结构大致如下:

json 复制代码
{
  // 会话信息
  "session_id": "string",           // 会话ID
  "transcript_path": "string",      // 会话JSON文件路径
  "cwd": "string",                  // Hook被调用时的当前工作目录

  // Hook事件特定数据
  "hook_event_name": "string"
  // ...
}

PreToolUse 输入

这里以 PreToolUse 类 hook 事件为例,展示 Claude Code 提供的特殊信息,其中tool_input的 JSON Schema 取决于调用的工具。

json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  }
}

PostToolUse 输入

tool_inputtool_response 的 JSON Schema 取决于调用的工具。

json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  },
  "tool_response": {
    "filePath": "/path/to/file.txt",
    "success": true
  }
}

Notification 输入

json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "Notification",
  "message": "Task completed successfully"
}

UserPromptSubmit 输入

json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "Write a function to calculate the factorial of a number"
}

Stop 和 SubagentStop 输入

当 Claude Code 已经由于停止 hook 而继续时stop_hook_active为 true。检查此值或处理记录以防止 Claude Code 无限运行。

json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "hook_event_name": "Stop",
  "stop_hook_active": true
}

PreCompact 输入

custom_instructions 字段的不同来源

  • 主动压缩(manual :当用户主动使用 /compact 命令时,可以在命令中传递自定义指令,这些指令会作为custom_instructions传递给PreCompacthook。例如,用户可能会执行/compact 请保留重要的技术细节这样的命令,那么"请保留重要的技术细节"就会成为custom_instructions字段的内容。
  • 自动压缩(auto :系统检测到对话过长并自动压缩时,默认是没有用户特殊要求的, 所以custom_instructions字段为空。
json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "hook_event_name": "PreCompact",
  "trigger": "manual",
  "custom_instructions": ""
}

SessionStart 输入

json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "hook_event_name": "SessionStart",
  "source": "startup"
}

SessionEnd 输入

json 复制代码
{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "SessionEnd",
  "reason": "exit"
}

Hook的标准输出(stdout/stderr)

Hook 有两种不同的输出方式来返回信息给 Claude Code。输出的信息包含了控制 Claude Code 行为的指示 以及向 Claude Code 和用户的显式反馈

输出方式一:返回退出错误码

Hook 不仅可以执行操作,还可以控制 Claude Code 是否继续执行,这一功能通过"退出状态码"实现。

退出状态码的含义

退出状态码 含义 行为
exit 0 成功 • stdout 会在 transcript 模式(可使用快捷键 CTRL+R 切换到该模式)下展示给用户 • 对于 UserPromptSubmit 和 SessionStart 的 hook 事件,stdout 会被添加到上下文中
exit 2 阻塞错误 • stderr 会自动反馈给 Claude Code 处理 • 后续 Claude Code 的具体行为取决于对应的 hook 事件类型
exit 其他退出代码 非阻塞错误 • stderr 会展示给用户 • Claude Code 会继续执行

对于不同 hook 事件类型,返回退出状态码2时 Claude Code 的行为也有所不同

hook 事件类型 行为
PreToolUse 阻止工具调用,向 Claude Code 展示 stderr
PostToolUse 工具调用后,向 Claude Code 展示 stderr
Notification Claude Code 不会作任何处理,仅向用户展示 stderr
UserPromptSubmit 阻止提示处理,擦除提示,仅向用户展示 stderr
Stop 阻止停止,向 Claude Code 展示 stderr
SubagentStop 阻止停止,向 Claude Code 的子代理展示 stderr
PreCompact Claude Code 不会作任何处理,仅向用户展示 stderr
SessionStart Claude Code 不会作任何处理,仅向用户展示 stderr
SessionEnd Claude Code 不会作任何处理,仅向用户展示 stderr

具体示例 - Bash 命令校验器

python 复制代码
#!/usr/bin/env python3
import json
import re
import sys

# 定义校验规则,每条规则由(正则表达式模式,提示信息)元组组成
VALIDATION_RULES = [
  (
    r"\bgrep\b(?!.*\|)",
    "建议使用 'rg'(ripgrep)替代 'grep',性能更好且功能更丰富",
  ),
  (
    r"\bfind\s+\S+\s+-name\b",
    "建议使用 'rg --files | rg pattern' 或 'rg --files -g pattern' 替代 'find -name',性能更优",
  ),
]


def validate_command(command: str) -> list[str]:
  # 校验命令,返回所有不符合规则的提示信息
  issues = []
  for pattern, message in VALIDATION_RULES:
    if re.search(pattern, command):
      issues.append(message)
  return issues


try:
  # 从标准输入读取并解析JSON数据
  input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
  print(f"错误:无效的JSON输入:{e}", file=sys.stderr)
  sys.exit(1)

tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
command = tool_input.get("command", "")

# 仅当工具名称为 Bash 且命令不为空时才进行校验
if tool_name != "Bash" or not command:
  sys.exit(1)

# 校验命令
issues = validate_command(command)

if issues:
  # 输出所有校验不通过的提示信息到标准错误
  for message in issues:
    print(f"• {message}", file=sys.stderr)
  # 退出码2:阻止工具调用,并将错误信息反馈给Claude
  sys.exit(2)

输出方式二:返回结构化的 JSON 数据

除了返回错误状态码,hook 还可以返回结构化的 JSON 数据到 stdout,以实现更复杂的控制。

通用 JSON 字段

所有 hook 类型都可以包含这些可选字段:

json 复制代码
{
  "continue": true, // Claude 是否应该在 hook 执行后继续(默认:true)
  "stopReason": "string", // 当 continue 为 false 时展示的消息

  "suppressOutput": true, // 在 transcript 模式中隐藏 stdout(默认:false)
  "systemMessage": "string" // 向用户展示的可选警告消息
}

如果 continue 为 false,Claude Code 会在 hooks 运行后停止处理。

  • 对于 PreToolUse,这与 "permissionDecision": "deny" 不同,后者仅阻止特定工具调用并向 Claude Code 提供自动反馈。
  • 对于 PostToolUse,这与 "decision": "block" 不同,后者向 Claude 提供自动反馈。
  • 对于 UserPromptSubmit,这防止提示被处理。
  • 对于 StopSubagentStop,这优先于任何"decision": "block" 输出。
  • 在所有情况下,"continue" = false 优先于任何"decision": "block" 输出。

stopReason 会根据 continue 向用户展示停止的原因,不向 Claude Code 展示。

PreToolUse 调用工具前的决策控制

PreToolUse hooks 可以通过输出的参数来控制 Claude Code 是否继续调用工具。 其中 permissionDecision 决定了权限处理方式,所以也被称为权限决策 ,而 permissionDecisionReason 则是根据不同的权限决策向不同的对象(用户或 Claude Code)展示相应的原因说明。

permissionDecision 功能描述 permissionDecisionReason 展示对象
"allow" 绕过权限系统 仅向用户展示
"deny" 防止调用工具 向 Claude Code 展示
"ask" 要求用户进一步确认是否调用工具 仅向用户展示
json 复制代码
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow" | "deny" | "ask",
    "permissionDecisionReason": "My reason here"
  }
}

PreToolUse hooks 的 decisionreason 字段已弃用。 请使用 hookSpecificOutput.permissionDecisionhookSpecificOutput.permissionDecisionReason。 弃用的字段 "approve""block" 分别映射到 "allow""deny"

具体示例 --- PreToolUse 与批准

python 复制代码
#!/usr/bin/env python3
import json
import sys

# 从 stdin 加载输入
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)

tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})

# 示例:自动批准文档文件的文件读取
if tool_name == "Read":
    file_path = tool_input.get("file_path", "")
    if file_path.endswith((".md", ".mdx", ".txt", ".json")):
        # 使用 JSON 输出自动批准工具调用
        output = {
            "decision": "approve",
            "reason": "Documentation file auto-approved",
            "suppressOutput": True  # 不在 transcript 模式中展示
        }
        print(json.dumps(output))
        sys.exit(0)

# 对于其他情况,让正常的权限流程继续
sys.exit(0)

PostToolUse 工具执行后的决策控制

PostToolUse hooks 可以在工具执行后向 Claude Code 提供工具反馈的信息。 其中 decision 决定了工具执行后的反馈方式,当 decision"block" 时,reason 会自动作为提示信息传递给 Claude Code;当为 undefined 时,reason 则会被忽略。

decision 功能描述 reason 处理方式
"block" reason 提示 Claude Code 向 Claude Code 展示原因
undefined 什么都不做 被忽略
  • 通过 hookSpecificOutput.additionalContext 参数可以在工具执行后为 Claude Code 添加要考虑的上下文。
json 复制代码
{
  "decision": "block" | undefined,
  "reason": "Explanation for decision",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Additional information for Claude"
  }
}

具体行为说明

  1. decision: "block"

    • Claude Code 会继续正常工作,不会停止。如果需要真正停止 Claude Code 的工作,应该使用"continue": false参数。
    • reason 中的信息会作为反馈传递给 Claude Code。
    • Claude Code 会根据反馈信息调整后续行为。
    • 适用于需要告知 Claude Code 工具执行结果或异常原因的场景。
  2. decision: undefined

    • 什么都不做,静默执行。
    • reason 字段被完全忽略。
    • 适用于仅需要记录或监控,不需要向 Claude Code 反馈信息的场景。

具体示例 --- PostToolUse 与代码质量检查

python 复制代码
#!/usr/bin/env python3
import json
import sys
import subprocess

# 从 stdin 加载输入
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)

tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})

# 示例:Python 文件编辑后自动运行代码风格检查
if tool_name == "Edit" and tool_input.get("file_path", "").endswith(".py"):
    file_path = tool_input.get("file_path")
    
    # 运行 flake8 检查代码风格
    try:
        result = subprocess.run(["flake8", file_path], 
                              capture_output=True, text=True)
        
        if result.returncode != 0:
            # 代码风格检查失败,向 Claude Code 反馈
            output = {
                "decision": "block",
                "reason": f"代码风格检查失败:\n{result.stdout}\n请修复这些问题后再继续。",
                "hookSpecificOutput": {
                    "hookEventName": "PostToolUse",
                    "additionalContext": "建议运行 autopep8 自动修复格式问题"
                }
            }
            print(json.dumps(output))
            sys.exit(0)
        else:
            # 代码风格检查通过,提供正面反馈
            output = {
                "decision": "block",
                "reason": "代码风格检查通过,文件格式良好。",
                "hookSpecificOutput": {
                    "hookEventName": "PostToolUse"
                }
            }
            print(json.dumps(output))
            sys.exit(0)
            
    except FileNotFoundError:
        # flake8 未安装,静默通过
        sys.exit(0)

# 对于其他情况,不做任何处理
sys.exit(0)

UserPromptSubmit 用户提示时的决策控制

UserPromptSubmit hooks 可以在用户提交提示时进行拦截和处理。 其中 decision 决定了是否允许提示继续处理,当 decision"block" 时,提示会被阻止并从上下文中擦除;当为 undefined 时,提示正常处理。

decision 功能描述 reason 处理方式
"block" 防止提示被处理,从上下文中擦除 向用户展示擦除的原因但不添加到上下文
undefined 允许提示正常处理 被忽略
  • 通过 hookSpecificOutput.additionalContext 参数可以在 Claude Code 处理提示前添加额外的上下文信息。
json 复制代码
{
  "decision": "block" | undefined,
  "reason": "Explanation for decision",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "My additional context here"
  }
}

具体行为说明:

  1. decision: "block"
    • 完全阻止 Claude Code 处理用户提示
    • 提示的内容会从对话上下文中被擦除
    • reason 信息仅向用户展示,不会展示给 Claude Code
    • 适用于内容过滤、权限控制或提示验证失败的场景
  2. decision: undefined
    • 允许 Claude Code 正常处理提示
    • 可通过 additionalContext 为 Claude Code 添加额外信息
    • 适用于提示增强、需额外上下文注入的场景

具体示例 --- UserPromptSubmit 与内容过滤

python 复制代码
#!/usr/bin/env python3
import json
import sys
import re

# 从 stdin 加载输入
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)

user_prompt = input_data.get("user_prompt", "")

# 敏感词列表
sensitive_words = ["password", "secret", "token", "api_key"]

# 检查是否包含敏感信息
for word in sensitive_words:
    if re.search(rf"\b{word}\b", user_prompt.lower()):
        # 阻止包含敏感信息的提示
        output = {
            "decision": "block",
            "reason": f"提示包含敏感信息 '{word}',已被阻止处理。请移除敏感内容后重新提交。",
            "hookSpecificOutput": {
                "hookEventName": "UserPromptSubmit"
            }
        }
        print(json.dumps(output))
        sys.exit(0)

# 检查提示长度,如果过长则添加警告上下文
if len(user_prompt) > 1000:
    output = {
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": "注意:用户提交了一个较长的提示,请仔细阅读并确保理解所有要求。"
        }
    }
    print(json.dumps(output))
    sys.exit(0)

# 正常情况下不做任何处理
sys.exit(0)

Stop/SubagentStop 代理任务停止时的决策控制

StopSubagentStop hooks 可以控制 Claude Code 是否允许停止工作。 其中 decision 决定了是否允许停止,当 decision"block" 时,Claude Code 会被要求继续工作,且必须提供 reason 告诉 Claude Code 为什么需要继续以及如何继续;当为 undefined 时,允许正常停止。

decision 功能描述 reason 处理方式
"block" 防止 Claude Code 停止,要求继续工作 向 Claude Code 展示继续工作的原因
undefined 允许 Claude Code 正常停止 被忽略
json 复制代码
{
  "decision": "block" | undefined,
  "reason": "Must be provided when Claude is blocked from stopping"
}

具体行为说明:

  1. decision: "block"

    • 阻止 Claude Code 停止当前任务
    • 必须提供 reason 告诉 Claude Code 为什么需要继续以及如何继续
    • Claude Code 会根据 reason 中的提示继续工作
    • 适用于任务未完成、需要额外步骤的场景
  2. decision: undefined

    • 允许 Claude Code 正常停止
    • reason 字段被忽略
    • 适用于任务已完成或可以正常结束的场景

具体示例 --- Stop 与任务完整性检查

python 复制代码
#!/usr/bin/env python3
import json
import sys
import os

# 从 stdin 加载输入
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)

# 检查项目是否有未提交的更改
try:
    result = os.system("git diff --quiet")
    has_uncommitted_changes = (result != 0)
    
    if has_uncommitted_changes:
        # 有未提交的更改,阻止停止
        output = {
            "decision": "block",
            "reason": "检测到未提交的代码更改。请先提交或暂存这些更改,然后运行测试确保代码质量,最后更新相关文档。"
        }
        print(json.dumps(output))
        sys.exit(0)
        
except:
    # 如果不是 git 仓库或其他错误,允许正常停止
    pass

# 检查是否有测试文件但没有运行测试
if os.path.exists("test") or os.path.exists("tests"):
    # 这里可以添加检查最近是否运行过测试的逻辑
    # 为简化示例,假设需要提醒运行测试
    output = {
        "decision": "block",
        "reason": "项目包含测试文件。建议在结束前运行测试套件确保代码质量:npm test 或 python -m pytest"
    }
    print(json.dumps(output))
    sys.exit(0)

# 正常情况下允许停止
sys.exit(0)

SessionStart 会话开始时自动加载上下文

SessionStart hooks 允许在会话开始时自动加载上下文信息。 这个 hook 主要用于为 Claude Code 提供项目相关的背景信息、开发环境状态或其他有用的上下文。

json 复制代码
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "My additional context here"
  }
}

具体行为说明:

  • 自动上下文注入 :通过 additionalContext 为 Claude Code 提供项目背景信息。
  • 多个 hooks 支持 :多个 SessionStart hooks 的 additionalContext 会被自动连接后添加到上下文中。
  • 会话初始化:在每次新会话或恢复会话时都会自动执行。
  • 无决策控制 :SessionStart hooks 不支持 decision 字段,所以不同于其他 hooks 可以控制 Claude Code 的行为,目前只能用于添加上下文。

具体示例 --- SessionStart 与项目上下文加载

python 复制代码
#!/usr/bin/env python3
import json
import sys
import os
import subprocess
from datetime import datetime

# 从 stdin 加载输入
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)

context_parts = []

# 添加项目基本信息
if os.path.exists("package.json"):
    context_parts.append("这是一个 Node.js 项目")
elif os.path.exists("requirements.txt") or os.path.exists("pyproject.toml"):
    context_parts.append("这是一个 Python 项目")
elif os.path.exists("Cargo.toml"):
    context_parts.append("这是一个 Rust 项目")

# 添加 Git 状态信息
try:
    result = subprocess.run(["git", "status", "--porcelain"], 
                          capture_output=True, text=True)
    if result.returncode == 0:
        if result.stdout.strip():
            context_parts.append("当前有未提交的更改")
        else:
            context_parts.append("工作目录是干净的")
            
        # 获取当前分支
        branch_result = subprocess.run(["git", "branch", "--show-current"], 
                                     capture_output=True, text=True)
        if branch_result.returncode == 0:
            branch = branch_result.stdout.strip()
            context_parts.append(f"当前分支:{branch}")
except:
    pass

# 添加最近的活动信息
if os.path.exists(".claude"):
    context_parts.append("项目已配置 Claude Code hooks")

# 添加时间信息
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
context_parts.append(f"会话开始时间:{current_time}")

# 构建上下文信息
if context_parts:
    additional_context = "项目状态概览:\n" + "\n".join(f"- {part}" for part in context_parts)
    
    output = {
        "hookSpecificOutput": {
            "hookEventName": "SessionStart",
            "additionalContext": additional_context
        }
    }
    print(json.dumps(output))

sys.exit(0)

SessionEnd 会话结束时自动运行

SessionEnd hooks 在会话结束时就会自动运行,一般用于在会话结束前完成必要的收尾工作,例如执行清理任务和数据保存。这类 hook 无法阻止会话终止,所以不需要向 Claude Code 传递任何参数。

具体行为说明:

  • 自动执行:在每次会话结束时自动触发,无需用户干预。
  • 无决策控制 :SessionEnd hooks 无法阻止会话终止,所以不需要输出 decision 等字段。
  • 清理任务:适用于临时文件清理、状态保存、日志记录等收尾工作。

具体示例 --- SessionEnd 与项目清理

python 复制代码
#!/usr/bin/env python3
import json
import sys
import os
import shutil
import subprocess
from datetime import datetime

# 从 stdin 加载输入
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
    sys.exit(1)

# 清理临时文件
temp_dirs = [".tmp", "temp", "__pycache__", ".pytest_cache"]
for temp_dir in temp_dirs:
    if os.path.exists(temp_dir):
        try:
            shutil.rmtree(temp_dir)
            print(f"已清理临时目录: {temp_dir}", file=sys.stderr)
        except Exception as e:
            print(f"清理 {temp_dir} 时出错: {e}", file=sys.stderr)

# 清理临时文件
temp_patterns = ["*.tmp", "*.log", "*.pyc"]
for pattern in temp_patterns:
    try:
        result = subprocess.run(["find", ".", "-name", pattern, "-delete"], 
                              capture_output=True, text=True)
        if result.returncode == 0:
            print(f"已清理临时文件: {pattern}", file=sys.stderr)
    except:
        pass

# 保存会话统计信息
session_log = {
    "session_end_time": datetime.now().isoformat(),
    "project_path": os.getcwd(),
    "git_status": None
}

# 获取 Git 状态
try:
    result = subprocess.run(["git", "status", "--porcelain"], 
                          capture_output=True, text=True)
    if result.returncode == 0:
        session_log["git_status"] = "clean" if not result.stdout.strip() else "dirty"
        
        # 获取当前分支
        branch_result = subprocess.run(["git", "branch", "--show-current"], 
                                     capture_output=True, text=True)
        if branch_result.returncode == 0:
            session_log["current_branch"] = branch_result.stdout.strip()
except:
    pass

# 保存会话日志
log_dir = ".claude/logs"
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json")

try:
    with open(log_file, 'w', encoding='utf-8') as f:
        json.dump(session_log, f, indent=2, ensure_ascii=False)
    print(f"会话日志已保存: {log_file}", file=sys.stderr)
except Exception as e:
    print(f"保存会话日志时出错: {e}", file=sys.stderr)

# 输出标准响应
output = {
    "hookSpecificOutput": {
        "hookEventName": "SessionEnd"
    }
}
print(json.dumps(output))
sys.exit(0)

写在最后

Hook 系统是 Claude Code 最强大的功能之一,从简单的自动化任务到复杂的工作流程集成,hook的可能性是无限的。

在下篇文章中,我将通过丰富的实战案例,深入探讨如何设计和优化 hook,帮助你掌握 hook 开发的最佳实践,让 Claude Code 在你的开发工作中发挥更大的价值。

这篇文章将会收录到原创专栏《油菜花的Claude Code快速上手指南》,欢迎感兴趣的小伙伴关注,一起学习,一起进步!

❤️ 感谢阅读


❤️ 如果你也关注 AI 的发展现状,且对 AI 应用开发感兴趣,我会跟你分享大模型与 AI 领域的开源项目和应用,提供运行实例和实用教程,帮助你快速上手AI技术!也非常欢迎你通过公众号发消息加入我们!

❤️ 微信公众号|搜一搜:蚝油菜花

相关推荐
cpp加油站2 小时前
一个小工具,可以很方便的切换claude code模型
ai编程
AImatters2 小时前
2025 年PT展前瞻:人工智能+如何走进普通人的生活?
人工智能·ai·具身智能·智慧医疗·智慧出行·中国国际信息通信展览会·pt展
AI小书房3 小时前
【人工智能通识专栏】第十五讲:视频生成
人工智能
cpp加油站3 小时前
项目上线后,我发现一个残酷的事实:AI编程2.0时代,会写代码成了次要的能力
ai编程·trae
zzywxc7873 小时前
AI工具全景洞察:从智能编码到模型训练的全链路剖析
人工智能·spring·ios·prompt·ai编程
甄心爱学习3 小时前
DataSet-深度学习中的常见类
人工智能·深度学习
伟贤AI之路3 小时前
【分享】中小学教材课本 PDF 资源获取指南
人工智能·pdf
玲小珑3 小时前
LangChain.js 完全开发手册(八)Agent 智能代理系统开发
前端·langchain·ai编程
aneasystone本尊3 小时前
详解 Chat2Graph 的推理机实现
人工智能