> ## Documentation Index
> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 使用 mod 响应事件

> 从 mod 处理 Claude Code 事件：观察、重写或回答工具调用、提示和轮次，过滤 hook 处理的事件，并为其他 mod 做计划。

hook 是一个事件处理程序：Claude Code 在命名事件发生时运行的函数。Claude Code 在即将采取行动的每个点触发事件，例如当它运行工具、提交提示、向模型发送请求或启动或结束会话时。你的 hook 在 Claude Code 采取行动之前运行，因此它可以观察事件、重写事件或代替 Claude Code 回答事件。你使用 [`on(eventName, handler)`](/docs/zh-CN/plugins/mods/reference#the-hook-function) 注册 hook。

在开始之前，请构建你的[第一个 mod](/docs/zh-CN/plugins/mods/create)。对于每个事件及其确切字段，请参阅[参考](/docs/zh-CN/plugins/mods/reference#events)或阅读[你的构建的类型](/docs/zh-CN/plugins/mods/create#get-the-types-for-your-build)。

<h2 id="how-a-hook-handles-an-event">
  hook 如何处理事件
</h2>

hook 位于事件和 Claude Code 对其采取的行动之间，因此它可以观察事件、重写事件或自己回答事件。它接收三个参数：[mods API](/docs/zh-CN/plugins/mods/api) 作为 `$`、事件作为 `e` 和下一个处理程序作为 `next`。事件的处理程序形成中间件链。`next(e)` 调用下一个处理程序，这是另一个 mod 的 hook 或链末端的 Claude Code 自己的行为，它解析为结果。你的 hook 对 `next` 做什么决定了它做以下三件事中的哪一件。

<h3 id="observe-an-event">
  观察事件
</h3>

要观察事件而不改变它，请执行你的工作并返回 `next(e)`。此 hook 记录 Claude 即将使用的每个工具：

```javascript theme={null}
on('tool.call', async ($, e, next) => {
  // 在工具运行之前运行
  $.ui.log('Claude is about to use ' + e.tool)
  // 原样传递事件
  return next(e)
})
```

在每个工具运行之前，转录中会出现一条暗线，例如 `● my-mod: Claude is about to use Bash`，其中 `my-mod` 是你的插件的名称。工具的运行方式与没有 mod 时相同。

要在事件后采取行动，请 `await next(e)`、执行你的工作并返回结果。此 hook 在每个工具运行后记录它：

```javascript theme={null}
on('tool.call', async ($, e, next) => {
  // 让工具运行，并等待其结果
  const result = await next(e)
  // 在工具运行后运行
  $.ui.log(e.tool + ' finished')
  // 原样返回结果
  return result
})
```

该行现在出现在每个工具完成后。Claude 读取相同的结果，因为 hook 返回 `next(e)` 解析的内容。

<h3 id="rewrite-an-event">
  重写事件
</h3>

要更改 Claude Code 作用的内容，例如提示的文本，请使用修改后的事件副本调用 `next`。事件本身是不可变的：它在每个深度都被冻结，分配给字段会抛出错误。此 hook 在发送前修剪每个提示：

```javascript theme={null}
on('prompt.submit', async ($, e, next) => {
  // 传递事件的副本，其文本已更改
  return next({ ...e, text: e.text.trim() })
})
```

后续处理程序和 Claude Code 接收修剪后的提示，永远看不到原始提示。你也可以更改结果：`await next(e)`，然后返回替换了字段的结果副本。

<h3 id="answer-an-event">
  回答事件
</h3>

要自己处理事件，请返回结果而不调用 `next`。这会短路链，因此后续 mod 和 Claude Code 自己的行为不会运行。此 hook 拒绝每个 Bash 命令：

```javascript theme={null}
on('tool.call', { tool: 'Bash' }, async () => {
  // 没有调用 next，所以命令永远不会运行
  return { deny: 'Bash is turned off in this project. Use the file tools.' }
})
```

当 Claude 尝试 Bash 命令时，命令不会运行，Claude 将 `deny` 文本读作工具的结果。每个事件都有自己的结果形状，[事件参考](/docs/zh-CN/plugins/mods/reference#events)列出了这些。

<h3 id="filter-which-events-a-hook-handles">
  过滤 hook 处理的事件
</h3>

要仅为某些事件运行 hook，请将过滤器作为第二个参数传递给 `on`。Claude Code 将过滤器称为 matcher。它是一个对象，其字段与事件的字段进行比较，只有当每个字段都匹配时，hook 才会运行。字段可以是值、允许值的数组或正则表达式。

此示例中的每一行都为更窄的工具调用集合注册相同的函数 `hook`：

```javascript theme={null}
// 字符串匹配一个值：仅 Bash 调用
on('tool.call', { tool: 'Bash' }, hook)
// 数组匹配其中任何值：Edit 调用和 Write 调用
on('tool.call', { tool: ['Edit', 'Write'] }, hook)
// 正则表达式按模式匹配：来自一个 MCP 服务器的每个工具
on('tool.call', { tool: /^mcp__github__/ }, hook)
```

`hook` 为 Bash、Edit 或 Write 调用各运行一次，为名称以 `mcp__github__` 开头的工具调用运行一次。对任何其他工具（如 Read）的调用都不匹配这三个中的任何一个，因此 `hook` 不会为它运行。

事件名称可以是通配符。`'classic.*'` 匹配每个[设置 hook 事件](#hook-the-settings-hook-events)。`'*'` 匹配除[遥测事件](/docs/zh-CN/plugins/mods/reference#telemetry)之外的每个事件，你可以按名称或作为 `'telemetry.*'` 来 hook 这些事件。

为每个 matcher 注册一次事件。如果你为 `session.start` 调用 `on` 两次而没有 matcher，模块将无法加载，错误为 `on("session.start") is registered twice without a matcher`。将你的 mod 在会话启动时执行的所有操作放在一个 hook 中。

<h2 id="hook-what-claude-is-doing">
  Hook Claude 正在做的事情
</h2>

Hook 这些事件以查看或更改工具调用、提示或轮次。对于每个事件以及 hook 可以返回的内容，请参阅[事件参考](/docs/zh-CN/plugins/mods/reference#events)。

<h3 id="guard-or-change-a-tool-call">
  保护或更改工具调用
</h3>

`tool.call` hook 看到 Claude 即将使用的每个工具，因此它可以拒绝调用、更改其参数或让其通过。`tool.call` 在 Claude Code 即将运行工具时触发，包括子代理进行的调用和对 MCP 工具的调用。`e.tool` 是工具的名称，工具的参数是 `e` 的字段，例如 Bash 的 `e.command`。当你调用 `next(e)` 时，Claude Code 运行权限检查，然后运行工具。

此 hook 拒绝强制推送的 Bash 命令，并告诉 Claude 原因：

```javascript theme={null}
// matcher 将 hook 限制为 Bash 调用，因此 e.command 是 shell 命令
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // 返回而不调用 next 会回答事件，所以命令永远不会运行
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  // 每个其他命令都会进行权限检查，然后进行 Bash
  return next(e)
})
```

当 Claude 尝试 `git push --force` 时，命令不会运行，也不会出现权限提示，因为 hook 永远不会调用 `next`。Claude 将 `deny` 文本读作工具的结果，因此将其写成 Claude 可以采取行动的指令。每个其他 Bash 命令的运行方式与没有 mod 时相同。

要在工具运行后采取行动，请 `await next(e)`、执行你的工作并返回 `next` 给你的内容。此 hook 记录 Claude 更改的每个 `.mdx` 文件，使用 [`$.ui.log`](/docs/zh-CN/plugins/mods/api#show-something-without-starting-a-turn)，它向转录中添加一条暗线，Claude 不会读取：

```javascript theme={null}
on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
  // 等待权限检查和工具，并保留它们生成的内容
  const result = await next(e)
  // 被拒绝的调用返回为 { deny }，失败的调用设置了 isError
  const changed = !result.deny && !result.isError
  if (changed && e.file_path.endsWith('.mdx')) $.ui.log('Claude changed ' + e.file_path)
  // 原样返回结果，所以 Claude 读取工具返回的内容
  return result
})
```

Claude 编辑或写入 `.mdx` 文件后，转录中的暗线会命名该文件。对于另一种文件或被拒绝或失败的调用，不会记录任何内容。Claude 对调用的看法不会改变，因为 hook 返回它接收的结果。

要更改调用，请将更改的参数传递给 `next`。要重试调用，请再次调用 `next(e)`：看到第一个结果上的 `isError` 的 hook 可以第二次运行工具并返回该结果。要自己回答调用，请返回带有 `result` 字段的对象，例如 `{ result: 'Skipped by my-mod' }`，而不调用 `next`。当你这样做时，不会出现权限提示，工具不会运行，因此你返回的结果是 Claude 了解发生了什么的全部内容。

你的组织的[托管设置](/docs/zh-CN/server-managed-settings)中的 hooks 在任何 mod 的 `tool.call` hook 之前运行，其中一个的块是最终的。

<h4 id="hold-a-tool-call-until-the-user-decides">
  保持工具调用直到用户决定
</h4>

hook 可以暂停工具调用并在继续之前询问用户该怎么做。`tool.call` hook 可以在调用 `next` 或返回之前 `await`，工具调用保持待处理状态直到那时。要向用户提出问题，请调用 `$.ui.ask`。它在 Claude 用来问你的对话框中的编号列表上方显示你的问题，并解析为用户选择的标签。在你的选项之后，对话框添加一行用于输入不同的答案和一个**聊天此问题**行。

此示例中的 `RISKY` 模式匹配 `rm -r`、`rm -rf`、`git reset --hard` 和带有 `--force` 的 `git push`，它会错过其他拼写，例如 `git push -f`。此模块在运行与模式匹配的 Bash 命令之前询问：

```javascript theme={null}
const RISKY = /\brm\s+-rf?\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force/

export function register(on) {
  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
    // 让每个其他命令通过而不提问
    if (!RISKY.test(e.command)) return next(e)
    // 从安全答案开始，所以没有人回答的问题会拒绝命令
    let answer = 'Refuse'
    try {
      // 工具调用在这里等待，直到用户选择两个标签之一
      answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
    } catch {
      // 用户关闭了问题，或这是一个 claude -p 运行，没有人可以问
    }
    if (answer !== 'Run it') {
      // 回答而不调用 next，所以命令不会运行
      return { deny: 'The user declined this command. Ask before trying a different approach.' }
    }
    return next(e)
  })
}
```

当 Claude 尝试诸如 `rm -rf build` 的命令时，问题会出现，命令会等待答案：

* **用户选择 Run it**：hook 调用 `next(e)`，通常的权限检查仍然在之后运行
* **用户选择 Refuse**：命令不会运行，Claude 读取 `deny` 文本
* **用户输入答案**：`$.ui.ask` 解析为输入的文本。hook 将其与 `Run it` 进行比较，因此任何其他文本都会拒绝命令。
* **没有人回答**：当用户关闭问题或选择**聊天此问题**时，`$.ui.ask` 会拒绝，在 `claude -p` 运行中也是如此，因此 `catch` 块将答案保留在 `Refuse`

将等待保持在 mods API 调用（如 `$.ui.ask`）内，因为该时间不计入 hook 的[10 秒时间限制](/docs/zh-CN/plugins/mods/reference#limits)。花在等待你自己的承诺上的时间确实计入。Claude Code 跳过超时的 hook，因此保持的命令会运行。

<h3 id="rewrite-or-add-to-a-prompt">
  重写或添加到提示
</h3>

`prompt.submit` hook 在轮次开始之前看到每个提示，因此它可以重写文本或添加到其中。`e.text` 是输入的内容。

| 要执行此操作 | 返回此内容 |
| :- | :- |
| 重写提示。转录中的消息显示新文本。 | `next({ ...e, text: newText })` |
| 仅添加 Claude 读取的文本，在提示之后 | `next({ ...e, context: [...(e.context ?? []), extraText] })` |
| 停止发送提示 | `{ drop: 'the reason' }` |

此 hook 在提示提及拉取请求时为 Claude 添加当前分支名称：

```javascript theme={null}
on('prompt.submit', async ($, e, next) => {
  // 原样传递不提及拉取请求的提示
  if (!/\bPR\b|pull request/i.test(e.text)) return next(e)
  const git = await $.process.run(['git', 'branch', '--show-current'])
  // 在 git 存储库外，命令失败，因此没有分支可添加
  if (git.exitCode !== 0) return next(e)
  // 保留早期 hook 添加的任何上下文，并为 Claude 添加一行
  return next({ ...e, context: [...(e.context ?? []), 'Current branch: ' + git.stdout.trim()] })
})
```

当你发送诸如 `open a PR for this change` 的提示时，你的消息在转录中看起来相同，Claude 也会在其后读取诸如 `Current branch: feature/auth` 的行。不提及拉取请求的提示会原样通过，`git` 不会运行。

[其他事件](/docs/zh-CN/plugins/mods/reference#prompts-and-what-claude-reads)涵盖 Claude 读取的其余内容：`prompt.section` 用于系统提示的每个部分，`prompt.context` 用于与第一条消息一起发送的上下文，`skill.prompt` 用于技能的文本。来自这些 hook 的文本在请求之间更改时会[使提示缓存失效](/docs/zh-CN/prompt-caching)。

<h3 id="follow-a-turn">
  跟踪轮次
</h3>

轮次是 Claude 为回答一个提示而做的所有事情。Hook `turn.start`、`turn.step` 和 `turn.complete` 来跟踪一个：

| 事件 | 何时触发 | hook 可以做什么 |
| :- | :- | :- |
| `turn.start` | 轮次开始 | 观察。`e.turnId` 在其他两个事件中标识轮次。 |
| `turn.step` | Claude Code 即将向模型发送一个请求。具有工具调用的轮次有多个。`e.agentId` 为子代理的请求设置。 | 读取每个请求的令牌使用情况，使用 `next({ ...e, model })` 将其发送到不同的模型，或在不调用模型的情况下回答 |
| `turn.complete` | 轮次结束，包括用户中断的轮次，其中 `e.isAborted` 为 `true`。`e.answer` 是 Claude 的最终文本，`e.durationMs` 是花费的时间，`e.usage` 是轮次的令牌总数。子代理的轮次使用 `e.agentId` 设置触发它。 | 观察，或返回带有 `text` 字段的对象，例如 `{ text: 'Done in 12 seconds' }`，以在答案下显示一行 |

将 `turn.step` hook 写成异步生成器，因为事件流。`yield* next(e)` 在流式传输时转发响应并评估为完成的结果。此 hook 记录每个请求中 Claude API 从[提示缓存](/docs/zh-CN/prompt-caching)提供的数量：

```javascript theme={null}
// function* 使 hook 成为生成器，可以逐块传递响应
on('turn.step', async function* ($, e, next) {
  // 发送请求，在每个片段到达时转发它，并保留完成的结果
  const result = yield* next(e)
  // 跳过不报告令牌计数的结果
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  // 原样返回结果，所以轮次照常继续
  return result
})
```

Claude 的响应流式传输到屏幕，就像没有 mod 时一样。每个请求完成后，转录中的暗线给出从缓存读取的令牌数和写入的令牌数。具有工具调用的轮次有多个请求，因此它添加多行。

`result.usage` 保存 Claude API 为请求报告的四个令牌计数，加上回答的 `model`：`input_tokens`、`output_tokens`、`cache_read_input_tokens` 和 `cache_creation_input_tokens`。hook 也为子代理的请求运行，因此当你只想要主对话时检查 `e.agentId`。

<h3 id="hook-the-settings-hook-events">
  Hook 设置 hook 事件
</h3>

设置 hooks 是你在设置文件中配置的命令、HTTP、提示和代理 hooks。每个[设置 hook 事件](/docs/zh-CN/hooks#hook-events)，例如 `Stop`、`SessionEnd` 或 `PostToolUse`，也是一个名为 `classic.` 后跟设置 hook 事件名称的事件，例如 `classic.Stop`。`e` 是设置 hook 在 stdin 上接收的 JSON，包括 `transcript_path`。

此 hook 使用 `Stop`（在 Claude 完成响应时触发）来记录会话的转录保存位置：

```javascript theme={null}
on('classic.Stop', async ($, e, next) => {
  // e 具有设置文件中的 Stop hook 从 stdin 读取的相同字段
  $.ui.log('Transcript saved at ' + e.transcript_path)
  // 传递事件，所以你的设置文件中的 Stop hooks 仍然运行
  return next(e)
})
```

每次 Claude 完成响应时，转录中的暗线都会给出转录文件的路径。hook 返回 `next(e)`，因此它观察事件并不改变轮次的结束方式。

<h2 id="run-alongside-other-mods">
  与其他 mod 一起运行
</h2>

多个 mod 可以 hook 同一事件，其中任何一个都可能失败。如果你的 mod 阻止工具调用，请检查它在链中的位置以及当其 hook 失败时会发生什么。

<h3 id="the-order-mods-run-in">
  mod 运行的顺序
</h3>

同一事件上的 hooks 形成一个中间件链。每个 mod 的 `next` 调用以下 mod 的 hook，最后的 `next` 到达 Claude Code 自己的行为。第一个 mod 是最外层的：它在其他 mod 之前看到事件，在它们之后看到结果，并决定其他 mod 是否运行。后续 mod 无法阻止早期 mod 看到事件。

Claude Code 按每个 mod 的来源对链进行排序：

1. 内置保护 `sec-default@builtin`，一个内置于 Claude Code 的 mod，`/plugin` 列为 `cc-plugin-sec-default`，其中[它加载](/docs/zh-CN/plugins/mods/admin#know-what-happens-by-default)，你的组织在 [`prependPlugins`](/docs/zh-CN/plugins/mods/admin#install-your-organizations-mods) 中列出的 mod，然后是任何其他计为你的组织的 mod，不在 `appendPlugins` 中
2. 你安装的 mod
3. 你的组织在 `appendPlugins` 中列出的 mod
4. 其他内置于 Claude Code 的 mod

在你安装的 mod 中，mod 在它在清单中的 `dependencies` 下列出的 mod 之前运行。在一个模块中，hooks 按 `register` 调用 `on` 的顺序运行。

<h4 id="where-settings-hooks-run-in-the-order">
  设置 hooks 在顺序中运行的位置
</h4>

在设置文件中配置的 `PreToolUse` hooks 也在工具调用期间运行，在 mod 链中的固定点：

* **来自托管设置的 `PreToolUse` hooks**：在第一个 mod 的 `tool.call` hook 之前运行，其中一个的块是最终的，因此没有 mod 看到调用。
* **来自每个其他设置文件和插件的 `hooks/hooks.json` 的 `PreToolUse` hooks**：在最后一个 mod 调用 `next` 后运行，作为 Claude Code 自己的行为的一部分。回答 `tool.call` 而不调用 `next` 的 mod 会阻止它们运行，调用 `next` 的 mod 在它返回的结果中看到它们的决定。

[`tool.check`](/docs/zh-CN/plugins/mods/reference#tools) 是 Claude Code 决定是否允许工具调用运行的事件。它在这些 hooks 和权限规则决定后触发，`next(e)` 解析为它们的决定。`tool.check` 上的 hook 可以返回不同的决定，例如 `{ decision: 'allow' }`，因此它可以批准第二组中的 hook 阻止的调用。[使用 hooks 扩展权限](/docs/zh-CN/permissions#extend-permissions-with-hooks)列出哪些决定对 mod 有效。

<h3 id="handle-a-hook-that-fails">
  处理失败的 hook
</h3>

失败的 hook 不会破坏会话，你可以决定接下来会发生什么。当没有 `.catch` 处理程序的 hook 抛出、超时或返回错误形状的结果时，接下来会发生什么取决于它是否调用了 `next`：

* **它在调用 `next` 之前失败**：Claude Code 跳过它，下一个处理程序代替运行
* **它在 `next` 解析后失败**：该结果成立，没有任何东西运行第二次

一行命名 mod、事件和原因，例如 `my-mod: tool.call hook skipped: threw Error: boom`。你读取它的位置取决于会话，如[找出 mod 为什么不做任何事](/docs/zh-CN/plugins/mods/troubleshoot#find-out-why-a-mod-does-nothing)列出的。其绘图不验证的 `ui.render` hook 的报告方式不同，如[从元素构建树](/docs/zh-CN/plugins/mods/interface#build-a-tree-from-elements)所述。

要使阻止调用的 hook 失败关闭，请添加一个 `.catch` 错误处理程序来代替回答。这里，`guard` 是你的 hook 函数：

```javascript theme={null}
// on 返回一个注册，.catch 将处理程序附加到该 hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind 是 'throw' 或 'timeout'，说明 guard 如何失败
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})
```

当 `guard` 工作时，处理程序永远不会运行。当 `guard` 在 Bash 调用上抛出或超时时，Claude Code 使用相同的事件调用处理程序。处理程序返回 `{ deny }`，所以命令不会运行，Claude 读取末尾带有 `throw` 或 `timeout` 的文本。没有处理程序，Claude Code 会跳过 `guard` 并运行命令。处理程序有[一秒](/docs/zh-CN/plugins/mods/reference#limits)来回答。

<h2 id="next-steps">
  后续步骤
</h2>

* [使用 mods API](/docs/zh-CN/plugins/mods/api)：添加命令和工具、调用模型并在计时器上运行工作
* [在界面中绘制](/docs/zh-CN/plugins/mods/interface)：在窗格中或提示上方显示你的 hooks 收集的内容
* [测试 mod](/docs/zh-CN/plugins/mods/test)：从测试中触发这些事件中的任何一个
* [Mods 参考](/docs/zh-CN/plugins/mods/reference)：每个事件、每个 mods API 方法和限制
