claude.dev 解读
TutorialsAddy Osmani2026-10-01约 11 分钟阅读原文 ↗

Getting started with Claude Code mods

Mod 是常驻会话的中间件钩子,能改写一切,权限与 Claude Code 等同

主张
6
审视
6
遗漏
3
  1. 01Mods 本质是装在 plugin 里的 JS/TS hook 模块,在会话里常驻,以中间件链的形式拦截工具调用、提示提交、轮次、斜杠命令和 UI 渲染,并能通过 $ 回调 Claude Code 去开 pane、跑进程、注册命令或工具。
  2. 02文章用约 80 行的 Token Weather 讲完了从目录、渲染、$.state 存状态、claude plugin validate/test 到 marketplace 分发的全流程,要求 Claude Code 2.1.287 或更高版本,而且默认开启。
  3. 03守卫型钩子作者自己承认只是安全网;
怎么读这篇
作者主张 · Addy Osmani我们的审视
01

本质就是钩子加插件

Mod 没有引入新的扩展形态,就是 hook 加 plugin:一个 JS/TS 模块,常驻在会话内,能看到每一个事件。

机制拆解

plugin 目录有 .claude-plugin/plugin.json 清单,hooks/hooks.json 的 modules 指向唯一一个模块,模块导出 register(on, options),再用 on(event, matcher?, hook) 挂钩子。所以分发、安装、marketplace 全部复用现有的 plugin 机制(¶45–¶47),不用学新东西。Claude Code 自己的 AGENTS.md 支持和 /diff pane 也是用 mod 写的(¶13),这说明 API 足以承载官方功能。

“Under the hood, mods are hooks, and they ship inside plugins.” ¶1

厂商视角

这是官方教程,口吻偏推广:强调「fun」「nothing new to learn」,鼓励发 X/LinkedIn,并引导用户把 mod 提交到 claude.ai/directory。「Claude Code 自身功能也用 mod 写」这一说法既是能力背书,也是营销。

自己验证到 anthropics/claude-code 公开仓库的 mods/ 目录,读 AGENTS.md 支持和 /diff pane 的源码与测试,核对它们用的是否就是文中公开的 $ API,有没有私有接口。
02

中间件链:观察改写或截答

钩子是中间件链:next(e) 把事件交给下一个插件,链的最底层是 Claude Code 的默认行为。一个钩子可以只观察,也可以改写事件,或者直接给出答复、不再往下传。

机制拆解

签名统一为 ($, e, next):$ 是 API 面(ui/session/state/process/fs/command 等),e 是纯数据形式的事件输入。Token Weather 先 await next(e) 再读数,属于纯观察(¶31)。Blast Radius 不调用 next 而是返回 { deny: ... },属于「answer」(¶53)。¶10 说有三种做法,但对应的表格不在原文中,「观察 / 改写 / 答复」这三类是我按示例归纳的推断。模块运行在独立沙箱里,没有 DOM 和 Node,对外一律经过 $(¶11)。

“Hooks form a chain, like middleware.” ¶10

遗漏

文章没有讲 headless 或无人值守场景:不带 UI 时 ui.render 钩子怎么处理,hold 型钩子(只要 next.signal 不中止就无限等待)会不会让 claude -p 卡住。

自己验证写一个最小 mod:turn.complete 时把一行写进日志,另加一个 tool.call 永远等待的分支。
03

常驻进程胜过每次起壳

Mod 和 settings hook 的本质区别是生命周期:settings hook 每个事件起一次 shell 进程,mod 加载一次后常驻,能持有状态、实时绘制 UI,还能反向调用 Claude Code。

机制拆解

settings hook 通过 stdin/stdout 传 JSON,没有状态,也不能画界面。mod 常驻之后可以订阅多个事件并在它们之间关联,比如 Replay Theater 用 turn.start/turn.complete 把一轮的编辑框在一起(¶63)。它还能 $.command.register 注册斜杠命令(¶64)、$.ui.open 开 pane、注册模型可调用的工具(¶12)。

“A mod is loaded once and stays in the session.” ¶12

遗漏

文章没讨论 mod 对成本和缓存的影响:prompt.submit 改写每条提示、注册模型可调用的工具、热重载期间工具集合变化,都可能改动提示前缀,从而影响缓存命中(推断)。

自己验证在同一任务上分别开、关一个会 prompt.submit 注入文本的 mod,对比 /usage 里的缓存占比;再在会话中途用热重载新增一个 tool,观察缓存读数的变化。
04

状态交给宿主才扛得住重载

状态必须放在宿主持有的 $.state 里,不能放在模块变量里。热重载会重新加载整个模块,模块变量清零,$.state 则能撑过重载,还顺带提供响应式重绘。

机制拆解

每次保存,register 和 session.start 都会重跑(¶69)。$.state 的值要在 types/index.d.ts 的 interface PluginState 里声明,否则 claude plugin validate 报错(¶33–¶34)。在 render 钩子里调用 $.state.get 会自动订阅这次绘制,之后每次 $.state.set 都会触发重绘,不需要手动 $.ui.invalidate(¶35)。

“a hot reload is a fresh load” ¶32

这一点暂无对应的审视。

05

静态校验加真实运行时测试

工具链自带静态校验和运行时测试:validate 会列出模块挂了哪些钩子、调用了哪些 API、读写了哪些 state;test 在真实运行时里,用测试注册的钩子充当 stub,控制宿主的应答。

机制拆解

测试里用 on(...) 注册的钩子排在 mod 后面,相当于替 Claude Code 应答,比如直接给 session.usage 返回指定的 tokens/window。再用 $.ui.mount 挂载组件、ui.find 断言渲染文本,就能验证「turn.complete 之后自动重绘」这样的行为(¶43–¶44)。

“runs the plugin's `*.test.ts` files against the real Claude Code runtime” ¶43

可复现性

validate 和 test 的输出、热重载行为、$.state 撑过重载、pane 在窄终端退化成 band,都可以在本机复现。前提是 CLI 版本达到 2.1.287,而本机 claude update 会被 SNI 阻断。

自己验证挂代理升级后 claude --version,照着 ¶20–¶45 从空目录搭 token-weather,执行 claude plugin validate ./token-weather 和 claude plugin test ./token-weather,比对输出和 ¶42、¶43 是否一致。另外故意用模块变量存状态,保存一次,确认历史会清零。
06

守卫只是安全网非权限

守卫型 mod 只是安全网,不是权限系统。mod 本身是第三方代码,权限和 Claude Code 相同,信任完全靠发布者和用户自己读源码。

机制拆解

Blast Radius 根据命令文本分类,$(…)、alias、间接调用 rm 的脚本都能绕过,作者建议硬拦截用 permission rules(¶59)。钩子每次分派有 10 秒自身时间,但在 $ 调用里等待的时间不计入,所以 hold 型钩子可以无限期等用户按键,直到 next.signal 中止为止(¶56)。¶49 明确说 mod「with the same access Claude Code has」,由发布者编写,安装前应先读 repo。

“It's a safety net, not a permission system.” ¶59

遗漏

安全模型只有一句「像装包一样谨慎」:没说签名或完整性校验,没说 directory 的审核标准,也没说 push 更新后已安装的用户会不会自动拉取新代码。沙箱只说了没有 DOM 和 Node,没说 $.process.run、$.fs、$.http 的权限边界。

自己验证查官方 plugin 和 marketplace 文档,确认有没有签名、版本锁定和自动更新策略。实测一个 mod 能否用 $.process.run 读 ~/.claude.json(里面存着 qcc 的 API key),以此评估凭据暴露面。

作者没有直接谈到这一点。

数字口径

Token Weather 里的百分比是「上一次回答所基于的输入」除以窗口(¶29),不包含本轮将产生的输出,也不是 auto-compact 的实际触发点。「Compact soon」的阈值 90% 是作者自己设的,测试里的 200k 窗口也是 stub 值。「about 80 lines」是粗略的说法。

自己验证在同一个会话里对比 mod 显示的 percent 和状态栏、/usage 的读数,再观察自动压缩实际发生时的百分比,看和 90% 的阈值有多大偏差。
01

机制聚焦

钩子中间件链怎样传递事件

每个钩子自己决定放行、改写还是直接答复,走到链底才轮到 Claude Code 的默认行为

  1. 01你的钩子先运行拿到事件 e,可只观察,也可改写
  2. 02或直接答复如 Blast Radius 返回 deny,不再往下传
  3. 03调用 next(e)把事件交给链上的下一个插件
  4. 04其他插件钩子依次处理同一事件
  5. 05链底默认行为Claude Code 做它本来就会做的事
依据原文 ¶10 ↗
02

核心观点

01

本质就是钩子加插件

Mod 没有引入新的扩展形态,就是 hook 加 plugin:一个 JS/TS 模块,常驻在会话内,能看到每一个事件。

机制拆解

plugin 目录有 .claude-plugin/plugin.json 清单,hooks/hooks.json 的 modules 指向唯一一个模块,模块导出 register(on, options),再用 on(event, matcher?, hook) 挂钩子。所以分发、安装、marketplace 全部复用现有的 plugin 机制(¶45–¶47),不用学新东西。Claude Code 自己的 AGENTS.md 支持和 /diff pane 也是用 mod 写的(¶13),这说明 API 足以承载官方功能。

Under the hood, mods are hooks, and they ship inside plugins.¶1 ↗

02

中间件链:观察改写或截答

钩子是中间件链:next(e) 把事件交给下一个插件,链的最底层是 Claude Code 的默认行为。一个钩子可以只观察,也可以改写事件,或者直接给出答复、不再往下传。

机制拆解

签名统一为 ($, e, next):$ 是 API 面(ui/session/state/process/fs/command 等),e 是纯数据形式的事件输入。Token Weather 先 await next(e) 再读数,属于纯观察(¶31)。Blast Radius 不调用 next 而是返回 { deny: ... },属于「answer」(¶53)。¶10 说有三种做法,但对应的表格不在原文中,「观察 / 改写 / 答复」这三类是我按示例归纳的推断。模块运行在独立沙箱里,没有 DOM 和 Node,对外一律经过 $(¶11)。

Hooks form a chain, like middleware.¶10 ↗

03

常驻进程胜过每次起壳

Mod 和 settings hook 的本质区别是生命周期:settings hook 每个事件起一次 shell 进程,mod 加载一次后常驻,能持有状态、实时绘制 UI,还能反向调用 Claude Code。

机制拆解

settings hook 通过 stdin/stdout 传 JSON,没有状态,也不能画界面。mod 常驻之后可以订阅多个事件并在它们之间关联,比如 Replay Theater 用 turn.start/turn.complete 把一轮的编辑框在一起(¶63)。它还能 $.command.register 注册斜杠命令(¶64)、$.ui.open 开 pane、注册模型可调用的工具(¶12)。

A mod is loaded once and stays in the session.¶12 ↗

04

状态交给宿主才扛得住重载

状态必须放在宿主持有的 $.state 里,不能放在模块变量里。热重载会重新加载整个模块,模块变量清零,$.state 则能撑过重载,还顺带提供响应式重绘。

机制拆解

每次保存,register 和 session.start 都会重跑(¶69)。$.state 的值要在 types/index.d.ts 的 interface PluginState 里声明,否则 claude plugin validate 报错(¶33–¶34)。在 render 钩子里调用 $.state.get 会自动订阅这次绘制,之后每次 $.state.set 都会触发重绘,不需要手动 $.ui.invalidate(¶35)。

a hot reload is a fresh load¶32 ↗

05

静态校验加真实运行时测试

工具链自带静态校验和运行时测试:validate 会列出模块挂了哪些钩子、调用了哪些 API、读写了哪些 state;test 在真实运行时里,用测试注册的钩子充当 stub,控制宿主的应答。

机制拆解

测试里用 on(...) 注册的钩子排在 mod 后面,相当于替 Claude Code 应答,比如直接给 session.usage 返回指定的 tokens/window。再用 $.ui.mount 挂载组件、ui.find 断言渲染文本,就能验证「turn.complete 之后自动重绘」这样的行为(¶43–¶44)。

runs the plugin's `*.test.ts` files against the real Claude Code runtime¶43 ↗

06

守卫只是安全网非权限

守卫型 mod 只是安全网,不是权限系统。mod 本身是第三方代码,权限和 Claude Code 相同,信任完全靠发布者和用户自己读源码。

机制拆解

Blast Radius 根据命令文本分类,$(…)、alias、间接调用 rm 的脚本都能绕过,作者建议硬拦截用 permission rules(¶59)。钩子每次分派有 10 秒自身时间,但在 $ 调用里等待的时间不计入,所以 hold 型钩子可以无限期等用户按键,直到 next.signal 中止为止(¶56)。¶49 明确说 mod「with the same access Claude Code has」,由发布者编写,安装前应先读 repo。

It's a safety net, not a permission system.¶59 ↗

03

批判性审视

厂商视角

这是官方教程,口吻偏推广:强调「fun」「nothing new to learn」,鼓励发 X/LinkedIn,并引导用户把 mod 提交到 claude.ai/directory。「Claude Code 自身功能也用 mod 写」这一说法既是能力背书,也是营销。

自己验证到 anthropics/claude-code 公开仓库的 mods/ 目录,读 AGENTS.md 支持和 /diff pane 的源码与测试,核对它们用的是否就是文中公开的 $ API,有没有私有接口。
数字口径

Token Weather 里的百分比是「上一次回答所基于的输入」除以窗口(¶29),不包含本轮将产生的输出,也不是 auto-compact 的实际触发点。「Compact soon」的阈值 90% 是作者自己设的,测试里的 200k 窗口也是 stub 值。「about 80 lines」是粗略的说法。

自己验证在同一个会话里对比 mod 显示的 percent 和状态栏、/usage 的读数,再观察自动压缩实际发生时的百分比,看和 90% 的阈值有多大偏差。
可复现性

validate 和 test 的输出、热重载行为、$.state 撑过重载、pane 在窄终端退化成 band,都可以在本机复现。前提是 CLI 版本达到 2.1.287,而本机 claude update 会被 SNI 阻断。

自己验证挂代理升级后 claude --version,照着 ¶20–¶45 从空目录搭 token-weather,执行 claude plugin validate ./token-weather 和 claude plugin test ./token-weather,比对输出和 ¶42、¶43 是否一致。另外故意用模块变量存状态,保存一次,确认历史会清零。
遗漏

文章没有讲 headless 或无人值守场景:不带 UI 时 ui.render 钩子怎么处理,hold 型钩子(只要 next.signal 不中止就无限等待)会不会让 claude -p 卡住。

自己验证写一个最小 mod:turn.complete 时把一行写进日志,另加一个 tool.call 永远等待的分支。
遗漏

安全模型只有一句「像装包一样谨慎」:没说签名或完整性校验,没说 directory 的审核标准,也没说 push 更新后已安装的用户会不会自动拉取新代码。沙箱只说了没有 DOM 和 Node,没说 $.process.run、$.fs、$.http 的权限边界。

自己验证查官方 plugin 和 marketplace 文档,确认有没有签名、版本锁定和自动更新策略。实测一个 mod 能否用 $.process.run 读 ~/.claude.json(里面存着 qcc 的 API key),以此评估凭据暴露面。
遗漏

文章没讨论 mod 对成本和缓存的影响:prompt.submit 改写每条提示、注册模型可调用的工具、热重载期间工具集合变化,都可能改动提示前缀,从而影响缓存命中(推断)。

自己验证在同一任务上分别开、关一个会 prompt.submit 注入文本的 mod,对比 /usage 里的缓存占比;再在会话中途用热重载新增一个 tool,观察缓存读数的变化。
本篇未覆盖(3)
风格