Claude Code Mods 解读:把扩展装进进程里

从进程外到进程内:读懂 Claude Code 的 mod 能做什么、在哪里生效、要付出多少信任

Posted by iceyao on Tuesday, October 6, 2026

一、引言: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 在工具里面运行,和事件循环共享同一个进程。

Claude Code Mods:把扩展装进进程里

这张图左右两块的差别,就是整篇文档的主线。左边那五项能力只有进程内才拿得到,代价也写在底部那行字里:拿到这些能力,就要对它付出完整的信任。后面几节都是在展开这两件事。


二、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 + '…' } })
  })
}

逐段看:

  1. let calls = 0 写在模块顶层。 这就是第 2.1 节说的"hooks 之间共享数据"。两个 hook 都闭包引用同一个变量,不需要任何外部存储。
  2. register(on) 是入口。 mod 加载时 Claude Code 调用它一次,你在里面用 on(事件名, [过滤条件], 处理函数) 注册 hook。
  3. 处理函数的签名是 ($, e, next)。 e 是事件本身;next 把事件交给后续处理(最终是 Claude Code 的原有行为);$ 是 mods API 的入口,示例里用到的 $.ui.invalidate('ui.render') 就是请求重绘界面。
  4. tool.call hook 只观察。 计数加一、请求重绘,然后 return next(e),工具照常运行。
  5. ui.render hook 带过滤条件 { component: 'Spinner' }。 它只在绘制 spinner 时触发,把 e.props.suffix 改成带计数的字符串,再交给 next。原来的 spinner 保留,只是多了一截后缀。

3.3 三种处理方式:Observe、Rewrite、Answer

这个示例恰好覆盖了 hook 对事件的前两种处理方式,文档还给了第三种:

方式 做法 示例中的对应
Observe(观察) 记录信息,事件原样继续 tool.call hook
Rewrite(改写) 修改事件后再继续 ui.render hook 加计数
Answer(接管) 自己处理事件,原有行为不再执行 例如拒绝某条命令

一个事件经过 mod hook 的三种走向

图里的分界线落在 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-authoring skill 帮你写,或者手写;
  • 装现成的:从 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 进程走,后者要看当前前端能不能渲染。

同一个 mod,在不同运行位置的表现

从这张矩阵可以读出三条规律:

  1. hooks 几乎处处运行。 除了桌面应用里的 WSL 会话(WSL 会话不支持 plugin),终端、桌面应用、VS Code 聊天面板、claude -p、Agent SDK、Remote Control、云端会话都会跑 hook。云端会话有前提:plugin 得被带到云端会话里。
  2. 界面只在有终端渲染的地方显示。 终端 claude(包括编辑器内置终端和 JetBrains 插件)完整显示;桌面应用 Code 标签也能显示,但 elements 表里标为仅终端的元素除外;Remote Control 的 hook 在本机会话里跑,画出来的东西显示在本机终端。
  3. 无头场景只剩逻辑。 VS Code 聊天面板、claude -p 和 Agent SDK 里,hook 照跑,但画的东西没人看得见。

所以文档给了一个明确建议:会画界面的 mod 应该检测当前运行环境,画不出来时退回成在对话记录里输出一行文字,或者通过命令的文本回复展示。

这一点对 Answer 类 mod 尤其要紧。像 blast-radius 这种"拦下命令、弹按钮让你选"的设计,如果放到 claude -p 的流水线里,按钮画不出来,拦截逻辑却还在执行。写这类 mod 时,要提前想好无界面时的默认行为。这是作者的判断,原文没有展开,但从矩阵本身可以直接推出来。


六、安全边界:mod 拿到的是你的权限

进程内运行的另一面,是 mod 拿到了和你一样的权限,而且没有隔离层。

mod 拿到的是你的权限,不是一个沙箱

6.1 它能做什么

文档列得很直白:mod 运行时拥有你的权限,可以以你的身份读写文件、启动程序、发送网络请求;读取环境变量和设置文件,包括其中的 API key;看到每条提示词和每次工具调用;改写提示词或工具调用、代你提交提示词、给你的其他会话发消息;在询问你之前自动批准工具调用;消耗你的套餐额度或 API key。

6.2 三条硬边界

  • 没有沙箱。 即使开了 sandboxing,被隔离的也只是模型执行的 Bash 命令,mod 启动的进程在沙箱之外运行。
  • 能越过部分权限规则。 会批准工具调用的 mod,可以批准本该由 ask 规则提示的调用,或者被你自己的 PreToolUse hook 拦下的调用,某些情况下甚至能批准被 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: 值得认真读一遍。

「真诚赞赏,手留余香」

爱折腾的工程师

真诚赞赏,手留余香

使用微信扫描二维码完成支付