Claude Code Mods 教程:从零写出第一个 Mod、测试与安装,附 15 个社区案例
更新于

Claude Code 的 mod 是一种插件,由运行在 Claude Code 内部的 JavaScript 或 TypeScript 函数组成。mod 可以绘制一个面板,或者在输入框上方加一行;可以添加立即执行的斜杠命令;还能在工具调用执行之前拦下它或改写它。mod 随 2026 年 10 月 1 日发布的 Claude Code 2.1.287 上线,默认开启。
按你的目的,选最快的上手方式:
| 你想要 | 这样做 |
|---|---|
| 不写代码,直接得到一个 mod | 在会话里描述它,比如「做一个 mod,显示每一轮花了多长时间」。Claude 会写好代码,并询问一次是否开启热重载 |
| 安装别人写的 mod | 在终端会话里运行 /plugin install <name>@<marketplace>,或在 shell 里运行 claude plugin install <name>@<marketplace> |
| 开发时加载一个文件夹 | claude --plugin-dir ./my-mod |
| 试用内置的侧边 agent | /plugin enable cc-plugin-you-should-know@builtin |
先检查版本。claude --version 应该输出 2.1.287 或更高版本。Desktop 应用自带一份 Claude Code,那份从 2.1.286 起就支持 mod。在 Code 标签页里输入 /status,可以看到你用的是哪个版本。
实测范围:2026 年 10 月 7 日,我在 macOS 上的 Desktop 应用 Code 标签页里,用 Claude Code 2.1.293 做了三个 mod:一个命令守卫、一个显示在输入框上方的轮次计量条,以及一个用于 Python 工作的
/pyenv命令。三个 mod 都通过了claude plugin validate,也通过了claude plugin test下的 8 个自动化测试。我还在开启热重载的会话里实际跑了守卫。社区相关的章节参考了三类资料:日本开发者 10 月 1 日到 7 日在 Zenn、Qiita 和 note.com 上发布的约 50 篇日文文章,官方 mods 文档 (opens in a new tab),以及 awesome-claude-code-mods (opens in a new tab) 合集 10 月 6 日的扫描数据。
- Claude Code Mods 教程:从零写出第一个 Mod、测试与安装,附 15 个社区案例
- Pi Coding Agent 1.0 使用教程:安装、接入模型(含 Ollama 本地模型)并跑通第一个任务
- Claude Code 现在会读 AGENTS.md:配置方法、四种加载模式,以及如何与 Codex、OpenCode、Cursor 共用一份文件
- GPT Image 2.5 使用指南:Flare 与 Sunburst 怎么选、API 调用与价格
- DeepSeek Harness 安装教程:一条命令装好 DSH 并跑通第一个任务
- Runcell Science:面向科研的开源 Claude Science 替代方案
- Mac 怎么不休眠:合盖继续运行 Codex、Claude Code 和本地 AI Agent
- OpenClaw vs ZeroClaw vs Pi Agent vs Nanobot:2026 年该选哪个 AI Agent 技术栈?
- Claude Code 能分析 Jupyter Notebook 吗?Data Science 场景下它到底做了什么
- Claude Code Routines 是什么?AI Agent 定时任务与自动触发指南
- Claude Code Desktop 绕过权限:如何开启 Bypass permissions
- 如何用 Google 的 A2A 协议构建两个 Python Agent:一步步教程
- 2025 年 Python 增长最快的 10 个数据可视化库
Mod 是什么,什么时候该用别的扩展方式
在 mod 出现之前,扩展 Claude Code 都是从外部入手:settings hook、skill、状态栏脚本,或者 MCP 服务器。区别在于代码在哪里运行。settings hook 是 Claude Code 从外部启动的脚本。mod 的函数则运行在 Claude Code 自己的进程里,所以 mod 能在界面上绘制内容,其他几种都做不到。
官方概览页的对比大致如下:
| Mod | Settings hook | Skill | MCP 服务器 | |
|---|---|---|---|---|
| 是什么 | 插件里的 JS 或 TS 函数 | settings.json 里的一条 shell 命令、HTTP 调用或 prompt | 一个写着指令的 SKILL.md 文件 | 为 Claude 提供工具的外部进程 |
| 能否在 UI 上绘制 | 能 | 不能 | 不能 | 不能 |
| 能改变什么 | 工具调用、提示词、命令、轮次、UI | 调用是否放行、调用的参数和结果、额外的上下文 | Claude 知道什么、做什么 | Claude 有哪些工具 |
| 适合的场景 | 你想要面板、横条、即时命令,或者想改写某个事件 | 你已经有一个用来拦截或记日志的脚本 | 你总在反复粘贴同一段指令 | Claude 需要连接外部系统 |
有个命名上的坑。在 mods 的文档页里,「hook」指的是 mod 里的函数,旧的那种叫「settings hook」。settings hook 仍然可用,也没有被废弃。一个插件可以同时包含 mod、skill 和 MCP 服务器。
Mod 能在哪些环境运行
hook 几乎在哪里都会运行,但只有终端和 Desktop 应用会绘制界面。
| Claude Code 的运行环境 | hook 是否运行 | 是否显示 mod UI |
|---|---|---|
终端里的 claude,包括编辑器内置终端和 JetBrains | 是 | 是 |
| Desktop 应用的 Code 标签页 | 是 | 是,仅限终端的元素除外 |
| Desktop 应用的 WSL 会话 | 否 | 否 |
| VS Code 扩展的聊天面板 | 是 | 否 |
claude -p 和 Agent SDK | 是 | 否 |
| 云端会话 | 是,前提是插件能进入该会话 | 否 |
这张表来自官方概览页。它意味着守卫类 mod 在 VS Code 和 claude -p 里照样能保护你,而计量条在这些环境里没有地方可画。
还要留意机器上有两份 Claude Code 的情况。好几位日本作者遇到过让人困惑的 validate 报错,原因是 Homebrew 装的旧版终端 CLI(他们报告的版本是 2.1.226 和 2.1.234)和 Desktop 应用自带的 2.1.286 同时存在。每个用到 Claude Code 的地方,都要单独检查版本。
Mod 的目录结构
一个小型 mod 只需要三个文件:
safe-shell/
├── .claude-plugin/
│ └── plugin.json name, version, description
└── hooks/
├── hooks.json { "modules": ["./register.ts"] }
└── register.ts your coderegister.ts 导出一个函数:register(on)。每调用一次 on(event, matcher, hook) 就添加一个 hook,每个 hook 都接收同样的三个参数。
$是引擎。你自己代码之外的一切都要经过它:$.ui.toast、$.process.run、$.state、$.model.complete。e是事件,比如 Claude 即将运行的那条 Bash 命令。next(e)把事件交给其他 mod,最后交给 Claude Code。不调用它就直接 return,你的 hook 就自己给出了答复。调用next({ ...e, command }),就能改变接下来实际发生的事。
模块里没有 Node,也没有 DOM。正是这个限制,让 Claude Code 能在 mod 运行之前列出它调用的所有东西。随便找一个 mod 文件夹试试:
claude plugin validate ./safe-shellMod 1:拦截破坏性命令的守卫
如果你让 Claude 在 auto 模式或 绕过权限(bypass permissions) 模式下运行,那么在 git reset --hard 之前没有任何东西会拦住它。这个 mod 会拒绝三类 shell 命令,并且不让 .env 文件进入对话。
import type { Register } from 'claude-code'
// Shell commands an agent should never run without a human typing them.
const BLOCKED: { pattern: RegExp; reason: string }[] = [
{
pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+(\/|~|\$HOME)\/?\*?(\s|$)/,
reason: 'recursive delete of / or the home folder',
},
{
pattern: /\bgit\s+push\b.*(--force\b|--force-with-lease\b|\s-f\b).*\b(main|master)\b/,
reason: 'force push to main or master',
},
{
pattern: /\bgit\s+reset\s+--hard\b/,
reason: 'git reset --hard throws away uncommitted work',
},
]
// .env, .env.local, config/.env.production ... but not .env.example.
const isSecretEnvFile = (path: string) =>
/(^|\/)\.env(\.[^/]+)?$/.test(path) && !/\.(example|sample|template)$/.test(path)
export const register: Register = on => {
on('tool.call', { tool: 'Bash' }, ($, e, next) => {
const hit = BLOCKED.find(rule => rule.pattern.test(e.command))
if (hit === undefined) {
return next(e)
}
$.ui.toast(`safe-shell blocked: ${hit.reason}`)
return { deny: `safe-shell: blocked (${hit.reason}). Ask the user to run it themselves.` }
}).catch(($, e, next) =>
// Fail closed: if the guard itself breaks, refuse instead of letting the command through.
next.called ? next(e) : { deny: 'safe-shell: the guard failed, so the command was not run.' },
)
for (const tool of ['Read', 'Edit', 'Write'] as const) {
on('tool.call', { tool }, ($, e, next) =>
isSecretEnvFile(e.file_path)
? { deny: `safe-shell: ${e.file_path} holds secrets and stays out of the conversation.` }
: next(e),
).catch(($, e, next) =>
next.called ? next(e) : { deny: 'safe-shell: the guard failed, so the file was not touched.' },
)
}
}比正则表达式更重要的是两个细节。
守卫必须在调用 next 之前做出判断。next 一旦执行,命令就已经跑完了。之后再返回 deny,什么也撤销不了。
用 .catch 让守卫 fail closed。抛出异常的 hook,或者超出 10 秒时间预算的 hook,都会被跳过,而被跳过的守卫会让命令直接通过。next.called ? next(e) : deny 这个处理函数会改为拒绝,也就是守卫自己出错时不放行。validate 会列出每个起拦截作用的 hook(gating hook),并标明它有没有 .catch。检查别人写的守卫时,第一件事就是看这里:
❯ ./register.ts hooks: tool.call{tool=Bash}, tool.call{tool=Read|Edit|Write}
❯ ./register.ts gating hook with .catch: tool.call{tool=Bash}
❯ ./register.ts gating hook with .catch: tool.call{tool=Read|Edit|Write}
❯ ./register.ts calls: $.ui.toast
✔ Validation passed不开会话也能测试
claude plugin test 会在引擎上运行 mod 的 *.test.ts 文件,不需要会话、登录或网络。测试工具包里,你的 mod 下层什么都没有,所以每个测试都要注册替身 hook(相当于测试里的 stub),接住 mod 往下传递的调用:
import { expect, test } from 'claude-code/testing'
const ran = { stdout: 'ran', stderr: '', interrupted: false }
test('blocks rm -rf on the home folder', async ($, on) => {
on('tool.call', { tool: 'Bash' }, () => ({ result: ran }))
const answer = await $.tool.call({ tool: 'Bash', command: 'rm -rf ~' })
expect(answer.deny).toContain('recursive delete')
})
test('lets ordinary commands through', async ($, on) => {
on('tool.call', { tool: 'Bash' }, () => ({ result: ran }))
const answer = await $.tool.call({ tool: 'Bash', command: 'rm -rf ./build && python -m pytest -q' })
expect(answer.deny).toBeUndefined()
})完整的测试文件里有四个测试:
(pass) blocks rm -rf on the home folder [16.23ms]
(pass) blocks a force push to main [7.73ms]
(pass) lets ordinary commands through [7.22ms]
(pass) keeps .env out of Read but allows .env.example [9.40ms]
4 pass
0 fail我第一次运行时报错 on("tool.call") after the test first called $。替身要在每个测试的开头注册,也就是在第一次调用 $ 之前。
在真实会话里的表现
开启热重载后,我让 Claude 运行 echo "the words git reset --hard inside an echo"。守卫以「blocked (git reset --hard throws away uncommitted work)」拒绝了它,同时弹出了一条 toast。可这条 echo 本身是无害的。模式匹配分不清真正的命令和只是提到这条命令的文本;藏在变量、脚本或别名里的命令,它也会漏掉。rafi-guard、delete-guard、board-guard 等类似守卫的日本作者,都报告过这两个问题。
把守卫 mod 当作第二道防线。绝对不能发生的操作,也要写进权限的 deny 规则。反方向的风险也要清楚。官方文档说,在没有托管设置(managed settings)的个人电脑上,mod 可以批准一个被 deny 规则拒绝的调用。mod 没有沙箱隔离,它以你的权限运行。
Mod 2:输入框上方的计量条
输入框上方的横条和侧边面板,是 mod 最常用的绘制位置。在 10 月 6 日的合集扫描中,41% 的 mod 在横条里绘制,40% 会打开面板。这个 mod 显示上一轮的耗时、工具调用次数和 token 数,还带一个 Hide 按钮。
绘制时要读取的值放在 $.state 里,每个值都在一个小的类型文件里声明:
// types/index.d.ts, named in plugin.json as "types": "./types/index.d.ts"
export type TurnStats = {
seconds: number
tools: number
inputTokens: number
outputTokens: number
model: string | null
}
declare module 'claude-code' {
interface PluginState {
'turn-meter': { last: TurnStats | null; isHidden: boolean }
}
}下面这些 hook 负责统计工具调用次数,从 turn.complete 读取 durationMs 和 usage,并绘制横条:
import { atom, read, update } from 'claude-code'
import type { Register } from 'claude-code'
import type { TurnStats } from '../types'
const last = atom({ plugin: 'turn-meter', key: 'last' } as const, null)
const isHidden = atom({ plugin: 'turn-meter', key: 'isHidden' } as const, false)
const formatTokens = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))
export const register: Register = on => {
let tools = 0
on('prompt.submit', ($, e, next) => {
tools = 0
return next(e)
})
on('tool.call', ($, e, next) => {
// Count the main conversation's calls; subagents carry an agentId.
if (e.agentId === undefined) tools += 1
return next(e)
})
on('turn.complete', async ($, e, next) => {
if (e.agentId !== undefined) return next(e)
const usage = e.usage
const stats: TurnStats = {
seconds: Math.round(e.durationMs / 1000),
tools,
inputTokens: usage
? usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens
: 0,
outputTokens: usage?.output_tokens ?? 0,
model: usage?.model ?? null,
}
await update($, last, () => stats)
if (stats.seconds > 120) $.ui.toast(`That turn took ${stats.seconds}s`)
return next(e)
})
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
const stats = await read($, last)
if (e.props.hasSurvey || stats === null || (await read($, isHidden))) return next(e)
const { Box, Button, Text } = $.ui.resolve(e)
const model = stats.model === null ? '' : ` · ${stats.model}`
// The band is shared by every mod: keep what the mods beneath drew, then add a row.
const below = await next(e)
return (
<Box flexDirection="column">
{below}
<Box>
<Text dimColor>
{`Last turn: ${stats.seconds}s · ${stats.tools} tool calls · ${formatTokens(stats.inputTokens)} in / ${formatTokens(stats.outputTokens)} out${model} `}
</Text>
<Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
</Box>
</Box>
)
})
}测试中,无论在终端还是 Desktop 界面,横条显示的都是:
Last turn: 42s · 0 tool calls · 18.2k in / 800 out · claude-opus-5-5 [ Hide ]做这个 mod 时我学到三件事:
- 要绘制的值放在
$.state里,不要放在模块变量里。热重载会重新运行register,把你的变量重置掉,而$.state会保留下来。/clear会让一切从头开始,如果某个值必须在它之后继续存在,就在session.end时写入$.store。 - 横条是共享的。要调用
next(e),并把它的结果放进你自己的Box,否则其他 mod 的行都会被你挡住。一位 note.com 作者在想明白这一点之前,就有一条横条被别的 mod 盖掉了。 Text元素不保留key。测试时按显示的文字来查找:ui.find({ type: 'Text', text: /Last turn/ })。
Mod 3:/pyenv,解决 Claude 用错 Python 的问题
我去找数据科学方向的 mod,发现这块还是空白。10 月 6 日扫描到的 2,690 个 mod 里,没有一个在名称或描述里提到 Jupyter、pandas、DataFrame、Polars、venv 或 conda。
这个 mod 要解决的问题,在数据工作里很常见。Claude 的 Bash 工具会运行 PATH 上排在最前面的 python3,而它往往不是你项目的环境。下面是探测脚本在我机器上的输出:
python: /Users/<you>/.local/bin/python3 (3.14.2)
env: none (system or tool-managed Python)
pandas: 3.0.1
numpy: 2.4.2
polars: not installed
matplotlib: 3.11.0
pygwalker: not installed当时没有激活任何虚拟环境,所以 Claude 如果执行 pip install,包会装进全局解释器。
/pyenv 命令会运行这个探测脚本,并分两部分作答。text 是你看到的那一行。context 是只有模型能读到的备注,这样 Claude 接下来的 Bash 调用就知道正在用哪个解释器,不用你再提醒一次。
import type { Register } from 'claude-code'
// Reads package versions from metadata instead of importing them, so it stays fast.
const PROBE = `
import os, sys
from importlib.metadata import PackageNotFoundError, version
env = os.environ.get("VIRTUAL_ENV") or os.environ.get("CONDA_PREFIX") or "none (system or tool-managed Python)"
print(f"python: {sys.executable} ({sys.version.split()[0]})")
print(f"env: {env}")
for name in ("pandas", "numpy", "polars", "matplotlib", "pygwalker"):
try:
print(f"{name}: {version(name)}")
except PackageNotFoundError:
print(f"{name}: not installed")
`
export const register: Register = on => {
on('session.start', async ($, e, next) => {
await $.command.register({
name: 'pyenv',
description: "Show which Python, environment and data packages this session's shell uses",
})
return next(e)
})
on('command.run', { command: 'pyenv' }, async $ => {
try {
const probe = await $.process.run(['python3', '-c', PROBE], { timeoutMs: 15_000 })
if (probe.exitCode !== 0) {
return { text: `pyenv: python3 exited with ${probe.exitCode}: ${probe.stderr.trim().slice(0, 300)}` }
}
const report = probe.stdout.trim()
// `text` is the row you see; `context` is a note only the model reads.
return {
text: report,
context: [`Python environment of this session's shell, from /pyenv:\n${report}`],
}
} catch (error) {
return { text: `pyenv: could not run python3 (${String(error).slice(0, 200)})` }
}
})
}mod 的斜杠命令会立刻执行你的函数,不需要模型跑一轮,即使 Claude 正在忙也可以。$.process.run 接收的是参数列表,不经过 shell;如果不设置 timeoutMs,30 秒后超时。
$.process.run 看到的 PATH 和 Claude 的 Bash 工具一样吗?有位社区作者发现,mod 启动的进程里找不到 nvm 装的 node,于是我写了一个临时的探测 mod 来验证。在我的 Desktop 会话里,直接调用 $.process.run 拿到的 PATH 和 Bash 工具完全一致,python3 也是同一个。改用登录 shell zsh -lc 运行探测脚本,结果反而错了:它解析到了 python.org 安装的 3.10.9。原因是非交互式的登录 shell 会跳过 ~/.zshrc,而大多数 PATH 设置都写在那里。所以保持直接调用就好。如果你机器上的结果看起来不对,可以在 Claude Code 的输入框里输入 !which python3 对比一下。
测试时还发现一个坑。对 $ 的调用,比如 $.process.run 和 $.command.register,同样需要替身,而且 $ 调用的替身要返回 { value: ... }。直接返回裸结果,测试工具包会拒绝。
如果你的分析大多在 notebook 而不是脚本里完成,这个办法作用有限,因为 Claude Code 是把 .ipynb 文件当作 JSON 来编辑的,看不到正在运行的内核。RunCell (opens in a new tab) 是一个 notebook agent,能直接运行 cell 并读取输出,更适合这类工作。
让 Claude 帮你写 mod
上面这些代码都不必手写。直接在会话里提要求就行,比如「做一个 mod,在输入框上方显示我的 5 小时用量」。Claude 会加载内置的 plugin-authoring skill,把文件写到 ~/.claude/dev-mods/<session-id>/。写第一个文件时,它会问一次「Enable hot reloading for this session?」。同意之后,每当 Claude 的一轮结束,改动就会自动重载。上面三个 mod 就是这样写出来并加载的。
文档和社区补充的几点:
- 把文件夹复制到一个长期保存的位置。Qiita 和 Zenn 上的作者反馈,dev-mods 文件夹大约 30 天后会随旧会话一起被清理。
- 开发时用
claude --plugin-dir ./my-mod启动。已安装的 mod 运行的是缓存副本,直接改它的文件夹不会生效,要等你更新插件。 - Desktop 应用不接受启动参数。改为在
~/.claude/settings.json的env块里设置CLAUDE_CODE_PLUGIN_DIRS。这一项写在项目设置里会被忽略。 - 运行
/plugin。看到一行灰色的1 mod active · safe-shell这类提示,就说明 mod 已经加载。 - 开发期间用
claude --debug启动。调试日志会写明每个被跳过的 hook,以及每次被引擎拒绝的绘制。
值得研究的 15 个社区 mod
awesome-claude-code-mods (opens in a new tab) 合集目前有 316 个 star。它在发布五天后的 10 月 6 日,扫描了 1,265 个仓库里的 2,690 个公开 mod。从扫描数据能看出大家在做什么:
- 68% 添加了斜杠命令
- 53% hook 了工具调用
- 41% 在输入框上方绘制,40% 会打开面板
- 40% 会启动进程,12% 会调用模型,11% 会访问网络
下面这 15 个能看出 mod 的覆盖范围。star 数截至 2026 年 10 月 7 日。
| Mod | 功能 | Star 数 | 值得看什么 |
|---|---|---|---|
| terminal-browser (opens in a new tab) | 在对话旁边放一个网页浏览器,可以看网站、本地 HTML 预览和 pull request | 3,695 | 目前最有野心的面板 |
| claude-image-view (opens in a new tab) | 在输入框上方显示粘贴图片的缩略图,代替 [Image #1] | 160 | 在终端里绘制图片 |
| hamzafer/claude-code-mods (opens in a new tab) | 一整套:上下文条、blast-radius 守卫、Markdown 预览、子 agent 仪表盘 | 138 | 一个仓库里有多种类型的 mod |
| claude-auto-handoff (opens in a new tab) | 把很长的会话交接给一个新会话,并附上结构化的交接说明 | 60 | 上下文管理 |
| prismantis (opens in a new tab) | 给回复套上主题:表格、代码、用字符画(box art)绘制的图表、从右到左的文字 | 49 | 改变 Claude 自己消息的样式 |
| cc-arcade (opens in a new tab) | 输入框上方的九个小游戏,Claude 完成时自动暂停 | 41 | 用帧时钟做动画 |
| jev-permission-gate (opens in a new tab) | 在 auto 模式下由 Jev 决策模型判断工具调用 | 28 | 隐私说明写得很细:它会把你最近三条消息(每条最多 1,500 个字符)发送到 TypeSafe 的 API |
| prompt-rail (opens in a new tab) | 把你过去的 prompt 排成一条导轨,悬停可查看,点击可跳转 | 22 | Zenn 上点赞最多的日文 mods 测评,主角就是它 |
| claude-paste-view (opens in a new tab) | 预览粘贴的图片和长文本 | 18 | 代码小巧易读 |
| claude-gfm-render (opens in a new tab) | 在回复里渲染 GitHub alert、任务列表和 Mermaid,输出字符画或 SVG | 14 | 按界面采用不同的绘制方式 |
| claude_qamods (opens in a new tab) | 其中的 qa-guide 在侧边面板里解释 Claude 提出的问题和选项 | 10 | 改进内置对话框 |
| CC-Usage-Band (opens in a new tab) | 在输入框上方显示 5 小时和 7 天用量上限、上下文和缓存命中率 | 10 | 最常见的 mod 类型:计量条 |
| harness-scope (opens in a new tab) | 按仓库设置 profile,隐藏全局的 skill、agent 和规则 | 1 | 作者实测 skill 列表从 98 项减到 49 项 |
| touch-map (opens in a new tab) | 显示 Claude 列出、读取、编辑或创建了哪些文件 | 0 | 作者发现,在一次重构调研中,Claude 动过 304 个文件里的 125 个 |
| claude-mods-router (opens in a new tab) | 用 $.model.classify 为每条 prompt 分配 effort 级别 | 0 | 作者的 Qiita 文章(26 个赞)指出,切换模型会破坏提示缓存 |
Anthropic 也在 claude-code-playground (opens in a new tab) 里发布了三个示例:token-weather、blast-radius 和 replay-theater。内置 mod 的源码(包括 /diff)在 claude-code 仓库 (opens in a new tab) 里。
社区总结出的最佳实践
发布后的第一周,仅日本作者就发表了约 50 篇文章,内容包括能跑的 mod、实测数据和踩坑记录。下面是反复出现的几条习惯。
- 安装前先读代码。克隆下来,运行
claude plugin validate,看hooks:和calls:这两行。凡是出现$.process、$.http、$.fs.write、$.env.get或$.model的地方,都去读源码。每次更新后重新 validate。通过 validate 只说明引擎能加载这段代码,不代表代码安全。 - 守卫在调用
next之前做判断,并且 fail closed。只做观察的 mod 则应该用.catch(($, e, next) => next(e))实现 fail open(出错时放行),这样计量条出了 bug 也不会挡住你的工作。 - 需要等待,就在
$调用里等。每个 hook 自己有 10 秒的时间。正在执行的$.process.run或$.ui.ask不计入这 10 秒,但你自己的 promise 要计入。 $不能存起来,也不能解构。只把它传给在文件顶层声明的函数。好几位作者的 mod 都因此静默加载失败,改掉之后才恢复正常。- 注意提示缓存。在会话中途切换模型,或者在两次请求之间改动 system prompt,都会让缓存作废。
- 按
e.surface分支处理。Svg只在 Desktop 应用里绘制,Raster和Image只在终端里绘制。 - 团队使用时锁定版本。这套 API 还处于 early access 阶段,不同版本之间会有变化。发布后的六个版本(2.1.288 到 2.1.293)里,有五个改动或修复了 mod 的行为。
遮蔽密钥是一个值得警惕的例子。一位 note.com 作者写了一个 mod,在 Claude 看到工具输出之前,先把其中的 API key 替换掉。可 Claude 在用 od -c 调试一次失败的编辑时,照样把原始值打印了出来;一次整文件 Write 还把占位符写回文件,覆盖了真实的 key。最后真正守住的,只有针对这个文件的权限 deny 规则。只改变屏幕显示内容的 mod,对模型什么也藏不住。
常见问题排查
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
/plugin 里没有列出这个 mod | 文件夹未被信任,用了 --safe-mode 或 --bare 参数,或者设置了 disableAllHooks | 信任该文件夹,去掉相应参数后重启 |
出现 hooks modules are turned off in this process | Anthropic 远程关闭了已安装的 mod | 本地不需要改什么。建议更新,2.1.289 和 2.1.290 修复了两种 mod 一直保持关闭的情况 |
validate 报错「expected record, received undefined」 | PATH 上排在前面的是旧版 CLI | 运行 claude --version 确认,然后更新 |
| 某个 hook 被跳过,却看不到报错 | 它抛出了异常,比如调用了 $.plugin.name(),而它其实是一个属性 | 用 claude --debug 启动,查看跳过记录 |
| 面板一直不出现 | 不是你主动打开的面板,要等宽度达到 144 列才会出现;只有在全屏布局下才会停靠在对话记录旁边;/diff 也可能把它盖住 | 拉宽窗口,试试全屏布局,关掉 /diff |
| 终端里正常,Desktop 里不行 | Desktop 自带独立的版本,而且有些元素只能在终端显示 | 在 Code 标签页查看 /status,并按 e.surface 分支处理 |
VS Code 或 claude -p 里没有 mod UI | 这些界面不绘制 | 改用对话记录里的一行文字,或命令返回的文本 |
| 测试报错「nothing beneath the plugins answers」 | 测试工具包里没有引擎 | 添加替身 hook,并用 { value } 响应 $ 调用 |
分享你的 mod
在 .claude-plugin/ 里,plugin.json 旁边放一个 marketplace.json:
{
"name": "my-mods",
"owner": { "name": "you" },
"plugins": [{ "name": "safe-shell", "source": "./" }]
}把文件夹推送到 GitHub,之后任何人都能在终端会话里安装:
/plugin install safe-shell --marketplace you/safe-shell在 Desktop 应用里,本地会话和 SSH 会话的安装路径是依次点击 +、Plugins、Add plugin。如果你给仓库加上 claude-code-mod 这个 topic,社区合集通常会在你下一次推送后的几个小时内收录它。
FAQ
Related Guides
- Claude Code Desktop 绕过权限:如何开启 Bypass permissions
- Claude Code AGENTS.md 配置指南:四种加载模式、与 CLAUDE.md 的区别、多工具共用
- Claude Code Routines 是什么?AI Agent 定时任务与自动触发指南
- 使用 Claude Agent SDK(TypeScript)构建一个类似 Claude Code 的 AI Agent
- Pi Coding Agent 1.0 使用教程:安装、接入模型(含 Ollama 本地模型)并跑通第一个任务