第九节
Hooks 钩子系统
在工具执行前后运行命令,实现自动化工作流
Hooks(钩子)允许你在 Claude Code 尝试运行工具之前或之后执行命令。这个功能非常强大,可用于实现各种自动化工作流,例如在文件编辑后运行代码格式化、 在文件更改时执行测试,或阻止访问特定文件。
什么是 Hooks
在理解 hooks 之前,让我们先回顾一下正常流程。当你向 Claude Code 提出问题时, 你的查询会被发送到 Claude 模型以及工具定义。Claude 模型可能会决定运行一个工具, 然后由 Claude Code 执行该工具并返回结果。
Hooks 在这个过程中插入自身, 允许你在工具执行之前或之后执行代码。
PreToolUse Hooks
在工具执行之前运行
PostToolUse Hooks
在工具执行之后运行
Hook 配置
Hooks 在 Claude 设置文件中定义。你可以将它们添加到:
全局配置
~/.claude/settings.json项目共享配置
.claude/settings.json项目本地配置
.claude/settings.local.json配置方式
你可以手动在这些文件中编写 hooks,或在 Claude Code 中使用 /hooks 命令。
配置结构
配置文件包含两个主要部分:
PreToolUse 示例
{
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "node /home/hooks/read_hook.ts"
}
]
}
]
}在执行 Read 工具之前,运行指定的命令。
PostToolUse 示例
{
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node /home/hooks/edit_hook.ts"
}
]
}
]
}在执行 Write、 Edit 或 MultiEdit 工具之后,运行指定命令。
Matcher 说明:matcher 使用正则表达式匹配工具类型,支持多个工具用 | 分隔。
PreToolUse 能力
PreToolUse hooks 会接收 Claude 想要执行的工具调用的详细信息。你可以:
允许操作
正常继续执行工具调用
阻止操作
阻止工具调用并向 Claude 发送错误消息
检查能力:你可以检查 Claude 想要做什么,如果出于任何原因不想允许, 可以阻止该工具使用操作。
PostToolUse 能力
由于工具调用已经发生,PostToolUse hooks 无法阻止操作。 但它们可以:
执行后续操作
例如格式化刚编辑的文件
提供反馈
向 Claude 提供有关工具使用的额外信息
// 运行代码质量检查 // 运行类型检查 // 将结果反馈给 Claude // Claude 可能根据反馈更新刚写入的文件
构建 Hook
创建 hook 需要四个主要步骤:
决定使用 PreToolUse 还是 PostToolUse
PreToolUse hooks 可以阻止工具调用执行,PostToolUse hooks 在工具使用后运行
确定要监听的工具类型
需要指定哪些工具应该触发你的 hook
编写接收工具调用的命令
该命令通过标准输入获取关于工具调用的 JSON 数据
命令向 Claude 提供反馈(如需要)
命令的退出码告诉 Claude 是允许还是阻止操作
可用工具
Claude Code 提供多个内置工具,你可以用 hooks 监控它们:
ReadWriteEditMultiEditBashGrepListMcpResourcesToolReadMcpResourceTool查看可用工具
要确切查看当前设置中有哪些工具可用,你可以直接询问 Claude。 这特别有用,因为添加自定义 MCP 服务器时可用工具可能会变化。
工具调用数据结构
当 hook 命令执行时,Claude 通过标准输入发送 JSON 数据,包含关于建议工具调用的详细信息:
{
"session_id": "2d6a1e4d-6...",
"transcript_path": "/Users/sg/...",
"hook_event_name": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "/code/queries/.env"
}
}你的命令从标准输入读取此 JSON,解析它,然后根据工具名称和输入参数决定允许或阻止该操作。
退出码和控制流
Hook 命令通过退出码向 Claude 传递信息:
退出码 0
一切正常,允许工具调用继续执行
退出码 2
阻止工具调用(仅限 PreToolUse hooks)
错误反馈:当在 PreToolUse hook 中使用退出码 2 时,你在标准错误中写入的任何错误消息 都会作为反馈发送给 Claude,解释为什么操作被阻止。
设置 Hook 配置
首先,我们需要在设置文件中配置 hook。打开 .claude/settings.local.json 文件, 创建一个 PreToolUse hook,因为我们想在工具执行前拦截调用。
配置需要两个关键部分:
- Matcher - 指定要监听的工具
- Command - 工具调用时运行的脚本
1. Matcher 配置
对于 matcher,我们要捕获可能访问 .env 文件的 read 和 grep 操作:
"matcher": "Read|Grep"
管道符号 | 作为 OR 运算符, 因此这会在任一工具被调用时触发。
2. Command 配置
对于 command,我们指向一个 Node.js 脚本:
"command": "node ./hooks/read_hook.js"
完整配置示例:
{
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "node ./hooks/read_hook.js"
}
]
}
]
}理解工具调用数据
当 Claude 尝试使用工具时,你的 hook 会通过标准输入作为 JSON 接收关于该调用的详细信息。此数据包括:
Hook 脚本处理此数据,可以通过退出特定代码来允许操作继续或阻止它。
实现 Hook 脚本
Hook 脚本需要从标准输入读取工具调用数据,并检查 Claude 是否试图访问 .env 文件。以下是核心逻辑:
async function main() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const toolArgs = JSON.parse(Buffer.concat(chunks).toString());
// 提取 Claude 试图读取的文件路径
const readPath =
toolArgs.tool_input?.file_path ||
toolArgs.tool_input?.path ||
"";
// 检查 Claude 是否试图读取 .env 文件
if (readPath.includes('.env')) {
console.error("You cannot read the .env file");
process.exit(2);
}
// 允许其他操作
process.exit(0);
}
main();脚本说明:
读取标准输入
使用 async iterator 收集所有数据块
解析 JSON
将接收的数据转换为 JavaScript 对象
提取文件路径
从 tool_input 中获取 file_path 或 path
检查敏感文件
如果路径包含 .env,阻止并返回错误
退出码说明:当你以代码 2 退出时,Claude 会收到错误消息并理解操作被 hook 阻止。 这让 Claude 知道操作失败的原因,通常会提到 read hook 阻止了文件访问。
测试你的 Hook
保存配置和 hook 脚本后,重启 Claude Code 使更改生效。然后通过要求 Claude 读取 .env 文件来测试它。
测试步骤:
预期结果:当 Claude 尝试读取操作时,你的 hook 会拦截它并返回错误消息。 Claude 会识别出操作被阻止,并向你解释这一点,通常会提到 read hook 阻止了文件访问。 对于 grep 操作,同样的保护也有效 - 如果 Claude 尝试在 .env 文件中搜索,hook 也会阻止它。
核心优势
这种方法提供了几个优势:
主动保护
在敏感数据被读取之前阻止访问
透明操作
Claude 理解操作失败的原因
灵活匹配
适用于多种工具(read、grep 等)
清晰反馈
提供有意义的错误消息
可扩展性:虽然这个特定示例专注于 .env 文件,但相同的模式可以保护项目中的任何敏感文件或目录。 你可以扩展逻辑以检查多个文件模式,或根据安全要求实现更复杂的访问控制。
快速参考:完整示例
以下是保护 .env 文件的完整示例总结:
配置文件 (.claude/settings.local.json)
{
"PreToolUse": [{
"matcher": "Read|Grep",
"hooks": [{
"type": "command",
"command": "node ./hooks/read_hook.js"
}]
}]
}Hook 脚本 (./hooks/read_hook.js)
async function main() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const toolArgs = JSON.parse(Buffer.concat(chunks).toString());
const readPath = toolArgs.tool_input?.file_path ||
toolArgs.tool_input?.path || "";
if (readPath.includes('.env')) {
console.error("不能读取 .env 文件");
process.exit(2);
}
process.exit(0);
}
main();更多应用场景
以下是使用 hooks 的更多方式:
代码格式化
在 Claude 编辑后自动格式化文件
自动测试
文件更改时运行测试
访问控制
阻止 Claude 读取或编辑特定文件
代码质量
运行 linter 或类型检查器
日志记录
跟踪 Claude 访问或修改的文件
验证检查
检查命名约定或编码标准
核心价值:Hooks 让你能够通过将自己的工具和流程集成到工作流中, 来扩展 Claude Code 的能力。PreToolUse hooks 让你控制 Claude 能做什么, 而 PostToolUse hooks 让你增强 Claude 已经做的事情。
注意事项与最佳实践
在使用 hooks 时,你可能会遇到一些需要注意的情况:
设置文件问题
运行 npm run dev 后, 你可能会注意到 .claude 目录中有两个 settings.json 文件。
.claude/ ├── settings.json # 项目共享配置 └── settings.local.json # 个人本地配置
这两个文件有不同的用途:settings.json 用于团队共享,而 settings.local.json 用于个人配置(不提交到版本控制)。
安全建议:使用绝对路径
Claude Code 文档建议使用绝对路径(而非相对路径) 来引用脚本。这有助于减轻路径拦截和二进制植入攻击。
安全风险:相对路径可能被利用来执行意外的脚本。始终使用绝对路径来指定 hook 脚本位置。
// 推荐使用绝对路径
"command": "/absolute/path/to/hooks/read_hook.js" // ✅ 安全 "command": "./hooks/read_hook.js" // ❌ 不安全
共享配置的挑战
使用绝对路径带来的一个挑战是,它使得共享 settings.json 文件变得更加困难。 原因很简单:你机器上任何 hook 脚本的绝对路径很可能与我的机器不同, 只是因为我们可能会将项目放在不同的目录中。
问题:
你的路径: /Users/username/projects/my-app/hooks/read_hook.js
我的路径: /Users/othername/projects/my-app/hooks/read_hook.js
为了解决这个问题,我们的项目使用了一个 settings.example.json 文件。
解决方案:设置脚本
在 settings.example.json 文件中,脚本引用包含 $PWD 占位符。
// settings.example.json
{
"PreToolUse": [{
"matcher": "Read|Grep",
"hooks": [{
"type": "command",
"command": "node $PWD/hooks/read_hook.js"
}]
}]
}当我们运行 npm run setup 时:
巧妙之处:这个脚本让我们能够共享 settings.json 文件,但仍然使用推荐的绝对路径! 每个开发者运行 setup 后,都会获得适合其本地环境的配置。
最佳实践总结
实用的 Hook 示例
Claude Code hooks 可以帮助解决 AI 辅助开发中的常见弱点,特别是在大型项目中。 这些 hooks 在 Claude 修改代码时自动运行,提供即时反馈并防止常见问题。
TypeScript 类型检查 Hook
最有用的 hook 之一解决了一个基本问题:当 Claude 修改函数签名时, 它通常不会更新项目中调用该函数的所有位置。
例如:如果你要求 Claude 在 schema.ts 中为函数添加一个 verbose 参数, 它会成功更新函数定义,但会遗漏 main.ts 中的调用点。这会创建 Claude 不会立即捕获的类型错误。
解决方案:
一个在每次文件编辑后运行 TypeScript 编译器的 post-tool-use hook:
// Hook 配置示例
{
"PostToolUse": [{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{
"type": "command",
"command": "npm run type-check"
}]
}]
}扩展性:这个 hook 适用于任何可以运行类型检查器的类型化语言。 对于非类型化语言,你可以使用自动化测试实现类似功能。
查询重复预防 Hook
在包含许多数据库查询的大型项目中,Claude 有时会创建重复功能而不是重用现有代码。 当你给 Claude 复杂的多步骤任务时,这尤其成问题,其中数据库操作只是一个组件。
场景示例:考虑一个项目结构,其中有多个查询文件,每个文件包含许多 SQL 函数。 当你要求 Claude "创建一个 Slack 集成,提醒超过 3 天未处理的订单" 时, 它可能会编写一个新查询,而不是使用现有的 getPendingOrders() 函数。
查询重复 hook 实现审查流程:
// Hook 配置示例
{
"PostToolUse": [{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{
"type": "command",
"command": "node ./hooks/check-duplicates.js"
}]
}]
}实现考虑
两个 hooks 都使用 pre-tool-use 或 post-tool-use hook 系统。 TypeScript hook 相对轻量级且运行快速。查询重复 hook 需要更多资源, 因为它为每次审查启动单独的 Claude 实例。
查询 hook 的权衡:
优势
更清洁的代码库,减少重复
成本
额外的 API 使用和审查时间
建议
仅监控关键目录以最小化开销
技术实现:这些 hooks 使用 Claude 的 TypeScript SDK 以编程方式与 AI 交互。 这允许你创建复杂的工作流,其中一个 Claude 实例可以审查并提供对另一个工作的反馈。
扩展这些概念
这些 hooks 演示了你可以应用于自己项目的更广泛原则:
使用编译器/检查器输出
提供即时反馈
实现代码审查流程
使用单独的 AI 实例
专注监控高价值目录
一致性最重要的地方
平衡自动化与性能
权衡自动化收益与性能成本
核心要点:关键是识别开发工作流程中的具体痛点,并创建针对性的 hooks, 这些 hooks 自动解决这些问题。从简单开始,根据实际需求逐步增加复杂性。