Claude Code Mods 把钩子搬进了进程内,你的威胁模型得重画
💡 工具推荐: JSON 格式化、YAML 格式化、Markdown 编辑器
Anthropic 在 Claude Code v2.1.287 里上线了 Mods,很多人第一眼看到的是「可以自己画界面了」。真正重要的不是能画界面,而是代码跑在哪里。settings hooks、skills、status line、MCP server 都是从外部工作的——各自跑一个脚本,或者给 Claude 塞文本、塞工具。mod 不是,它是一个由 JavaScript / TypeScript 事件处理器组成的插件,Claude Code 在自己的进程内调用它。范围是 CLI 与 Desktop 的 v2.1.290、VS Code 扩展的 v2.1.299。这一句话同时解释了你为什么能做到以前做不到的事,以及你为什么要重新算一遍风险。
mod 不是 agent 旁边的脚本,而是 agent 身体里的代码
一、mod 到底是什么:一个目录,一个 register 函数
官方文档给的形态很朴素:一个插件目录,.claude-plugin/plugin.json 是插件 manifest,mods 不新增任何必填字段;hooks/hooks.json 里写一个 modules 数组,指向 hooks/register.js。这个 register.js 是入口,导出 register(on, options),options 里放 manifest 声明的 userConfig 值,带默认值。真正的逻辑都在 on 里:on 接收事件名、一个可选的 matcher(对事件字段的过滤,比如 { tool: 'Bash' })、以及 hook 函数本身,签名是 async ($, e, next) => next(e)。$ 是 mods API,e 是深度冻结的事件输入——想改就复制一份传给 next,别原地改。next 是下一个处理器,也就是中间件语义:跑完后面的 hook,再跑 Claude Code 自己的行为。写成这样,一个 mod 就不是「旁挂的脚本」,而是请求链上的一环。
# 1) The shape on disk. No required manifest fields are added by a mod.
deploy-guard/
.claude-plugin/
plugin.json # plugin manifest (mods add no required fields)
hooks/
hooks.json # { "modules": ["./register.js"] }
register.js # entry point: exports register(on, options)
types/
index.d.ts # only needed if you use $.state or add a namespace
# hooks.json
{
"modules": ["./register.js"]
}二、$ 暴露了什么:几乎是整个 agent
这是这篇文档里最需要坐下来读十分钟的表格。$ 按命名空间分组:$.ui 有 open、close、panes、toast、notify、ask、selection,能画也能问;$.tool 能 register、call、check、list;$.model 能 complete、fork、classify;$.prompt 能 submit、read、fill;$.turn 能 abort;$.session 能拿 messages、cwd、model、turns、repo、usage;还有 $.fs、$.env、$.config、$.settings、$.command、$.agent。换句话说,一个 mod 能读写文件、跑命令、发网络请求、改另一个模型的调用、替用户提交提示词。文档自己也说得很直白:mod 是以你的权限运行的代码,它能读写你的文件、启动进程、发起网络请求;只从你信任的作者和市场安装。这不是免责声明,这是设计说明。你把一个 mod 加进仓库,等于把一段代码加进 agent 的进程。
// 2) register(on, options) is the whole entry point.
// on(event, matcher, hook). Return next(e) to pass through,
// { deny: reason } to block, or { value } to answer for the engine.
export function register(on, options) {
const blocked = options.blockedCommands ?? ["rm -rf /"];
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
const cmd = String(e.input?.command ?? "");
for (const bad of blocked) {
if (cmd.includes(bad)) {
return { deny: "deploy-guard: refused a destructive command" };
}
}
return next(e); // unchanged, continue down the chain
});
}三、顺序和时间都是显式的,忽略它们就会变慢
mod 的运行顺序由 tier 决定:prepend、user、append、builtin。next.origin 会告诉你事件是谁发的——Claude Code 自己是 { plugin: 'engine', tier: 'core' }。next.to(e, tier) 可以跳到后面的层级,比如跳过用户装的 mod,但只有 prependPlugins 或 appendPlugins 里的 mod 才能这么干。时间也是显式的:next.budget.ms 是整个时间上限,next.budget.remainingMs 是现在还剩多少。这两个字段必须用起来。一个 hook 卡住 2 秒,不会只让这个 hook 变慢,它会让整个工具调用变慢,而用户只会觉得「今天 Claude Code 有点卡」。文档还提到 next.error,只出现在 .catch 处理器里,kind 是 throw 或 timeout;还有 kind 为 re-entry 的情况——说明这次事件是从 mod 自己的 API 调用里发出来的,别写成死循环。
每一次 mods API 调用,本身也是一个可被拦截的事件
// 3) The $ argument is the mods API. Every namespace method exists,
// and every one of those calls is itself an event.
on("turn.step", async ($, e, next) => {
const { context, cost } = $.session.usage();
// context: { tokens, window, percent } cost: reported spend so far
await $.ui.toast({
text: `context ${context.percent}% of ${context.window}`,
kind: context.percent > 80 ? "warn" : "info",
});
return next(e);
});
// The methods you will actually reach for first:
// $.fs.read / write $.tool.register / call
// $.model.complete / classify $.prompt.submit / read
// $.session.messages / cwd / repo / authorize
// $.ui.panes / open / notify / ask / selection四、威胁模型:进程内的代码,进程外的信任
以前的扩展模型是「外面跑一个进程,用 stdio 或 HTTP 跟 agent 说话」。那种模型下,攻击面边界清楚:进程边界就是信任边界。mod 把这条边界抹了。它能替换 Claude Code 自己画的东西——工具调用的那一行、spinner、提问对话框;它能 hold 住一个工具调用,先问用户一句再决定放不放;它能改请求去另一个模型。这些能力放在企业里,每一条都需要审批。文档给了两个组织级开关的名字:allowManagedModsOnly(只跑属于你组织的 mod 和内置 mod,用户的 settings hooks 照旧)和 allowModsToOverrideDenyRules。第二个名字本身就是一个提醒:能被 mod 覆盖的 deny 规则,不算安全边界。
// 4) Order and time are both explicit. Ignoring them is how a mod
// turns a 30ms tool call into a 3-second one.
// next.origin.tier : prepend | user | append | builtin
// next.budget.ms : total limit, next.budget.remainingMs : left now
// next.to(e, tier) : skip ahead (append | builtin | core)
on("model.complete", async ($, e, next) => {
if (next.budget.remainingMs < 50) return next(e); // do not risk a timeout
const cheap = await $.model.classify({
state: e.request?.prompt ?? "",
choice: "path=trivial,needs-frontier",
});
if (cheap.choice === "trivial" && next.origin.tier !== "prepend") {
return next.to(e, "builtin"); // skip the mods a user installed
}
return next(e);
});五、先做什么,以及先别做什么
如果要在自己仓库里试第一个 mod,我建议挑「只观察、不改」的那种:用 $.session.usage() 拿 context 的 tokens、window、percent 和 cost,在状态区显示一个数字。它的失败代价接近零,而且很快就能验证你对 API 的理解对不对。反过来,先别做的第一件事是「自己写一个权限网关」。理由不是能力问题,是审计问题:一个决定哪些 Bash 命令能跑的 mod,等于把安全策略藏进了一段 TypeScript 里,而没有人 review 过它。文档说 mods 未经沙箱化,但 sandbox 会限制直接访问,真正的做法是:版本锁定、只装你读过代码的 mod、要求 .catch 处理器、以及把「它有没有碰 $.fs 插件目录之外的东西」写进 review 清单。
# 5) The review questions that decide whether a mod ships.
# A mod is code that runs with your permissions. It can read and
# write files, start processes, and make network requests.
# Anthropic's own advice: install mods only from authors and
# marketplaces you trust.
mod_review:
- id: deploy-guard
version: 2.1.4
pinned: true # pin the version, not a range
reads:
- ".claude-plugin/plugin.json"
- "hooks/register.js"
- "types/index.d.ts"
questions:
- "Does it touch $.fs outside the plugin root?"
- "Does it call $.env.get or read credentials?"
- "Does it hold a tool call long enough to hit next.budget?"
- "Does it ship a .catch handler, or will an error kill the turn?"
org_guard:
allowManagedModsOnly: true # org mods + built-ins only
📌 常见问题 FAQ
Mods 和我已经在用的 settings hooks 有什么区别?
settings hooks 是从外部工作的:它在设置文件里配置成一条 shell 命令、一个 HTTP 请求或一段提示词,由 Claude Code 去执行。mod 的 handler 是运行在 Claude Code 进程内的函数,所以它能画界面、重画 Claude Code 自己的界面元素、共享状态,也能拦截 API 调用。文档把两者都叫 hook,设置文件里那种叫 settings hook。
Mods 是沙箱化的吗?
不是。文档的原话是:mod 是以你的权限运行的代码,它可以读写你的文件、启动进程、发起网络请求,只从你信任的作者和市场安装。所以 install 这个动作本身就是一次安全决策。
一个 mod 能拿到我所有的会话内容吗?
通过 $.session,它能拿到 messages、cwd、repo、model、turns、usage 等。这是设计上就给的权限,不是漏洞。问题在于你装的那个 mod 会不会把这些内容发出去——而它能发网络请求。
组织怎么限制?
文档列出了组织级管理能力,其中两个开关是 allowManagedModsOnly(只允许组织自己的 mod 和 Claude Code 内置 mod 跑 hook,用户的 settings hooks 照旧运行)和 allowModsToOverrideDenyRules(允许 mod 覆盖 deny 规则,默认不该开)。
我该现在就写一个 mod 吗?
先对照一遍:你的需求能不能用 settings hook、skill 或 MCP server 解决?文档本身就叫你先比较。这三样都能从外部做完的事,就别急着把代码搬进进程里。真正只有 mod 能做的,是界面层、工具调用拦截与改写、以及 hook 之间共享状态这三类。
🔧 推荐工具
📚 参考资料
- Claude Code Docs — Mods overview (what a mod is, what it can do, trust warning)
- Claude Code Docs — Mods reference (v2.1.290 CLI/Desktop, v2.1.299 VS Code; events, mods API, tiers)
- Claude Code Docs — Mods API methods (namespaces: ui, tool, model, prompt, session, fs, …)
- GitHub — Anthropic's claude-code-playground, home of the sample mods directory