第九节

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 需要四个主要步骤:

1

决定使用 PreToolUse 还是 PostToolUse

PreToolUse hooks 可以阻止工具调用执行,PostToolUse hooks 在工具使用后运行

2

确定要监听的工具类型

需要指定哪些工具应该触发你的 hook

3

编写接收工具调用的命令

该命令通过标准输入获取关于工具调用的 JSON 数据

4

命令向 Claude 提供反馈(如需要)

命令的退出码告诉 Claude 是允许还是阻止操作

可用工具

Claude Code 提供多个内置工具,你可以用 hooks 监控它们:

Read
Write
Edit
MultiEdit
Bash
Grep
ListMcpResourcesTool
ReadMcpResourceTool

查看可用工具

要确切查看当前设置中有哪些工具可用,你可以直接询问 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

退出码 0

一切正常,允许工具调用继续执行

2

退出码 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 接收关于该调用的详细信息。此数据包括:

Session ID 和 transcript 路径
Hook 事件名称(在我们的例子中是 PreToolUse)
工具名称(Read、Grep 等)
工具输入参数,包括文件路径

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 文件来测试它。

测试步骤:

1
重启 Claude Code 以加载新的 hook 配置
2
向 Claude 请求:'请读取 .env 文件的内容'
3
观察 Claude 的响应 - 它应该报告访问被阻止
4
尝试其他文件操作 - 应该正常工作

预期结果:当 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 时:

安装一些依赖项
运行 scripts 目录中的 init-claude.js 脚本
该脚本会用项目的绝对路径替换 $PWD 占位符
复制 settings.example.json 文件并将其重命名为 settings.local.json

巧妙之处:这个脚本让我们能够共享 settings.json 文件,但仍然使用推荐的绝对路径! 每个开发者运行 setup 后,都会获得适合其本地环境的配置。

最佳实践总结

使用绝对路径: 避免相对路径的安全风险
使用占位符: 在示例配置中使用 $PWD 等占位符
创建设置脚本: 自动生成适合本地环境的配置
分离配置文件: settings.json 共享,settings.local.json 个人使用

实用的 Hook 示例

Claude Code hooks 可以帮助解决 AI 辅助开发中的常见弱点,特别是在大型项目中。 这些 hooks 在 Claude 修改代码时自动运行,提供即时反馈并防止常见问题。

TypeScript 类型检查 Hook

最有用的 hook 之一解决了一个基本问题:当 Claude 修改函数签名时, 它通常不会更新项目中调用该函数的所有位置。

例如:如果你要求 Claude 在 schema.ts 中为函数添加一个 verbose 参数, 它会成功更新函数定义,但会遗漏 main.ts 中的调用点。这会创建 Claude 不会立即捕获的类型错误。

解决方案:

一个在每次文件编辑后运行 TypeScript 编译器的 post-tool-use hook:

运行 tsc --noEmit 检查类型错误
捕获发现的任何错误
立即将错误反馈给 Claude
提示 Claude 修复其他文件中的问题

// Hook 配置示例

{
  "PostToolUse": [{
    "matcher": "Write|Edit|MultiEdit",
    "hooks": [{
      "type": "command",
      "command": "npm run type-check"
    }]
  }]
}

扩展性:这个 hook 适用于任何可以运行类型检查器的类型化语言。 对于非类型化语言,你可以使用自动化测试实现类似功能。

查询重复预防 Hook

在包含许多数据库查询的大型项目中,Claude 有时会创建重复功能而不是重用现有代码。 当你给 Claude 复杂的多步骤任务时,这尤其成问题,其中数据库操作只是一个组件。

场景示例:考虑一个项目结构,其中有多个查询文件,每个文件包含许多 SQL 函数。 当你要求 Claude "创建一个 Slack 集成,提醒超过 3 天未处理的订单" 时, 它可能会编写一个新查询,而不是使用现有的 getPendingOrders() 函数。

查询重复 hook 实现审查流程:

1
当 Claude 修改 ./queries 目录中的文件时触发
2
以编程方式启动单独的 Claude Code 实例
3
要求第二个实例审查更改并检查类似的现有查询
4
如果发现重复,向原始 Claude 实例提供反馈
5
提示 Claude 删除重复项并使用现有功能

// 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 自动解决这些问题。从简单开始,根据实际需求逐步增加复杂性。