Claude Code 的 mods 允许你改会话内部的行为:响应一次工具调用、往界面里加元素、在事件送达其余部分之前改写它。这能力很强,代价是它给的权限也很大——mod 是跑在你机器上的可执行代码,能碰到你这个用户能碰到的一切。

Flavio Copes 用一个很小的例子把这套机制讲清楚了:写一个 mod,让每条 Bash 命令都停下来等你明确选择,才继续往下走。

mod 和 settings hook 不是一回事

settings hook 是围绕事件配置的外部命令:Claude Code 把输入递过去,读它的返回。mod 则是注册在 Claude Code 内部的 JavaScript 或 TypeScript 函数,处理函数构成一条链——调用 `next(e)` 继续、把改过的事件传下去、或者直接返回结果自己处理。

判断标准很简单:普通 hook 够用,就别上 mod。mod 更适合需要改界面、或者要替换内建行为的场景。

这个例子怎么写的

插件结构很常规:`.claude-plugin/plugin.json` 描述自己,`hooks/hooks.json` 指向模块,`hooks/preview.mjs` 是逻辑。

核心就几行:监听 `tool.call`,matcher 限定 `tool: 'Bash'`;用 `$.ui.ask` 弹一个问题,选项是"Run once"和"Cancel";答案不是"Run once"就返回 `{ deny: ... }`,并且不调用 `next`。

它刻意不用正则去识别"危险命令"。作者的理由很实在:匹配 `--force` 会漏掉 `-f`、漏掉别名、漏掉另一个效果相同的程序。与其猜语义,不如每条都问。

那个容易写错的地方

注册末尾必须挂 `.catch`。因为 Claude Code 的默认行为是:hook 在调用 `next` 之前失败,就跳过它,让链继续往下走——超时、返回非法结果,都算这一类。也就是说,不写错误处理,你的"守门员"崩了之后命令会畅通无阻。

正确写法是:只要 `next` 还没被调用,就返回 deny;如果已经调用过,就把已有的结果取回来,绝不能重跑一次。另外,等用户回答的时间不计入 hook 的超时限制。

还有一层语义要说清:批准只代表放行到下一环,Claude Code 自己的权限检查照旧。mod 的提问和工具本身的权限是两个独立决策。

不执行命令也能测

因为 handler 就是个普通函数,可以用 `node:test` 把 `ask` 和 `next` 都替换掉,纯测分支。作者写了八条用例:拒绝类回答("Cancel"、"Maybe later"、undefined)、提问抛异常、批准时事件原样透传、宿主在 next 之前抛错或超时、next 之后失败要保留已有结果。全程不碰真实命令。

上线前还有两步验证:`claude plugin validate ./bash-preview` 检查插件声明;然后用 `--plugin-dir` 在一次性项目里加载,先拿 `git diff --stat` 这种无害命令把两个选项都点一遍,包括直接关掉弹窗,确认取消真的没执行、批准真的走到了正常路径。

预览的边界

作者自己点明了局限:一条 shell 命令的含义取决于工作目录、环境变量、它读了哪些文件。把命令文本显示出来帮你审,并不等于完整描述了它的效果。要做生产级管控,应该用带明确目标的结构化操作,并把预览绑定到那个确切意图上。

更要紧的是,mod 不是沙箱。它能访问文件、进程、网络和密钥。而且链上还有别的 mod,它们可能在你提问之后改写事件。要做组织级强制,得用受管配置加隔离边界,别把用户自己装的小插件当成安全边界。

阅读原文 → 返回 AI 技术文档

内容与图片版权归原作者所有 · 原文: https://flaviocopes.com/claude-code-mods/