返回博客列表

Claude Code Mods 深度解析:能画界面、能拦工具调用的插件新形态

2026-10-05T14:30:00+08:00
Claude CodeMods插件AgentHooksMCPDeepSeek Harness

Claude Code Mods 深度解析:能画界面、能拦工具调用的插件新形态

Skill 教模型做事,MCP 给模型接工具,Mods 改的是工具本身。

今天日报里有一条新闻:DeepSeek Harness v0.2.1-alpha.1 加了 Claude Code Mods 兼容层,作者崔添翼说这是一次"验证"——验证 Claude Code Mods 提供给插件作者的扩展能力,是否大致是 DSH"一切皆插件"架构能力的子集。看完官方文档我大概理解他为什么这么说了:Mods 不是"再给 Claude 加个工具",而是把 Claude Code 这个工具本身变成可编程的。

官方对它的定义是一句话:Mod 是一个改变 Claude Code 外观与行为的插件,由 JavaScript 或 TypeScript 的事件处理函数组成;当工具调用、提示词提交、界面绘制等事件发生时,Claude Code 调用你的函数,你可以观察它、改写它,或者直接接管它。

关键是最后半句——这些函数跑在 Claude Code 自己的进程里。已有的 settings hooks(在 settings.json 里配的 shell 命令 / HTTP 请求 / prompt)是从外面执行脚本;Mod 是从里面改行为。这一个字的差别,撑开了完全不同的能力边界。

本文提纲

  1. 和 Skill、MCP、settings hook 的分工
  2. 能做什么:从画界面到接管工具调用
  3. 事件与 API:可编程的边界
  4. 权力与信任:它是代码,且不在沙箱里
  5. 多 Mod 共存:中间件链、执行顺序与失败语义
  6. 限额、可用范围与生态信号

和 Skill、MCP、settings hook 的分工

官方给了一张很清楚的对照表,我按中文习惯整理(术语保留英文):

Mod Settings hook Skill MCP server
它是什么 插件里的一组函数,由 Claude Code 在自己进程内调用 生命周期事件触发的 shell 命令 / HTTP 请求 / prompt 一份 SKILL.md 指令文件 提供工具的外部进程或服务
能改变什么 工具调用、提示词、命令、整轮对话,以及界面画什么 工具调用或提示词是否放行、工具参数与返回值、给 Claude 追加的上下文 Claude 知道什么、怎么做 Claude 有哪些工具
能在界面里画东西 能 不能 不能 不能
你要写什么 JavaScript / TypeScript 脚本 + settings.json 配置 Markdown 任意语言的服务端
什么时候选它 想要一个面板、prompt 上方的状态带、自定义命令,或想改写事件 想用现成脚本拦截、放行、记录事件 你老是把同一段指令粘进对话 Claude 需要访问外部系统

一句话总结三者关系:Skill 决定 Claude 怎么想,MCP 决定 Claude 能碰到什么,Mod 决定 Claude Code 这个程序本身长什么样、怎么运行。 而且三者不互斥——一个插件可以同时包含 Mod、Skill 和 MCP server。

能做什么:从画界面到接管工具调用

官方在 overview 里列了五类别的机制做不到的事:

  1. 画出可交互界面:transcript 旁边的 pane(面板),或 prompt 上方的 band(状态带),里面可以有 tab、按钮、输入框。
  2. 重绘 Claude Code 自己画的东西:比如替换或改造工具调用那一行、spinner、提问对话框。
  3. 介入工具调用或请求:把一次工具调用挂住去问用户、不执行工具直接给答案、或者把某个请求发给另一个模型。
  4. 注册自己的命令:/command 立刻执行你的函数,不产生一次 Claude turn,Claude 正在忙的时候也能用。
  5. 在多个 hook 之间共享状态:同一个模块里的 hook 共享变量——一个 hook 记录数据,另一个负责显示。

第 5 条用官方那个最小的完整例子最能说明问题。整个 Mod 只要三个文件:

first-mod/
├── .claude-plugin/
│   └── plugin.json
└── hooks/
    ├── hooks.json
    └── register.js

register.js 里注册两个 hook,统计 Claude 调用了多少次工具,并把计数加到 spinner 后面:

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

效果是 spinner 上显示 Thinking · tool calls: 3…。注意三个细节:hook 签名是 ($, e, next)——$ 是 mods API 入口,e 是事件,next 是把控制权交给下一个 handler(最后会交给 Claude Code 自己的行为);ui.render 里的 { component: 'Spinner' } 是事件过滤,只在你关心的组件渲染时触发;而 next({...e, props: {...}}) 是"改写后继续",不是"取代"。

事件与 API:可编程的边界

官方文档里能看到的事件覆盖了会话与界面的关键节点:

session.start     turn.start      turn.step       turn.complete
tool.call         tool.check      prompt.context  prompt.submit
prompt.section    skill.prompt    ui.render       result.usage

和事件配套的 mods API 分几个命名空间(写法都是 $.xxx):

界面与状态:$.ui.invalidate / $.ui.log / $.ui.status / $.ui.toast   $.store
命令与工具:$.command.register / $.tool.register
模型:     $.model.complete / $.model.fork
调度:     $.clock.after / $.clock.every / $.clock.now / $.clock.sleep
系统:     $.fs.list / $.fs.read / $.process.run / $.http.fetch / $.env / $.settings
会话:     $.session.send / $.agent.list / $.prompt.submit   $.mcp

对每个事件,hook 有三种处理方式:观察(记录后原样放行)、改写(改完再往下传)、接管(自己处理,后续行为不执行,比如直接拒绝一条命令)。

这里有一条设计上的关键点:hook 想做任何"自己代码之外"的事——画界面、注册命令、调模型、读文件、起进程、发网络请求——都必须经过 mods API。 没有别的路径。这正是 claude plugin validate 能在你不运行它的情况下,列出这个 mod "想干什么"的前提。

权力与信任:它是代码,且不在沙箱里

这一段是我认为最需要被更多人看到的。官方的警告很直白:

A mod is code that runs with your permissions.

一旦加载,一个 Mod 可以:

  • 以你的身份操作机器:在你能读写的任何位置读写文件、启动程序、发起网络请求;
  • 读你的密钥:环境变量和设置文件,包括你放在里面的 API key;
  • 看到你的会话:你发的每一条 prompt、Claude 的每一次工具调用;
  • 改变你的会话:改写 prompt 或工具调用、以你的名义提交 prompt、给你的另一个会话发消息;
  • 替你做决定:在你被问之前就把工具调用批准掉;
  • 花你的额度:用你套餐或 API key 调模型。

而且——Mods 不在沙箱里。就算你开了 sandboxing,那个沙箱隔离的是 Claude 执行的 Bash 命令;Mod 自己起的进程跑在沙箱外面。

有个细节挺耐人寻味:Mod 可以给 Claude Code 的界面做大量重绘,但改不了权限提示本身,不能改变权限提示给你看的内容。这等于在 UI 层给"权限确认"留了一块不可篡改的自留地。

安装前的审计方式是命令行校验:

claude plugin validate ./some-mod

输出里的 hooks: 和 calls: 两行会告诉你这个 mod 处理哪些事件、要求 Claude Code 做什么(读文件、发请求……),不用运行它。

想关掉也有三层粒度,从细到粗:

  • 单个 mod:在 /plugin 的 Installed 标签里禁用它或卸载插件;
  • 本次会话全部禁用:用 --safe-mode 启动(会连带禁用你其它的定制);
  • 所有会话全部禁用:在 ~/.claude/settings.json 里设 "disableAllHooks": true(你的 settings hooks 和自定义 status line 也会停,但组织管理的仍会运行)。

组织还可以用 allowManagedModsOnly 之类的受管设置限制用户装哪些 mod。另外两个版本细节:Mods 需要 Claude Code v2.1.287 或更高,且默认开启;早期访问阶段用过的 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS 环境变量在这个版本之后被忽略——设成 0 并不能关掉它。

多 Mod 共存:中间件链、执行顺序与失败语义

这是工程上最有意思的部分,也是最容易踩坑的地方。同一个事件上的多个 hook 组成一条中间件链:每个 mod 的 next 调用链上下一个 hook,最后一个 next 落到 Claude Code 自己的行为。第一个 mod 在最外层——它先看到事件、后看到结果,并且它决定后面的 mod 到底跑不跑;反过来,靠后的 mod 无法阻止靠前的 mod 看到事件。

顺序不是随机的,官方按来源固定:

  1. 内置守卫 sec-default@builtin、组织在 prependPlugins 里列的 mod、以及算作组织的其它 mod(不在 appendPlugins 里的);
  2. 你自己安装的 mod;
  3. 组织在 appendPlugins 里列的 mod;
  4. Claude Code 其它内置 mod。

你安装的 mod 之间,dependencies 里声明的前置会先跑;同一个模块内,按 register 调用 on 的顺序。

settings hooks 也被插在这条链的固定位置:受管设置里的 PreToolUse 跑在第一个 mod 的 tool.call 之前,且它的拦截是终局的(任何 mod 都看不到这次调用);其它设置文件和插件 hooks.json 里的 PreToolUse 跑在最后一个 mod 调用 next 之后。而 tool.check 事件在权限规则判定之后触发,所以挂在它上面的 hook 可以批准一个被第二组 hook 拦下来的调用。

失败语义也很明确:没有 .catch 的 hook 抛异常、超时、或返回形状不对时——

  • 如果它在调用 next 之前失败:Claude Code 跳过它,下一个 handler 顶上;
  • 如果它在 next resolve 之后失败:那个结果算数,不会重跑。

会话里会出现一行说明,形如 my-mod: tool.call hook skipped: threw Error: boom。

对于"拦工具调用"这种安全相关的 mod,官方给了一个 fail-closed 的写法——给这一个 hook 单独挂 .catch,失败时返回 deny:

// on returns a registration, and .catch attaches a handler to that one hook
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind is 'throw' or 'timeout', which says how guard failed
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

没有这个 handler 时,guard 失败会被跳过、命令照常执行;加上之后,失败会变成拒绝执行,Claude 会读到带 throw 或 timeout 的原因。这就是"安全组件失败时要向安全一侧倒"的具体实现,而且它有独立的、更短的时间预算(1 秒)。

限额、可用范围与生态信号

限额这张表值得单独看,因为它决定了你能写出什么样的 mod(节选):

限制项 值
一个 hook 处理单个事件的执行时间(不含 next 内和 mods API 调用) 10 秒;prompt.edit hook 只有 50 毫秒
.catch handler 执行时间 1 秒
$.process.run 超时 默认 30 秒,最多 10 分钟
$.model.complete 的 maxTokens 默认 1024,最多 64000 或模型输出上限
$.fs.read / $.fs.write 单文件 4 MiB
$.store 总量 4 MiB JSON
$.ui.invalidate('ui.render') 重绘 节流 10 次/秒;终端里可见面板、展开的状态带、提示行下方可到 30 次/秒,更快的调用会被合并
未经用户同意就打开的面板 终端宽度 144 列起才放,用户主动打开过一次后降到 110

另外两个工程细节值得记:$.store 给你 4MiB 的持久状态,而 $.clock.every / $.clock.after 让你在会话里跑定时任务——这已经是一个小型运行时了。

画界面这件事不是哪里都能用:终端(含编辑器内置终端和 JetBrains 插件)和 Desktop 应用的 Code 标签能显示;VS Code 扩展的聊天面板、claude -p、Agent SDK、云会话里 hook 会跑但界面不显示;WSL 会话里插件不可用,两者都没有。所以一个会画东西的 mod 需要自己判断运行环境,并在画不出来的地方退化成一行文字或命令回复。

生态上的信号也值得留意:

  • 一些内置功能本身就是 mod:/diff 面板(cc-plugin-diff)、AGENTS.md 加载(cc-plugin-agents-md)、cc-plugin-you-should-know 那个"帮你盯着长任务"的旁路 agent,都是 mod;它们的源码公开在 anthropics/claude-code 仓库的 mods/ 目录里,带 hooks 模块和测试,是学写 mod 最好的范本。
  • 官方示例 mod 放在 anthropics/claude-code-playground 的 claude-code/mods/:token-weather(把上下文占用画成天气预报)、blast-radius(拦住 rm -rf、force push 这类命令,展示影响范围再让你选)、replay-theater(加一个 /replay 命令回放上一轮文件改动)。
  • 其他 harness 的反应:DeepSeek Harness 直接做了兼容层来"验证",并公开表示自己的插件架构能力是其超集。这件事反过来解释了 Mods 的定位——它定义的不只是一个功能点,而是一套插件 API 的语义边界:事件有哪些、能改写什么、失败怎么办。谁定义了这套语义,谁就在生态里占了位。

参考链接

你会用 Mods 干什么?给上下文做个仪表盘,还是给危险命令加一道闸?评论区聊聊,觉得有用点个赞让更多折腾 Agent 工具链的人看到。


作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

本文首发于 AI人工智能时代,转载请注明出处。

分享给朋友