Skip to content

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

更新于

Claude Code mod(v2.1.287+)是由 TypeScript 函数组成的插件,能绘制面板、添加即时执行的斜杠命令、拦截工具调用。本文讲清楚 mod 怎么用:从零编写并实测 3 个可用的 mod,整理 15 个社区案例,并列出安装别人的 mod 之前要做的安全检查。

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 日的扫描数据。

Mod 是什么,什么时候该用别的扩展方式

在 mod 出现之前,扩展 Claude Code 都是从外部入手:settings hook、skill、状态栏脚本,或者 MCP 服务器。区别在于代码在哪里运行。settings hook 是 Claude Code 从外部启动的脚本。mod 的函数则运行在 Claude Code 自己的进程里,所以 mod 能在界面上绘制内容,其他几种都做不到。

官方概览页的对比大致如下:

ModSettings hookSkillMCP 服务器
是什么插件里的 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 code

register.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-shell

Mod 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 request3,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 排成一条导轨,悬停可查看,点击可跳转22Zenn 上点赞最多的日文 mods 测评,主角就是它
claude-paste-view (opens in a new tab)预览粘贴的图片和长文本18代码小巧易读
claude-gfm-render (opens in a new tab)在回复里渲染 GitHub alert、任务列表和 Mermaid,输出字符画或 SVG14按界面采用不同的绘制方式
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、实测数据和踩坑记录。下面是反复出现的几条习惯。

  1. 安装前先读代码。克隆下来,运行 claude plugin validate,看 hooks: 和 calls: 这两行。凡是出现 $.process、$.http、$.fs.write、$.env.get 或 $.model 的地方,都去读源码。每次更新后重新 validate。通过 validate 只说明引擎能加载这段代码,不代表代码安全。
  2. 守卫在调用 next 之前做判断,并且 fail closed。只做观察的 mod 则应该用 .catch(($, e, next) => next(e)) 实现 fail open(出错时放行),这样计量条出了 bug 也不会挡住你的工作。
  3. 需要等待,就在 $ 调用里等。每个 hook 自己有 10 秒的时间。正在执行的 $.process.run 或 $.ui.ask 不计入这 10 秒,但你自己的 promise 要计入。
  4. $ 不能存起来,也不能解构。只把它传给在文件顶层声明的函数。好几位作者的 mod 都因此静默加载失败,改掉之后才恢复正常。
  5. 注意提示缓存。在会话中途切换模型,或者在两次请求之间改动 system prompt,都会让缓存作废。
  6. 按 e.surface 分支处理。Svg 只在 Desktop 应用里绘制,Raster 和 Image 只在终端里绘制。
  7. 团队使用时锁定版本。这套 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 processAnthropic 远程关闭了已安装的 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