❤️ 如果你也关注 AI 的发展现状,且对 AI 应用开发感兴趣,我会跟你分享大模型与 AI 领域的开源项目和应用,提供运行实例和实用教程,帮助你快速上手AI技术!也非常欢迎你通过公众号发消息加入我们!
❤️ 微信公众号|搜一搜:蚝油菜花
在AI编程助手的世界里,Claude Code以其强大的功能和灵活性脱颖而出。而其中最令人眼前一亮的功能之一,就是hook(钩子)系统。今天,我将深入探讨这个让 Claude Code 变得更加智能和可控的核心功能。
Hook这部分的内容高达两万字,篇幅较长,所以我将这一部分内容分为上下两篇,建议先收藏再慢慢消化!
- 上篇:引入 hook 概念,一步步地跟随文章带你学会创建自己的 hook,循序渐进地深入了解 hook 系统。
- 下篇:以具体的 hook 示例和解析为核心内容,了解 hook 的最佳实践方法,丰富你的实战经验,学会优化 hook。
🌟 如果你还不知道什么是 Claude Code,或者你还想知道怎么安装和快速上手,可以阅读前文:
- 一、《油菜花的Claude Code快速上手指南》--- 安装与运行 Claude Code
- 二、将GLM 4.5接入Claude Code,打造最具性价比的AI工程师
- 三、Claude Code 核心命令详解,让开发效率飙升10倍!
- 四、详解Claude Code的"大脑":CLAUDE.md让AI记住你的项目
- 五、详解Claude Code子代理功能,用AI打造私人专业团队
什么是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

命令解析:
jq -r
:jq
是一个强大的 JSON 处理工具。
-r
参数表示解析 JSON 数据后输出为字符串。
- jq 工具的过滤表达式:
'"\(.tool_input.command) - \(.tool_input.description // "No description")'
。
\(.tool_input.command)
:提取 JSON 中的命令名称。\(.tool_input.description // "No description")
:提取命令描述,如果没有描述则展示"No description"。
>> ~/.claude/bash-command-log.txt
:将结果追加到用户主目录下的.claude/bash-command-log.txt
文件中。
步骤4:保存配置
选择User settings
作为存储位置,那么这个 hook 的配置将会被保存到用户级配置~/.claude/settings.json
中,它将在全局生效并应用到所有项目。这里仅简单提及下,在后续章节我会详细介绍 hook 配置的存储位置和优先级。

步骤5:测试创建的hook
使用 Claude Code 运行简单的命令,比如 ls
,当执行命令时,这个 hook 会被触发并执行以下操作:
- 接收 Claude Code 传递的 JSON 数据,例如:
json
{
"tool_input": {
"command": "ls -la",
"description": "列出当前目录的详细信息"
}
}
jq
工具依照过滤表达式'"\(.tool_input.command) - \(.tool_input.description // "No description")'
,提取 JSON 数据中的关键信息并格式化为字符串,生成类似这样的日志记录:
plain
ls -la - 列出当前目录的详细信息
- 追加到日志文件
.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
- 文件读取Edit
、MultiEdit
- 文件编辑Write
- 文件写入WebFetch
、WebSearch
- 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 发送通知时运行。
一般出现以下情况时会发送通知:
- Claude需要权限使用工具:"Claude needs your permission to use Bash"
- 提示输入已空闲至少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执行时间过长
- "hooks" :具体要执行的 hook 列表
Hook 的匹配条件---matcher
匹配器(matcher)是 hook 的"匹配条件",决定是否执行 hook。matcher
字段需要填写匹配的工具名称,注意区分大小写:
匹配类型 | 示例 | 含义 |
---|---|---|
精确匹配 | "matcher": "Write" |
只有当 Claude Code 要使用 Write 工具时才触发 |
正则表达式匹配 | `"matcher": "Edit | Write"` |
正则表达式匹配 | "matcher": "Notebook.*" |
匹配所有以"Notebook"开头的工具 |
通配符匹配 | "matcher": "*" |
Claude Code 使用任何工具都会触发 |
🌟 小贴士 :目前 matcher
字段仅适用于 PreToolUse
和 PostToolUse
。对于像 UserPromptSubmit
、Notification
、Stop
和 SubagentStop
这样不需要匹配器的事件,您可以省略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_input
和 tool_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
传递给PreCompact
hook。例如,用户可能会执行/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
,这防止提示被处理。 - 对于
Stop
和SubagentStop
,这优先于任何"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 的
decision
和reason
字段已弃用。 请使用hookSpecificOutput.permissionDecision
和hookSpecificOutput.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"
}
}
具体行为说明:
-
decision: "block"
- Claude Code 会继续正常工作,不会停止。如果需要真正停止 Claude Code 的工作,应该使用
"continue": false
参数。 reason
中的信息会作为反馈传递给 Claude Code。- Claude Code 会根据反馈信息调整后续行为。
- 适用于需要告知 Claude Code 工具执行结果或异常原因的场景。
- Claude Code 会继续正常工作,不会停止。如果需要真正停止 Claude Code 的工作,应该使用
-
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"
}
}
具体行为说明:
- decision: "block"
- 完全阻止 Claude Code 处理用户提示
- 提示的内容会从对话上下文中被擦除
reason
信息仅向用户展示,不会展示给 Claude Code- 适用于内容过滤、权限控制或提示验证失败的场景
- 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 代理任务停止时的决策控制
Stop
和 SubagentStop
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"
}
具体行为说明:
-
decision: "block"
- 阻止 Claude Code 停止当前任务
- 必须提供
reason
告诉 Claude Code 为什么需要继续以及如何继续 - Claude Code 会根据
reason
中的提示继续工作 - 适用于任务未完成、需要额外步骤的场景
-
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技术!也非常欢迎你通过公众号发消息加入我们!
❤️ 微信公众号|搜一搜:蚝油菜花