创建一个 mod
让 Claude 从描述中编写一个 Claude Code mod,或者自己编写一个来计算工具调用并添加命令。学习重新加载和验证循环。
Mod 是一个 Claude Code 插件,具有一个入口文件,称为 hooks 模块:一个 JavaScript 或 TypeScript 文件,其函数在事件发生时由 Claude Code 调用。有两种方式来创建一个:
- 让 Claude 编写它:在 Claude Code 会话中描述你想要的内容
- 自己编写:按照教程学习 mod 代码的工作原理。你不需要 Node.js、打包工具或构建步骤,因为 Claude Code 直接加载
.js和.ts文件。
如果你还没有决定 mod 是否是合适的工具,请先阅读概述中的比较。
Mod 需要 Claude Code v2.1.287 或更高版本。在你的 shell 中,运行 claude --version 来检查。要查看 mod 是否可以为你加载,请参阅检查 mod 是否可以加载。
让 Claude 为你编写一个 mod
在交互式 Claude Code 会话中描述你想要的 mod,Claude 会编写它。Claude 使用一个名为 plugin-authoring 的内置技能,它告诉 Claude 在哪里编写 mod、你的版本有哪些事件和方法,以及 mod 如何被加载。当你请求一个 mod 时,Claude 可以加载该技能,或者你可以通过在 Claude Code 提示符处运行 /plugin-authoring 来自己加载它。
一旦你批准了 mod,它就会运行,除了在某些会话中 Claude 编写的 mod 无法加载的情况。
描述 mod
用你自己的话请求 mod,例如 make a mod that shows the current git branch above the prompt。Claude 在会话的 mods 文件夹中的自己的目录中编写 mod,该文件夹是 ~/.claude/dev-mods/ 后跟会话的 ID。mod 的完整路径看起来像 ~/.claude/dev-mods/3f2a9c1e-5b7d-4e8a-9c21-6d0f4b8a7e13/git-branch/。
在 default 和 acceptEdits 权限模式中,Claude Code 在 Claude 创建 mod 的每个文件之前都会询问,因为 ~/.claude 是一个受保护的路径。在每个文件出现时批准它。
批准 mod
当 Claude 保存第一个文件时,Claude Code 会询问是否为会话启用热重新加载。热重新加载运行此会话中 Claude 编写的 mod,并在每个后续更改它们的转折处选择每个更改。
选择以下答案之一:
- 为此会话启用:会话的 mods 文件夹中的 mod 在转折结束时加载,并在每个更改它们的转折结束时重新加载。你的答案在整个会话中持续,包括在你恢复它之后。
- 暂时不:现在什么都不加载。文件保留在 Claude 编写的位置,mod 在该会话下次启动时加载。要防止 mod 加载,请删除其目录。
检查 mod 是否已加载
在 Claude Code 提示符处运行 /plugin,然后按 Tab 直到选中已安装选项卡。它列出了 mod,你可以在那里关闭它。
尝试 mod
使用你请求的内容。对于示例提示,当前分支名称出现在提示框上方。如果 mod 没有做你想要的,告诉 Claude 要改变什么。mod 在每个更改其文件的转折结束时重新加载,所以你可以在 Claude 完成后立即尝试更改。
在其他会话中使用 mod
Claude 编写的 mod 仅在创建它的会话中加载,一旦 Claude Code 的会话比 cleanupPeriodDays 更旧,它就会删除该会话的 mods 文件夹。要保留 mod,请将其目录从 mods 文件夹复制到你自己的位置,例如 ~/mods/git-branch。然后选择如何加载它:
- 在你启动的会话中:在你的 shell 中,运行
claude --plugin-dir ~/mods/git-branch - 对于其他人:将其添加到市场,以便他们可以安装它
Claude 编写的 mod 无法加载的会话
Claude 编写的 mod 仅在你批准它后加载,在允许 mod 运行的受信任工作区中。在这些会话中它不会加载:
- 没有人在那里批准:会话无法向你显示提示,如在
claude -p运行或dontAsk模式中 - 工作区不受信任:你还没有接受目录的信任提示
- Mod 已停止:你使用
--safe-mode或--bare启动,你设置了disableAllHooks,或你的组织的托管设置阻止了它
自己编写一个 mod
在本教程中,你构建一个名为 first-mod 的 mod,它计算 Claude 进行的工具调用,在 Claude 工作时在微调器旁边显示计数,并添加一个 /tally 命令来打印它。然后你读取 Claude Code 在你的 mod 旁边写入的类型声明,并运行 claude plugin validate。它们一起向你展示你的版本提供的事件和方法,以及 Claude Code 从你的代码中读取的内容。
这个录制显示了完成的 mod。微调器计算工具调用,/tally 打印计数,对代码的编辑在会话运行时生效:
你编写三个文件:
first-mod/
├── .claude-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── register.js
创建插件目录
创建保存文件的两个目录:
mkdir -p first-mod/.claude-plugin first-mod/hooks
New-Item -ItemType Directory -Force first-mod\.claude-plugin, first-mod\hooks
编写清单
Mod 是一个插件,mod 需要一个清单。这个 mod 的清单没有特殊字段。将其保存为 first-mod/.claude-plugin/plugin.json:
{
"name": "first-mod",
"version": "0.1.0",
"description": "Counts Claude's tool calls, shows the count beside the spinner, and adds a /tally command",
"author": { "name": "Your Name" }
}
告诉 Claude Code 你的代码在哪里
当 Claude Code 加载一个插件时,它读取插件的 hooks/hooks.json。该文件中的 modules 键给出你的代码的路径,拥有它是使插件成为 mod 的原因。列出一个路径,相对于 hooks.json。这里它指向 register.js,你在下一步中编写。
将其保存为 first-mod/hooks/hooks.json:
{
"description": "The first-mod hooks module",
"modules": ["./register.js"]
}
编写代码
这个文件是 mod 的代码,称为 hooks 模块。当 mod 加载时,Claude Code 调用文件导出的 register 函数,并传递一个名为 on 的函数。每次调用 on 都会为它命名的事件注册一个事件处理程序,称为 hook。
将其保存为 first-mod/hooks/register.js:
// The count, shared by the hooks below
let calls = 0
// Claude Code calls this once when the mod loads
export function register(on) {
// Runs when the session starts, before your first prompt
on('session.start', async ($, e, next) => {
// Add the /tally command
await $.command.register({
name: 'tally',
description: 'Show how many tool calls Claude has made',
})
// Let the session start as usual
return next(e)
})
// Runs each time Claude is about to use a tool
on('tool.call', async ($, e, next) => {
calls += 1
// Ask Claude Code to draw the interface again, so the new count shows
$.ui.invalidate('ui.render')
// Let the tool run as usual
return next(e)
})
// Runs when you type /tally, and only then, because of the matcher
on('command.run', { command: 'tally' }, async () => {
// The text to print in the transcript
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
// Runs each time Claude Code draws the spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, with the count added after its word
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}
该文件在 calls 中保持计数,并注册四个 hook:
session.start在会话启动时运行,在你的第一个提示之前,以及每次 mod 重新加载时。它将/tally命令添加到 Claude Code。tool.call每次 Claude 即将使用工具时运行。它将 1 添加到calls并要求 Claude Code 再次绘制界面。command.run当你键入/tally时运行。它返回要打印的文本。ui.render每次 Claude Code 绘制微调器时运行。它在微调器的单词后添加计数。
示例 mod 如何工作解释了每个 hook 采用的三个参数以及每个参数返回的内容。
加载 mod
使用 --plugin-dir 标志启动 Claude Code,它为一个会话加载一个插件目录而不安装它:
claude --plugin-dir ./first-mod
尝试 mod
要求 Claude 做一些需要几个工具调用的事情,例如 list the files here and read the README。当 Claude 工作时,微调器的单词后跟一个上升的计数,如 Thinking · tool calls: 2…。当 Claude 完成时,键入 /tally 并按 Enter。转录显示 first-mod: Claude has made 2 tool calls since this mod loaded,带有你自己的计数。Claude Code 将插件的名称放在命令的文本前面。
要在非交互模式下检查命令,请运行它:
claude -p "/tally" --plugin-dir ./first-mod
first-mod: Claude has made 0 tool calls since this mod loaded
如果 /tally 不在命令列表中,则模块未加载。请参阅找出为什么 mod 什么都不做。
在会话运行时更改代码
保持会话打开。在 register.js 中,在 ui.render hook 中将 ' · tool calls: ' 更改为 ' · tools used: ' 并保存。突出显示的行是更改的行:
// Runs each time Claude Code draws the spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, with the count added after its word
return next({ ...e, props: { ...e.props, suffix: ' · tools used: ' + calls + '…' } })
})
转录中的一行说 first-mod 重新加载并列出其 hook,下一个微调器使用新文本,如 Thinking · tools used: 1…。
示例 mod 如何工作
你传递给 on 的每个函数都是一个 hook,它是一个事件处理程序。Claude Code 将相同的三个参数传递给每个 hook:
- Mods API,名为
$:mod 可以调用的每个方法来到达自身之外,在命名空间中,例如$.ui和$.command - 事件,名为
e:事件的输入作为纯数据,例如工具调用的名称和参数 - 下一个处理程序,名为
next:一个函数,将事件传递给其他 mod,然后传递给 Claude Code 自己的行为,并返回结果
first-mod 中的 hook 以 hook 可以处理的三种方式处理它们的事件:
- 观察:
session.starthook 注册命令,tool.callhook 计算调用并要求重新绘制。两者都返回next(e),所以会话启动,工具照常运行。 - 回答:
command.runhook 返回自己的结果,从不调用next。on的第二个参数{ command: 'tally' }是一个过滤器,称为匹配器,所以 hook 仅对/tally运行。 - 重写:
ui.renderhook 使用e的副本调用next,其suffix保持计数,所以 Claude Code 绘制其通常的微调器,你的文本在单词后面
Claude Code 监视使用 --plugin-dir 加载的目录,并在其中的文件更改时热重新加载 hooks 模块。每次重新加载都会再次运行 register,所以 calls 回到 0,/tally 开始重新计数。要在重新加载中保持值,请参阅保持状态。
继续处理 mod
一旦 mod 加载,你可以让 Claude 更改它,根据你的版本的类型定义检查你的代码,列出 Claude Code 在其中找到的事件和调用,并测试它。
使用 Claude 更改 mod
要更改你已有的 mod,使用 --plugin-dir 指向 mod 的目录启动会话,以便 Claude 编写的内容在同一会话中加载:
claude --plugin-dir ./first-mod
然后请求更改,例如 add a /tally-reset command to this mod that sets the tally back to zero。Claude 编辑 hooks 模块,运行 claude plugin validate,并修复它报告的内容。你使用 --plugin-dir 加载的目录是一个受保护的路径,所以在 default 和 acceptEdits 模式中,你被要求批准 Claude 对 mod 的每个编辑。受保护的路径表给出其他权限模式的结果。
Claude 在其转折期间保存的文件在转折结束时重新加载,所以你可以在 Claude 完成后立即尝试 /tally-reset。
获取你的版本的类型定义
每次 Claude Code 从你传递给 --plugin-dir 的目录加载或重新加载 mod,或 mod Claude 为你编写时,它会将 TypeScript 声明文件(以 .d.ts 结尾)写入 mod 目录内的 .claude-plugin/types/。它们描述你正在运行的 Claude Code 版本中的确切事件、mods API 方法和元素,所以你的编辑器可以自动完成和类型检查你的 hooks。要在线浏览声明,请阅读 Claude Code 仓库中的 mods/types/claude-code.d.ts,其第一行命名了写入它的版本。该目录包含这些文件:
| 路径 | 它声明的内容 |
|---|---|
claude-code/index.d.ts |
每个事件及其输入和结果,每个 mods API 命名空间和方法,以及每个表面可以绘制的元素 |
claude-code-tools/index.d.ts |
内置工具的输入和结果,以便检查 e.tool === 'Bash' 缩小 e |
claude-code-mcp/index.d.ts |
上次在 mod 中保存文件时连接的 MCP 工具的输入 |
以插件命名的目录中的 index.d.ts |
该插件添加到 mods API 的内容。你的 plugin.json 在 dependencies 下列出的每个插件都有一个目录。 |
tsconfig.json |
适合 hooks 模块的编译器选项 |
如果你的 mod 没有自己的 tsconfig.json,Claude Code 会在 mod 的根目录添加一个,扩展生成的那个,所以你的编辑器和 tsc -p ./first-mod 类型检查 mod 而无需更多设置。
事件和方法可以在版本之间更改,所以当它们不同意时,相信这些文件而不是任何页面,包括这个。
claude-code/index.d.ts 是你的构建的最完整的参考,每个 mods API 方法都有注释和示例。要查找某些内容,请在文件中搜索其名称,例如 'tool.call'。
检查 Claude Code 从你的 mod 中读取的内容
要以 Claude Code 看到的方式查看你的 mod,而不运行你的代码或启动会话,请使用 claude plugin validate。它检查清单并对 hooks 模块的源运行相同的静态分析,Claude Code 在加载 mod 时运行。在你的 shell 中,在 mod 的目录上运行它:
claude plugin validate ./first-mod
对于 first-mod,输出包括这些行。
❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
❯ ./register.js calls: $.command.register, $.ui.invalidate
✔ Validation passed
hooks: 行列出你的模块 hook 的事件,每个都带有其在大括号中的过滤器。calls: 行列出它调用的每个 mods API 方法。读取或设置环境变量的模块也会获得 env reads: 和 env writes: 行,使用 $.state 的模块会获得 state reads: 和 state writes:。
如果你打算 hook 的事件在第一行中缺失,Claude Code 也不会调用该 hook。通常的原因是事件名称拼写错误,命令报告为错误,例如 "tool.calls" is not an event。
遵循这些规则,以便静态分析可以找到每个 hook 和调用:
- 完整拼写每个 mods API 调用:
$、命名空间,然后是方法,如$.store.get('notes')。你可以将$传递给在同一文件的顶级声明的函数,对于你的名为loadNotes的函数,calls:行然后读取$.store.get (via loadNotes)。将$传递给方法、在 hook 内定义的函数或从另一个文件导入的函数会导致验证失败。$.state使用的read和update函数是可以接受它的导入。不要将$或其命名空间之一分配给变量、解构它或使用计算名称索引它。const ui = $.ui失败,出现$.ui is used as a value。 - 在每个
on调用中将事件名称写为字符串文字,例如'tool.call'。变量或循环遍历名称列表会失败,出现the event name passed to on() is not a string literal。 - 在
register内,不要声明第二个名为on的变量或参数。验证失败,出现"on" is declared again (shadowed)。 - 仅从插件目录内的文件导入,通过相对路径。唯一允许的裸导入是
claude-code,用于类型和一些帮助程序。 - 在文件顶部使用
import声明,如import { name } from './file.js'。动态import()失败,出现a dynamic import(); a hooks module imports its own files with an import declaration。 - 将每个文件写为 ES 模块,使用
import而不是require。参考列出 Claude Code 加载的文件扩展名。
测试 mod
你可以为 mod 编写自动化测试,并使用 claude plugin test 从你的 shell 运行它们,无需会话、登录或网络。测试引发你的 hook 处理的事件,并检查 hook 做了什么。
这个测试引发两个工具调用,运行 /tally,并检查回复计算两者。将其保存为 first-mod/tests/first-mod.test.ts:
import { expect, test } from 'claude-code/testing'
test('/tally reports the tool calls the mod has seen', async ($, on) => {
// Answer each tool call in Claude Code's place, so no tool runs
on('tool.call', () => ({ result: 'ok' }))
// Raise two tool calls, which the mod's tool.call hook counts
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// Run /tally and check the text its hook returns
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})
在你的 shell 中,从 first-mod 目录运行测试:
claude plugin test
输出命名每个测试及其是否通过,时间从运行到运行变化:
tests/first-mod.test.ts:
(pass) /tally reports the tool calls the mod has seen [22.87ms]
1 pass
0 fail
Ran 1 test across 1 file. [0.19s]
测试 mod涵盖存根模型调用或存储,以及测试计时器和绘图。
分享你的 mod
Mod 是一个插件,所以你在清单中对其进行版本控制,人们使用 /plugin 命令安装和更新它。要将其提供给其他人,将其添加到市场。
在你这样做之前,检查插件的 name:claude plugin validate 失败一个看起来像 Anthropic 自己的名称,例如以 claude- 开头的名称。事件和方法可以在版本之间更改,所以你的 README 是说明你测试的 Claude Code 版本的地方。
继续针对目录使用 --plugin-dir 进行开发,而不是针对已安装的副本。Claude Code 按版本缓存已安装的插件,所以你的编辑在你提高版本并再次安装之前不会到达已安装的副本。
后续步骤
- 在界面中绘制:打开一个窗格,在提示上方绘制,并添加按钮和文本字段
- 对事件做出反应:hook 工具调用、提示和转折
- 使用 mods API:添加命令和工具、调用模型、在计时器上运行工作
- 测试 mod:存根 Claude Code 会回答的内容,以及测试计时器和绘图
- 排查 mod 故障:mod 什么都不做的原因和调试日志
- 阅读内置 mod 的源代码:完整的插件,每个都有其 hooks 模块和测试