一、引言:hook 这个词换了意思
原文链接:Mods overview
来源:Claude Code 官方文档 · Plugins / Mods 章节
如果你写过 Claude Code 的 hook,脑子里的画面大概是这样:在 settings.json 里配一条规则,事件发生时 Claude Code 去跑一个 shell 命令、发一个 HTTP 请求,或者塞一段 prompt,然后根据返回值决定放行还是拦截。
Mods 文档一上来就把这个词重新定义了一遍:
- 在 Mods 文档里,hook 指 mod 的处理函数,它运行在 Claude Code 进程内部;
- 原来那种配在 settings 文件里的 hook,改名叫 settings hook。
改名本身不重要,重要的是背后的位置变化。settings hook、skill、status line、MCP server 都在工具外面工作,通过文件、子进程或协议和 Claude Code 交互;mod 在工具里面运行,和事件循环共享同一个进程。
这张图左右两块的差别,就是整篇文档的主线。左边那五项能力只有进程内才拿得到,代价也写在底部那行字里:拿到这些能力,就要对它付出完整的信任。后面几节都是在展开这两件事。
二、Mod 是什么:一个装着事件处理函数的 plugin
先看定义。mod 是一种特殊的 plugin,用来改变 Claude Code 的外观和行为。它由 JavaScript 或 TypeScript 写的事件处理函数组成:工具调用、提交提示词、绘制界面的某一部分,这些事件发生时,Claude Code 会调用你的函数,函数可以观察、修改或接管这个事件。
2.1 五项进程内独有的能力
因为运行在进程内,mod 可以做到下面五件事:
| 能力 | 具体含义 |
|---|---|
| 绘制可交互界面 | 在对话旁边加面板,或在输入框上方加横条,支持标签页、按钮和文本框 |
| 重绘自带界面 | 替换或重新设计工具调用行、spinner、提问对话框等 |
| 介入工具调用或请求 | 暂停工具调用去询问用户;不执行工具,直接返回结果;把某个请求转给其他模型 |
即时执行 /command |
命令直接运行你的函数,不经过模型轮次,模型正在工作时也能用 |
| hooks 之间共享数据 | 同一文件里的变量对所有 hook 共享,一个 hook 计数,另一个 hook 显示 |
这五项里,第 1、2 项是"画",第 3 项是"拦",第 4 项是"绕开模型",第 5 项是"有状态"。settings hook 每次都起一个新进程,天然没有状态;skill 只是给模型读的文字;MCP server 只能给模型加工具,碰不到界面。这几块空白,mod 一次性补上了。
2.2 它仍然是 plugin
mod 没有另起一套分发体系,它就是 plugin,通过 marketplace 安装。文档明确说,关于 marketplace、作用域、VS Code 扩展、桌面应用、更新的那些说明,对带 mod 的 plugin 同样适用,不需要改动。
一个 plugin 可以同时装着 mod、skill、MCP server、commands 和 agents。这带来一个值得留意的细节:disableAllHooks 和组织层面的 allowManagedModsOnly 只停用 mod 那一部分,plugin 本身仍保持安装,里面的 skills、commands、agents、MCP servers 照常加载。关掉 mod,不等于卸掉整个 plugin。
三、拆一个最小 mod:三个文件、两个 hook
3.1 目录结构
一个最小的 mod 只有三个文件:
first-mod/
├── .claude-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── register.js
| 文件 | 作用 |
|---|---|
plugin.json |
plugin 的清单(manifest) |
hooks.json |
指向你的代码文件 |
register.js |
你的代码,称为 hooks module,告诉 Claude Code 在哪些事件上运行哪些函数 |
plugin.json 和 hooks.json 的具体字段,概览页没有展开,分别放在 manifest reference 和 mods reference 的 files 部分。这里只需要记住分工:清单声明"我是谁",hooks.json 声明"代码在哪",hooks module 声明"我管哪些事件"。
3.2 官方示例:给 spinner 加一个工具调用计数
下面这段 register.js 统计工具调用次数,并在 Claude 工作时显示在 spinner 后面,效果类似 Thinking · tool calls: 3…:
// The count, shared by the two hooks below
let calls = 0
// Claude Code calls this once when the mod loads
export function register(on) {
// 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 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 + '…' } })
})
}
逐段看:
let calls = 0写在模块顶层。 这就是第 2.1 节说的"hooks 之间共享数据"。两个 hook 都闭包引用同一个变量,不需要任何外部存储。register(on)是入口。 mod 加载时 Claude Code 调用它一次,你在里面用on(事件名, [过滤条件], 处理函数)注册 hook。- 处理函数的签名是
($, e, next)。e是事件本身;next把事件交给后续处理(最终是 Claude Code 的原有行为);$是 mods API 的入口,示例里用到的$.ui.invalidate('ui.render')就是请求重绘界面。 tool.callhook 只观察。 计数加一、请求重绘,然后return next(e),工具照常运行。ui.renderhook 带过滤条件{ component: 'Spinner' }。 它只在绘制 spinner 时触发,把e.props.suffix改成带计数的字符串,再交给next。原来的 spinner 保留,只是多了一截后缀。
3.3 三种处理方式:Observe、Rewrite、Answer
这个示例恰好覆盖了 hook 对事件的前两种处理方式,文档还给了第三种:
| 方式 | 做法 | 示例中的对应 |
|---|---|---|
| Observe(观察) | 记录信息,事件原样继续 | tool.call hook |
| Rewrite(改写) | 修改事件后再继续 | ui.render hook 加计数 |
| Answer(接管) | 自己处理事件,原有行为不再执行 | 例如拒绝某条命令 |
图里的分界线落在 next 上:Observe 和 Rewrite 都调用 next,区别只是传进去的是原事件还是改过的副本,最后都汇到"原有行为执行";Answer 不调用 next,事件在 hook 里就结束了,原有行为被跳过。写 mod 时,一个 hook 到底是"旁观者"还是"拦截者",看它有没有、以及怎样调用 next 就能判断。
3.4 一切外部动作都要经过 mods API
文档里有一句容易被略过的话:hook 想做自身代码以外的任何事,比如绘制、添加命令、调用模型、读文件、启动进程、发网络请求,都必须通过 mods API。
这条约束的价值在安装前。因为所有对外动作都走同一套 API,Claude Code 可以在不运行 mod 的情况下静态列出它会做什么。第四节的 claude plugin validate 正是建立在这一点上。换个角度说,mods API 既是能力的入口,也是审计的抓手。
四、怎么拿到一个 mod,怎么在装之前看清它
4.1 三种来源
- 用内置的:Claude Code 自带的部分功能本身就是 mod,例如
/diff; - 自己写:在会话里描述需求,让 Claude 借助内置的
plugin-authoringskill 帮你写,或者手写; - 装现成的:从 marketplace 安装。
4.2 安装与更新
格式是 插件名@marketplace名:
/plugin install token-chart@your-org # 会话内
claude plugin install token-chart@your-org # shell 中
如果会话开着的时候你在 shell 里安装或更新了 mod,需要在那个会话里执行 /reload-plugins,否则要等下次启动才会加载。
4.3 先试官方示例
官方在 anthropics/claude-code-playground 仓库的 claude-code/mods 目录放了几个示例,按原样提供,不提供支持:
| 示例 | 功能 |
|---|---|
token-weather |
在输入框上方画一个上下文窗口"天气预报" |
blast-radius |
拦截高风险 shell 命令(如 rm -rf、force push),展示影响范围,给出继续 / 取消按钮 |
replay-theater |
添加 /replay 命令,逐步回放上一轮的文件编辑 |
这三个示例分别对应三类能力:横条绘制、工具调用介入(Answer 方式的典型用法)、即时命令。试用流程是:克隆仓库,用 --plugin-dir 让单个会话临时加载,再用 /plugin 确认已加载。想长期使用,就把克隆下来的 claude-code/mods 目录添加为 marketplace,从 claude-code-playground-mods 安装。要注意,这种方式依赖本地目录,克隆目录一旦移动或删除,mod 就不再加载。
4.4 装之前先审查
claude plugin validate ./some-mod
输出里的 hooks: 行列出这个 mod 处理哪些事件,calls: 行列出它请求哪些操作。这个命令不会运行 mod。对来路不明的 mod,这一步应该是必选项:一个只声明了 ui.render 的计数器,和一个声明了网络请求、读文件、启动进程的"计数器",风险完全不同。
4.5 查看、开关与版本
会话里运行 /plugin,标签下方的灰色文字会显示已加载 mod 的数量和名称,例如 1 mod active · first-mod。
mod 默认开启。版本要求:终端需要 v2.1.287 及以上,桌面应用自带的版本从 v2.1.286 起支持。终端里用 claude --version 查看;桌面应用在 Code 标签的本地会话里输入 /status 查看。
关闭方式按范围分三档:
| 关闭范围 | 方法 |
|---|---|
| 单个 mod | 在 /plugin 的 Installed 标签里禁用或卸载 |
| 所有已安装 mod,仅本次会话 | 用 --safe-mode 启动(其他自定义项也会一并禁用) |
| 所有自装 mod,所有会话 | 在 ~/.claude/settings.json 里设置 "disableAllHooks": true;settings hook 和自定义 status line 也会停用,组织托管的内容照常运行 |
如果你在早期测试时设置过 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS,请删掉它。从 v2.1.287 起这个变量被忽略,设成 0 也关不掉 mod。
五、运行在哪里,画在哪里
mod 的逻辑和它画出来的界面是两回事。前者跟着 Claude Code 进程走,后者要看当前前端能不能渲染。
从这张矩阵可以读出三条规律:
- hooks 几乎处处运行。 除了桌面应用里的 WSL 会话(WSL 会话不支持 plugin),终端、桌面应用、VS Code 聊天面板、
claude -p、Agent SDK、Remote Control、云端会话都会跑 hook。云端会话有前提:plugin 得被带到云端会话里。 - 界面只在有终端渲染的地方显示。 终端
claude(包括编辑器内置终端和 JetBrains 插件)完整显示;桌面应用 Code 标签也能显示,但 elements 表里标为仅终端的元素除外;Remote Control 的 hook 在本机会话里跑,画出来的东西显示在本机终端。 - 无头场景只剩逻辑。 VS Code 聊天面板、
claude -p和 Agent SDK 里,hook 照跑,但画的东西没人看得见。
所以文档给了一个明确建议:会画界面的 mod 应该检测当前运行环境,画不出来时退回成在对话记录里输出一行文字,或者通过命令的文本回复展示。
这一点对 Answer 类 mod 尤其要紧。像 blast-radius 这种"拦下命令、弹按钮让你选"的设计,如果放到 claude -p 的流水线里,按钮画不出来,拦截逻辑却还在执行。写这类 mod 时,要提前想好无界面时的默认行为。这是作者的判断,原文没有展开,但从矩阵本身可以直接推出来。
六、安全边界:mod 拿到的是你的权限
进程内运行的另一面,是 mod 拿到了和你一样的权限,而且没有隔离层。
6.1 它能做什么
文档列得很直白:mod 运行时拥有你的权限,可以以你的身份读写文件、启动程序、发送网络请求;读取环境变量和设置文件,包括其中的 API key;看到每条提示词和每次工具调用;改写提示词或工具调用、代你提交提示词、给你的其他会话发消息;在询问你之前自动批准工具调用;消耗你的套餐额度或 API key。
6.2 三条硬边界
- 没有沙箱。 即使开了 sandboxing,被隔离的也只是模型执行的 Bash 命令,mod 启动的进程在沙箱之外运行。
- 能越过部分权限规则。 会批准工具调用的 mod,可以批准本该由
ask规则提示的调用,或者被你自己的PreToolUsehook 拦下的调用,某些情况下甚至能批准被deny规则拒绝的调用。 - 改不了权限提示框。 mod 可以重画大部分界面,但不能改动权限提示,也不能改变提示框里显示的内容。
第三条是整个设计里唯一一处"界面不可被 mod 接管"的地方。可以这样理解:权限提示是用户做最终判断时看到的东西,如果它也能被重画,前两条边界就连最后的知情权都保不住了。
6.3 你手里的开关
把第四节的开关和这里对照着看:装前用 claude plugin validate 看清 hooks: 和 calls:;单个 mod 在 /plugin 里禁用或卸载;--safe-mode 关闭本次会话的所有 mod;disableAllHooks 关闭所有会话里的自装 mod;组织层面,管理员可以通过 managed settings 控制 mod 是否运行、允许哪些 mod,例如 allowManagedModsOnly。
图右下角那条注意事项值得单独说一句:这些开关对内置 mod 无效。下一节展开。
七、内置 mods:已经在你会话里跑着的那几个
在 /plugin 的 Installed 标签里,有一个 Built-in 分组。内置 mod 不能更新或卸载,也不计入 mods active 的数量。
| 名称 | 功能 | 关闭方式 |
|---|---|---|
cc-plugin-agents-md |
把 AGENTS.md 作为项目指令加载 |
在 /plugin 中禁用,或选择要加载哪些指令文件 |
cc-plugin-diff |
接管 /diff 并绘制面板,仅交互式终端会话 |
在 /plugin 中禁用,之后 /diff 由内置版本响应 |
cc-plugin-plugin-authoring |
提供编写 mod 的 plugin-authoring skill(只含 skill,没有 mod 代码) |
在 /plugin 中禁用 |
cc-plugin-sec-default |
防止用户自装的 mod 干扰组织托管内容 | 用户无法关闭,由管理员在 managed settings 中设定顺序 |
cc-plugin-telemetry |
发送工具和内置 mod 记录的分析数据 | 在 /plugin 中禁用,或关闭分析(如 DISABLE_TELEMETRY) |
cc-plugin-you-should-know |
运行一个旁路 agent,长任务中发现你可能漏掉的信息时,在输入框上方提示 | 默认禁用;用 /plugin enable cc-plugin-you-should-know@builtin 开启 |
disableAllHooks、--bare、--safe-mode 都不会停用内置 mod。要关某个内置 mod,只能逐个到 /plugin 里处理。
这张表能看出 Anthropic 自己在用 mod 做什么。AGENTS.md 支持、/diff 面板、遥测、安全默认值,这些过去可能是硬编码在 Claude Code 里的功能,现在都被拆成了 mod。其中四个的源码开源在 anthropics/claude-code 仓库的 mods 目录,每个都附带 hooks module 和测试,适合当参考实现:
diff:按钮绑定键盘操作,滚动由 mod 自己处理,可参考如何画一个完整面板;agents-md:带userConfig配置项,可参考如何让用户配置 mod;sec-default:策略类 mod 的参考范例;telemetry:提供供其他 mod 调用的方法,并附带类型定义,可参考 mod 之间如何协作。
想写 mod 的话,与其从零摸索 API,不如先读这四份源码。
八、总结:四种扩展机制怎么选
文档最后给了一张对比表,整理如下:
| 维度 | Mod | Settings hook | Skill | MCP server |
|---|---|---|---|---|
| 是什么 | plugin 中由 Claude Code 在自身进程内调用的函数 | 在生命周期事件上运行的 shell 命令、HTTP 请求或 prompt | 模型读取的 SKILL.md 指令文件 |
为模型提供工具的外部进程或服务 |
| 能改变什么 | 工具调用、提示词、命令、轮次、界面绘制 | 工具调用或提示词是否继续、工具参数与结果、追加给模型的上下文 | 模型知道什么、做什么 | 模型拥有哪些工具 |
| 能否绘制界面 | 能 | 否 | 否 | 否 |
| 编写语言 | JavaScript / TypeScript | 脚本加一条 settings.json 配置 |
Markdown | 任意语言实现的服务 |
| 适用场景 | 需要面板、输入框上方横条、自定义命令或改写事件 | 用已有脚本拦截、放行或记录事件 | 反复粘贴同样的指令时 | 模型需要访问外部系统时 |
把表格的"适用场景"一行竖过来,就是一条选型顺序:
这张图的排序是作者的判断,依据是"能力越大,需要的信任越多":skill 只影响模型读到的文字;MCP server 给模型加工具,但调用仍然走权限规则;settings hook 能拦截事件,但它是一次性的外部进程;mod 常驻进程、能改写事件、能自动批准工具调用,还能画界面。能在上面一层解决的问题,就不必下沉到 mod。
回到开头那张图。Mods 带来的变化,是 Claude Code 的扩展点从"进程外的若干接口"变成"进程内的一整条事件链"。对插件作者,这意味着界面、命令、工具调用、状态第一次可以放进同一个文件里统一编排;对使用者,这意味着装一个 mod 之前,claude plugin validate 那两行 hooks: 和 calls: 值得认真读一遍。
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付