Claude Code Mods 完整指南

Mods 让插件里的函数直接跑在 Claude Code 进程内:能看、能改、能代答工具调用、提示词、命令、回合和界面。本页按「先判断要不要用 → 装之前查什么 → 怎么写 → 多个 Mod 与权限怎么裁决 → 坏了怎么查」的顺序整理,所有事实逐条对照官方页面,二手说法单独标注。

最低版本
Claude Code v2.1.287,默认开启
发布日期
2026-10-01
资料核验日
2026-10-02(逐页打开官方文档)
实测情况
未做运行实测:写作用的机器是 2.1.286,低于门槛

一屏结论与版本基线

读完这一屏就能决定要不要继续往下看。

它是什么

Mod 是一个带 hooks module 的插件:hooks/hooks.json 里的 modules 指向一个 JS/TS 文件,Claude Code 在事件发生时调用其中的函数。每个 hook 拿到 ($, e, next),可以观察(Observe)、改写(Rewrite)或代答(Answer)。 官方文档

和旧机制的差别

settings hook、Skill、状态栏、MCP server 都在 Claude Code 外面工作;Mod 在里面,所以能画界面、改内置界面、按住一次工具调用去问用户、把一次请求换到别的模型、在没有 Claude 回合时响应 /命令。 官方文档

没有安全沙箱

Mod 以安装者的用户权限读写文件、启动进程、发网络请求,能在权限弹窗出现之前批准或拒绝工具调用。Mod 启动的进程不受 Claude Code 沙箱约束。 官方文档

个人环境风险更高

在没有托管设置、也没用 Team/Enterprise 登录的机器上,内置守卫不加载,此时 Mod 的 tool.check 能批准 deny 规则拒绝的调用。deny 规则压过 Mod 只在守卫加载的环境里成立。 官方文档

出错默认放行

hook 抛错、超过 10 秒或返回形状不对时会被跳过,事件继续往下走。拦截类 hook 不加 .catch,一出错命令就照常执行。 官方文档

流传说法要打折

中文社区流传的解读里,有几处与官方文档不符或没有官方出处(源码「全部公开」、claude -p 不加载、提示词 token 减少 26.6% 等)。逐条对照见更正记录。

  • 官方文档 来自 Anthropic 的文档、博客、发布说明或源码仓库,2026-10-02 打开核对过。
  • 二手转述 出自第三方文章或账号,没有打开一手出处。
  • 编辑推导 基于官方事实做的归纳或建议,不是官方说法。
  • 待核实 这次没找到明确答案,不要当事实用。

事件和方法会随版本变化。官方的说法是:文档与类型声明文件不一致时,以你本机生成的 .claude-plugin/types/claude-code/index.d.ts 为准。本页所有 API 名称以 v2.1.287 文档为基线。

选型与运行环境

先确认 Mod 是不是对的工具,再确认它在你用的那个界面里能不能跑、能不能画。

四种扩展方式对比

表格较宽,可在表格区域内左右滑动。

官方对比表(译自 overview 页) 官方文档
维度ModSettings hookSkillMCP server
是什么插件里的函数,Claude Code 在自己进程内调用生命周期事件上运行的 shell 命令、HTTP 请求或 prompt一份 Claude 会读的 SKILL.md 指令给 Claude 提供工具的外部进程或服务
能改什么工具调用、提示词、命令、回合,以及界面画什么工具调用或提示词是否放行、工具调用的参数与结果、给 Claude 追加的上下文Claude 知道什么、怎么做Claude 有哪些工具
能画界面吗能不能不能不能
你要写什么JavaScript 或 TypeScript一个脚本加一条 settings.json 配置Markdown任意语言的 server
什么时候选它想要一个面板、输入框上方的信息带、自定义命令,或要改写某个事件已有脚本,只想拦截、放行或记录某个事件总在反复粘贴同一段指令Claude 需要访问外部系统

注意第二行:settings hook 也能改工具调用的参数和结果、追加上下文,「旧 hook 只能拦截」的说法过头了。官方还补了一句:一个插件可以同时装下这四样。

只有 Mod 能做的事

画界面

开面板、在输入框上方画信息带、加按钮和输入框。

重画内置界面

工具行、spinner、Claude 向你提问的对话框。权限弹窗除外,它不能被改样式。

介入一次调用或请求

按住工具调用去问用户、不跑工具直接给结果、把某一次请求发给另一个模型。

无回合响应命令

自定义 /命令 直接跑代码,不启动 Claude 回合,Claude 正在干活时也能用。

hook 之间共享数据

同一个文件里的变量,所有 hook 都能读写。

选型顺序 编辑推导

  1. 只是让 Claude 记住一套做法 → 写 Skill。
  2. 要接外部系统(数据库、工单、API)→ MCP server。
  3. 固定的命令或路径要放行或禁止 → 权限规则(如 Bash(npm test)),不用写代码。
  4. 已有脚本,只做拦截、放行、记录 → settings hook。
  5. 要画界面、要自定义命令、要按当时的状态动态改写事件 → Mod。

在哪里能跑、在哪里能画

「hook 是否执行」和「界面是否显示」是两件事,分开看。表格可左右滑动。

运行环境(overview 页) 官方文档
环境hook 是否执行界面是否显示
终端 CLI是是
桌面 App 的 Code 标签(非 WSL)是是,仅终端可用的元素除外
桌面 App 的 WSL 会话否否
VS Code 扩展聊天面板是否
claude -p 与 Agent SDK是否
Remote Control在本机执行画在本机终端
云端会话插件可达时执行否

会画界面的 Mod 可以检查自己跑在哪个应用里,在画不出来的地方退回成 transcript 里的一行字或命令的文本回复。

安装前检查与停用方式

装一个第三方 Mod,相当于让一段别人的代码以你的身份常驻在 Claude Code 里。先知道它能碰什么,再知道怎么让它停下。

不运行代码,先看它会做什么

Mod 只能通过 mods API(代码里写作 $)触达文件、进程和网络,而且必须把每个调用写全,所以 Claude Code 能在安装前静态列出来:

命令与输出节选 · 官方 admin 页示例
claude plugin validate ./some-mod

  ❯ ./register.js hooks: session.start, tool.call, ui.render{component=Pane}
  ❯ ./register.js calls: $.fs.read, $.http.fetch, $.store.set, $.ui.open

hooks: 是它接收的事件,calls: 是它调用的方法。读写环境变量会多出 env reads: / env writes: 行。写法让这条命令读不出来的 Mod,Claude Code 会拒绝加载。

calls 行里要留意的调用 官方文档

$.fs.read / $.fs.write

读写该用户能碰到的任何文件。即使 Read(.env) 被 deny,Mod 仍能用 $.fs.read 读它。

$.process.run / $.process.spawn

以该用户身份启动程序,沙箱和网络策略都不覆盖这些程序。

$.http.fetch

发网络请求。组织关闭 web fetch 时会被拒。

$.env.get / $.settings.read

读环境变量与设置,里面可能有 API key。

$.env.set

给 Claude Code 及其之后启动的每个命令、MCP server 设环境变量,能改变那些程序的行为。

$.mcp.call

调用已连接 MCP server 的工具,受会话权限规则约束。

$.model.complete

用你的套餐或 API key 调模型,会产生用量。

$.prompt.submit

提交提示词,而且可以当成你本人说的话发出。

$.session.send

发消息给另一个会话或子 Agent 的 Claude。

hooks 行里要留意的事件 官方文档

  • tool.call、prompt.submit:看得到每一次工具调用和每一条提示词,并且能改。
  • tool.check:能在权限弹窗出现前批准或拒绝调用。
  • session.append:能在对话每一行落盘前改写它。
  • ui.render{component=AskUserQuestion}:能重画 Claude 向你提问的对话框。

安装前七项检查 编辑推导

下面每一条都由上文的官方事实推出,顺序是我排的。

  1. 版本:claude --version 不低于 2.1.287,否则装了也不加载。
  2. 拿到源码目录再跑 validate:看 hooks: 与 calls: 两行,和它自称的功能对不对得上。一个「只显示 token 用量」的 Mod 不该出现 $.process.run。
  3. 有没有 tool.check:有的话,它能替你批准调用。个人环境没有内置守卫,连 deny 规则都可能被它推翻。
  4. 有没有不调 next 就返回结果的 tool.call hook:这类 hook 会让非托管的 PreToolUse hook 不再运行,Claude 只看得到它给的结果。
  5. 拦截类 hook 是否带 .catch:没有的话,它出错时就等于不存在。
  6. 作者是否写明测试过的 Claude Code 版本:官方要求作者在 README 里写;事件和方法会随版本变。
  7. 先想好怎么关:确认下面的停用方式在你的环境里可用,再装。

停用方式各自管到哪

关一个 Mod

/plugin → Installed 页里关闭。

本次会话全部不加载

claude --safe-mode。所有已安装的 Mod(含组织的)和其他自定义项都不加载;内置 Mod 照跑。适合判断问题是不是某个 Mod 引起的。

所有会话全部停用

在 ~/.claude/settings.json 写 "disableAllHooks": true。副作用:settings hook 和自定义状态栏一起停。组织托管的 Mod 照跑,内置 Mod 照跑,插件里的 skill、命令、agent、MCP server 照常加载。

内置 Mod

每个有自己的开关,在 /plugin → Installed → Built-in 里看。cc-plugin-sec-default 不能关。

已失效的开关

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS 从 v2.1.287 起被忽略,设成 0 也关不掉 Mod。

你控制不了的一种停用

Anthropic 可以远程关闭已安装的 Mod,此时本机没有任何设置能重新打开。

安装、加载与首次验收

得到一个 Mod 有三条路:用内置的、装别人的、自己做。这一节讲前两条。

从 marketplace 安装

局部片段 · 在 Claude Code 提示符里
/plugin marketplace add your-org/my-mods
/plugin install token-weather@my-mods
/reload-plugins
局部片段 · 在 shell 里
claude plugin install token-weather@my-mods --scope user

在 shell 里安装后,已经开着的会话要执行 /reload-plugins,否则下次启动才加载。 官方文档

Anthropic 另有一个社区插件市场的只读镜像仓库 anthropics/claude-plugins-community,安装写法是 claude plugin install <插件名>@claude-community。README 称其中的插件经 claude.ai 提交、通过自动安全扫描并获批分发。 源码仓库 自动扫描不等于有人逐行审过代码,安装前七项检查照做。 编辑推导

不安装,只在一次会话里加载

局部片段
claude --plugin-dir ./first-mod

这样加载的目录会被监视,保存文件即热重载。开发时一直用这种方式,不要改已安装的副本(原因见分发一节)。

首次验收

  1. 第一次打开的目录,先在交互会话里回答 trust 提示,否则任何 Mod 都不加载。
  2. 运行 /plugin:顶部会有一行暗色文字,如 1 mod active · first-mod。这一行不含内置 Mod。
  3. 用一下它声称的功能(命令、面板、信息带)。
  4. 没反应时,带 --debug 重开会话,在调试日志里找这样的加载行:
调试日志示例 · 官方 troubleshoot 页
hooks module first-mod@inline loaded (worker, environment 2, tier user); events: session.start,tool.call,command.run,ui.render

先确认环境允许 Mod 加载

在一个不含 Mod 的目录里运行 claude plugin test,不需要会话。看它打印的消息:

no hooks module to load

Mod 可以加载,只是当前目录没有可测的 Mod。

hooks modules are turned off here

有设置挡住了:你自己的 disableAllHooks,或组织策略。

hooks modules are turned off in this process

Anthropic 远程关闭了已安装的 Mod。

组织设了 allowManagedModsOnly 的情况,这条命令不会报告。

内置条目一览 官方文档

官方表里有六个内置条目,其中 plugin-authoring 只有 skill、没有 Mod 代码,所以不宜说成「六个内置 Mod」。

cc-plugin-agents-md

读取 AGENTS.md。除了读不到该文件的会话,每个会话都开。源码公开,带一个 userConfig 选项。

cc-plugin-diff

/diff 现在由它实现,交互式终端会话里开启;关掉后由内置的 /diff 接手。源码公开。

cc-plugin-plugin-authoring

只含一个 skill,教 Claude 怎么写 Mod。除非 Anthropic 远程关闭已安装 Mod,否则开启。

cc-plugin-sec-default

内置守卫,不能关。只在有托管设置的机器或 Team/Enterprise 登录时加载。源码公开。

cc-plugin-telemetry

开了分析的地方就开;可用 DISABLE_TELEMETRY 关。它给其他 Mod 提供可调用的方法和类型。源码公开。

cc-plugin-you-should-know

默认关闭;组织可用时出现在 /plugin → Installed → Show disabled。启用:/plugin enable cc-plugin-you-should-know@builtin。

写第一个 Mod

不需要 Node.js、打包器或构建步骤,Claude Code 直接加载 .js 和 .ts。下面是官方教程的 first-mod:数工具调用次数,显示在 spinner 旁,并加一个 /tally 命令。

三个文件

目录结构
first-mod/
├── .claude-plugin/
│   └── plugin.json
└── hooks/
    ├── hooks.json
    └── register.js
完整示例 · first-mod/.claude-plugin/plugin.json
{
  "name": "first-mod",
  "version": "0.1.0",
  "description": "Counts Claude's tool calls, shows the count beside the spinner, and adds a /tally command",
  "author": { "name": "Your Name" }
}
完整示例 · first-mod/hooks/hooks.json(modules 只列一个路径,有它才算 Mod)
{
  "description": "The first-mod hooks module",
  "modules": ["./register.js"]
}
完整示例 · first-mod/hooks/register.js(官方教程代码,注释为中文转写) · v2.1.287 文档
// 计数器,下面几个 hook 共用
let calls = 0

// Mod 加载时 Claude Code 调用一次
export function register(on) {
  // 会话开始时(以及每次重载时)运行
  on('session.start', async ($, e, next) => {
    await $.command.register({
      name: 'tally',
      description: 'Show how many tool calls Claude has made',
    })
    return next(e)            // 观察:原样放行
  })

  // Claude 每次要用工具之前运行
  on('tool.call', async ($, e, next) => {
    calls += 1
    $.ui.invalidate('ui.render')   // 请求重画界面
    return next(e)            // 观察:原样放行
  })

  // 只有输入 /tally 时运行(第二个参数是 matcher)
  on('command.run', { command: 'tally' }, async () => {
    // 代答:不调 next,自己给结果
    return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
  })

  // 每次画 spinner 时运行
  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
    // 改写:把带新 suffix 的事件副本交给 next
    return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
  })
}

跑起来

局部片段
claude --plugin-dir ./first-mod          # 交互会话:spinner 显示 Thinking · tool calls: 2…
claude -p "/tally" --plugin-dir ./first-mod   # 非交互:输出 first-mod: Claude has made 0 tool calls since this mod loaded

第二条命令本身就说明:用 --plugin-dir 或安装方式加载的 Mod,在 claude -p 里 hook 照常执行。保存 register.js 后,transcript 会出现一行说明 first-mod 已重载并列出它的 hook;每次重载都会重新执行 register,所以 calls 归零。

这个计数记的是「经过这个 hook 的 tool.call 次数」,不是成功执行的工具数:hook 在调用被权限检查之前就加了一,被拒绝的调用也算在内。 编辑推导

三种处理方式

观察 Observe

做完自己的事,return next(e)。要在事件之后做事,就 const r = await next(e),处理完再 return r。

改写 Rewrite

事件对象是深度冻结的,赋值会抛错。把改过的副本传给 next;也可以改 next 返回的结果再交回去。

代答 Answer

不调 next 直接返回结果,链条就此短路:后面的 Mod 和 Claude Code 自己的行为都不再运行。

官方介绍里也用过「watch the event, change it, or take it over」的说法,所以把 Answer 译成「接管」不算错;文档里的正式名称是 Observe / Rewrite / Answer。

matcher:只处理一部分事件

局部片段 · 官方 events 页
on('tool.call', { tool: 'Bash' }, hook)             // 字符串:只匹配一个值
on('tool.call', { tool: ['Edit', 'Write'] }, hook)  // 数组:匹配其中任意一个
on('tool.call', { tool: /^mcp__github__/ }, hook)   // 正则:按模式匹配

事件名可用通配:'classic.*' 匹配所有 settings hook 事件;'*' 匹配除 telemetry 之外的所有事件。同一个事件、同一个 matcher 只能注册一次,无 matcher 地给 session.start 注册两次会导致模块加载失败。

写完先过静态检查

命令与输出节选 · 官方 create 页
claude plugin validate ./first-mod

  ❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
  ❯ ./register.js calls: $.command.register, $.ui.invalidate

✔ Validation passed

想挂的事件没出现在 hooks: 行,Claude Code 就不会调用它,常见原因是事件名拼错,报错形如 "tool.calls" is not an event。为了让静态分析读得出来,要遵守这些写法规则: 官方文档

  • 每个 mods API 调用写全:$.store.get('notes')。可以把 $ 传给同文件顶层声明的函数;不能把 $ 或它的命名空间赋给变量、解构,或用计算出来的名字取成员。const ui = $.ui 会报 $.ui is used as a value。
  • on 的事件名必须是字符串字面量,不能用变量或循环。
  • register 里不能再声明名为 on 的变量或参数。
  • 只能用相对路径导入插件目录内的文件;唯一允许的裸导入是 claude-code。
  • 不能用动态 import();每个文件都写成 ES module,不用 require。

类型声明:你这个版本的权威参考

从 --plugin-dir 目录加载或重载 Mod 时(Claude 代写的也一样),Claude Code 会在 Mod 目录的 .claude-plugin/types/ 下写入 .d.ts 文件:claude-code/index.d.ts(全部事件、方法和可画元素,每个方法带注释和示例)、claude-code-tools/index.d.ts(内置工具的输入输出)、claude-code-mcp/index.d.ts(已连接 MCP 工具的输入),以及一个 tsconfig.json。在线版本是仓库里的 mods/types/claude-code.d.ts,首行写着生成它的版本。

让 Claude 代写

Claude 靠内置 skill plugin-authoring 来写 Mod,可以让它自动加载,也可以自己输入 /plugin-authoring。流程如下: 官方文档

  1. 描述需求,例如「做一个在输入框上方显示当前 git 分支的 mod」。Claude 把文件写进 ~/.claude/dev-mods/<会话ID>/<mod名>/。在 default 和 acceptEdits 模式下,~/.claude 是受保护路径,每个文件都要你批准。
  2. 选择是否热重载。第一个文件保存时会问:选 Enable for this session,Mod 在本回合结束时加载,之后每个改动它的回合结束时重载;选 Not now 则暂不加载,文件保留,该会话下次启动时才加载。
  3. 确认已加载:/plugin 的 Installed 页里能看到它,也能在那里关掉。
  4. 试用并迭代:不满意就告诉 Claude 改什么。
  5. 想留下就拷走:这样写出来的 Mod 只在创建它的会话里加载,会话的 mods 文件夹超过 cleanupPeriodDays 后会被删除。拷到自己的目录,再用 --plugin-dir 加载或放进 marketplace。

哪些会话里 Claude 代写的 Mod 不加载

没人能批准的会话(claude -p、dontAsk 模式)、目录未被信任、Mod 被停用(--safe-mode、--bare、disableAllHooks 或组织策略)。这份清单只针对 Claude 当场写的 Mod,不是对所有 Mod 的限制。

按任务扩展能力

想做某件事时,该挂哪个事件、调哪个方法。代码均取自官方 events 页,注释为中文转写。

拒绝某类工具调用

局部片段(放在 register 内)
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (/git push .*--force/.test(e.command)) {
    // 不调 next:命令不执行,也不出现权限弹窗
    return { deny: 'Force pushes are not allowed in this repository. Push to a new branch instead.' }
  }
  return next(e)   // 其他命令继续走权限检查,然后执行
})

tool.call 覆盖子 Agent 的调用和 MCP 工具。deny 的文字会作为工具结果交给 Claude,所以要写成 Claude 能照做的指令。想自己给结果而不跑工具,返回 { result: '...' };想重试,就再调一次 next(e)。

执行前先问用户

局部片段(据官方示例缩写,匹配规则只留了强制推送一种)
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
  if (!/\bgit\s+push\b.*--force/.test(e.command)) return next(e)
  let answer = 'Refuse'            // 默认答案是拒绝
  try {
    // 工具调用停在这里,直到用户选一项
    answer = await $.ui.ask('Run this command? ' + e.command, ['Run it', 'Refuse'])
  } catch {
    // 用户关掉了提问,或这是没人可问的 claude -p
  }
  if (answer !== 'Run it') {
    return { deny: 'The user declined this command. Ask before trying a different approach.' }
  }
  return next(e)                   // 用户同意后,常规权限检查照样进行
})

$.ui.ask 用的是 Claude 向你提问的那个对话框,选项之后还会多出「输入其他答案」和 Chat about this 两行。等待时间放在 mods API 调用里不计入 10 秒限制;自己 await 的 promise 会计入,而超时的 hook 会被跳过,被按住的命令就会执行。

按当时的状态决定权限

局部片段
on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
  // 权限规则和 settings hook 已经得出的决定:'allow'、'ask' 或 'deny'
  const decided = await next(e)
  if (!e.input.command.includes('git push')) return decided
  const branch = await $.process.run(['git', 'branch', '--show-current'])
  if (branch.stdout.trim() !== 'main') return decided
  // reason 是写给 Claude 的指令:改从 main 以外的分支推送
  return { decision: 'deny', reason: 'Push from a branch other than main' }
})

这段代码只在当前分支是 main 时拒绝;reason 保留官方原文,它是一句祈使句,意思是「请从 main 以外的分支推送」。官方自己的提醒:这个 hook 只是匹配命令文本,应把它当成给 Claude 的提醒;要对所有人禁止推 main,去 Git 托管平台上保护分支。

改写或补充提示词

改写提示词

next({ ...e, text: newText }),transcript 里显示新文本。

追加只给 Claude 看的文字

next({ ...e, context: [...(e.context ?? []), extraText] })

阻止发送

{ drop: 'the reason' }

其余 Claude 会读到的内容由别的事件管:prompt.section(系统提示词的每一段)、prompt.context(随第一条消息发送的上下文)、skill.prompt(skill 的正文)。这些 hook 产出的文字如果每次请求都变,会让 prompt cache 失效。

跟踪回合、读用量、换模型

局部片段 · turn.step 是流式事件,hook 要写成异步生成器
on('turn.step', async function* ($, e, next) {
  const result = yield* next(e)    // 发出请求,边流边转发,拿到最终结果
  if (result.usage) {
    $.ui.log('cache read ' + result.usage.cache_read_input_tokens + ' · wrote ' + result.usage.cache_creation_input_tokens)
  }
  return result
})

一个带工具调用的回合有多次请求,每次都触发 turn.step,子 Agent 的请求也算(看 e.agentId)。把某次请求发给别的模型:next({ ...e, model });也可以不调模型直接代答。turn.complete 里返回 { text: '...' } 会在回答下方显示一行。

其他常见任务

加一个命令

在 session.start 里 $.command.register(...),再用带 matcher 的 command.run 代答。

画面板或信息带

挂 ui.render 并指定渲染位(如 Pane、AbovePrompt),用 $.ui.open 打开面板,数据变了调 $.ui.invalidate。

改内置界面

对 Spinner、ToolUse、AskUserQuestion 等渲染位挂 ui.render,把改过 props 的事件交给 next。

自己调一次模型

$.model.complete({ model, system, prompt }),maxTokens 默认 1024。

定时任务

$.clock.every、$.clock.after。hooks module 里没有 setTimeout。

不打扰回合地显示信息

$.ui.log(transcript 里的暗色行,Claude 不读)、$.ui.toast(默认 4 秒)、$.ui.status。

接 settings hook 的事件

每个 settings hook 事件都有对应的 classic.<事件名>,如 classic.Stop;e 就是 settings hook 从 stdin 读到的 JSON。

官方教程的三个示例 Mod 官方博客

Token Weather

把上下文占用画成「天气」信息带,阈值为 25%、50%、75%、90%。

Blast Radius

在递归强制删除、git 硬重置、git clean、强制推送、数据库迁移这类命令执行前先做 dry run,再让用户选 Proceed 或 Cancel。教程原话是「安全网,不是权限系统」:命令替换、别名、脚本都能绕过。

Replay Theater

记录 Edit 和 Write 并支持回放,只观察,不改任何东西。

多 Mod 组合与权限裁决

同一个事件上的所有 hook 串成一条中间件链。谁在外层、谁的决定是终局,决定了你的守卫到底有没有用。

洋葱链:四层顺序

事件由外向内传,结果由内向外回。最外层的 Mod 最先看到事件、最后看到结果,并决定里面的要不要运行;靠后的 Mod 无法阻止靠前的 Mod 看到事件。 官方文档

一次工具调用的裁决时序

  1. Claude 发出工具调用。
  2. 托管设置里的 PreToolUse hook 先跑。 终止分支:这里的 block 是终局,任何 Mod 都看不到这次调用。Mod 之后改写了调用的话,托管 hook 会对改写后的调用再跑一遍。
  3. 各 Mod 的 tool.call hook 按四层顺序运行。 终止分支:任何一个 hook 不调 next 就返回 { deny } 或 { result },后面的 Mod、非托管 PreToolUse hook、权限检查和工具本身都不再运行,也不出现权限弹窗。
  4. 最后一个 Mod 调了 next,进入 Claude Code 自身行为:其他 settings 文件和插件 hooks/hooks.json 里的 PreToolUse hook 运行,权限规则给出 allow、ask 或 deny。
  5. tool.check 链运行,next(e) 拿到上一步的决定,hook 可以原样返回,也可以换成另一个决定。
  6. 按最终决定处理:allow 直接执行;ask 弹出权限框(Mod 改不了它的样子);deny 把理由交给 Claude。auto 模式下,Mod 批准的调用不再经过分类器。
  7. 工具执行,结果沿 tool.call 链由内向外返回,每个 await next(e) 的 hook 都能看到并改写结果。

Mod 的 tool.check 能推翻什么 官方文档

ask 规则

能。Mod 可以直接批准本该弹窗询问的调用。

非托管 PreToolUse hook 的 block

能。包括你自己在 settings.json 里写的 hook。

托管 PreToolUse hook 的 block

不能,它是终局。

auto 模式分类器

Mod 批准的调用不经分类器检查就执行。

deny 规则

有托管设置的机器,或用 Team/Enterprise 登录时:deny 规则默认压过 Mod,组织可用 allowModsToOverrideDenyRules 放开。其他任何环境:Mod 可以批准 deny 规则拒绝的调用。

Mod 自己的 $.fs / $.process

deny 规则和托管 hook 只管 Claude 的工具调用,管不到 Mod 自己发起的文件和进程操作。

auto 模式下改写输入会被拒

auto 模式里,如果 hook 在分类器审过之后又改了工具调用的输入,调用会被拒,理由是 a hook changed this call's input after the model wrote it。改输入的可能是 Mod 的 tool.call、turn.step hook,也可能是 PreToolUse settings hook,消息不会说是哪一个。 官方文档

写守卫类 Mod 的七条注意

  1. 固定的命令或路径用权限规则,不写代码。tool.check 留给「取决于当时状态」的判断。 官方文档
  2. 文本匹配只是提醒。官方示例自己就说明它会漏掉 git push -f 这样的写法。真正的边界放在 Git 托管平台的分支保护、托管设置这类 Mod 之外的地方。 官方文档
  3. 加 .catch 让它失败时拒绝,否则 hook 一出错就被跳过。
  4. 等用户决定时,等待放在 $.ui.ask 里,并把默认答案设成拒绝。
  5. deny 文案写给 Claude 看:说清不能做什么、该改做什么。
  6. 知道自己在链上的位置:排在你前面的 Mod 代答后,你的 hook 根本不运行。 编辑推导
  7. 守卫可能不在场:--safe-mode、worker 崩溃三次、Anthropic 远程关闭、hook 被跳过,都会让它消失。.catch 只保护那一个 hook,不是系统级保证。 编辑推导

状态、预算与失败恢复

三层状态,寿命各不相同

模块变量

写法最简单(如 let calls = 0)。每次重载都重新初始化,热重载、/reload-plugins 后归零。

$.state

会话内保留,重载不丢。/clear、/resume、/branch 会把它重置为默认值,而且 session.start 不会再触发,要在 classic.SessionStart hook 里把存过的值重新装回来。

$.store

跨会话保留,落在 ~/.claude/plugins/store/,总量上限 4 MiB 的 JSON。超过 cleanupPeriodDays 没有读写会被清理。

所以「跨重载存数据」用 $.state 就够;「跨会话存数据」才需要 $.store。两者不要混为一谈。

10 秒预算怎么算

计入 10 秒hook 自己的代码;自己 await 的 promise;$.clock.sleep
不计入next(e) 里花的时间;其他 mods API 调用里花的时间(如 $.ui.ask 等用户、$.process.run 跑进程)

另外:.catch 处理函数只有 1 秒;所有 session.end hook 合计 1.5 秒;$.process.run 默认 30 秒、最长 10 分钟。 官方文档

hook 失败之后发生什么

没有 .catch 的 hook 抛错、超时或返回形状不对时,取决于它是否已经调过 next:

在调 next 之前失败

这个 hook 被跳过,由下一个处理者接手。对拦截类 hook 来说,这等于放行。

在 next 返回之后失败

next 的结果照常生效,不会有任何东西再跑一遍。

会话不会因此中断,只留一行记录,如 my-mod: tool.call hook skipped: threw Error: boom。要注意「放行」的准确含义:是这个失败的 hook 不参与了,其余 Mod、权限规则、settings hook 仍按原样运行。

局部片段 · 让拦截类 hook 失败时拒绝(guard 是你的 hook 函数)
on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
  // next.error.kind 是 'throw' 或 'timeout'
  return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

worker 崩溃

所有已安装的 Mod 共用一个 worker 线程。 官方文档

  • 能定位到是哪个 Mod 卡死或弄崩了 worker(比如一个从不 await 的循环):只卸载那一个,记录为 first-mod was unloaded: it crashed the hooks worker。
  • worker 停了三次又定位不到:卸载所有非内置 Mod,包括组织安装的,记录为 hooks: mods that run in the hooks worker are off for this session: it crashed 3 times。执行 /reload-plugins 恢复。

测试、保留与分发

自动化测试

claude plugin test 在 shell 里运行,不需要会话、登录或网络。测试文件以 .test.ts 结尾,放在插件目录内任意位置,从 claude-code/testing 导入测试工具。有测试失败时退出码为 1,可以接进 CI。

完整示例 · first-mod/tests/first-mod.test.ts(官方代码,注释为中文转写)
import { expect, test } from 'claude-code/testing'

test('/tally reports the tool calls the mod has seen', async ($, on) => {
  // 替 Claude Code 回答每次工具调用,所以不会真的跑工具
  on('tool.call', () => ({ result: 'ok' }))

  // 触发两次工具调用,Mod 的 tool.call hook 会计数
  await $.tool.call({ tool: 'Bash', command: 'ls' })
  await $.tool.call({ tool: 'Read', file_path: 'README.md' })

  // 运行 /tally,检查 hook 返回的文字
  const answer = await $.command.run({ command: 'tally', args: '' })
  expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})

写测试时最容易踩的规则: 官方文档

  • 测试里的 $ 站在 Claude Code 的位置上,不是 hook 拿到的那个 mods API;on 用来注册 stub,替 Claude Code 作答。
  • 所有 stub 必须在第一次调用 $ 之前注册。
  • session.start 不会自动触发。hook 依赖它做的准备时,要先手动触发,并给 command.register 之类的调用配上 stub。
  • mods API 调用的 stub 返回 { value: ... };事件的 stub 返回该事件自己的结果形状,如 { result: 'ok' }。让调用失败用 { deny: '原因' }。
  • 定时器用 mock.clock(on) 拨时间;存储和环境变量有 mock.store、mock.env。
  • 界面用 $.ui.mount 画出某个渲染位,再用 press、input、select、find 操作和断言。它只检查 hook 返回的树是否合法,不检查实际画出来的样子,新布局还得进真实会话看一眼。

分发

  • Mod 就是插件:在 manifest 里写版本,别人用 /plugin 命令安装和更新;要给别人用就放进 marketplace。
  • 插件名不能像 Anthropic 官方的名字,例如以 claude- 开头的名字过不了 claude plugin validate。
  • 在 README 里写明测试过的 Claude Code 版本。
  • 已安装的插件按版本缓存。直接改已安装副本不会生效,要提高版本号重新安装。开发始终对着 --plugin-dir 的工作目录。

症状驱动的排错

Mod 的模块或 hook 出错时会被跳过、会话照常继续,所以「坏了」和「没反应」看起来一样。先做两个检查,再按症状找。

检查一:Claude Code 从文件里读到了什么

claude plugin validate ./first-mod。能查出拼错的事件名、有问题的 manifest、读不了的模块,不需要开会话。

检查二:那一行记录写在哪

热重载插件目录的交互会话(--plugin-dir,或为 Claude 代写的 Mod 开了热重载):transcript 里的暗色行。其他交互会话:只在调试日志里,用 claude --debug 启动。claude -p 加 --plugin-dir:stderr。

Mod 根本没加载

版本低于 2.1.287

处理
升级 Claude Code。

mods active 那一行里没有它

原因
hooks module 没加载。调试日志里找 hooks module <名字> not loaded:,冒号后面是理由。
处理
对照下面的拒绝消息。

validate 通过,但没有 hooks: 行

原因
hooks/hooks.json 没有 modules 键或拼错了。
处理
加上 "modules": ["./register.js"]。

hooks module did not load:

原因
模块加载失败,例如顶层代码抛错;理由里有文件和行号。
处理
按理由修。

options do not fit plugin.json userConfig

原因
某个选项不符合 userConfig 的定义,或必填项没填。
处理
改 settings.json 里对应的 pluginConfigs 条目。

第一次打开的目录里什么都不加载

原因
还没回答 trust 提示。
处理
在该目录启动交互会话并接受。

所有已安装插件都不加载

原因
用了 --safe-mode 启动。
处理
去掉这个参数。

拒绝加载的消息

hooks modules are turned off for installed plugins in this process

Anthropic 远程关闭,本机设置无法恢复。

disableAllHooks in managed settings

组织关掉了已安装插件的 hook。

only managed plugins and built-in plugins run

设了 allowManagedHooksOnly,或在托管设置之外的设置文件里设了 disableAllHooks。

installed plugins that are not managed load no hooks module in this mode (--bare)

用了 --bare 启动。

another plugin of that name loads first

两个插件重名,用的是托管的那个或先加载的那个。

mods are limited to your organization's by policy (allowManagedModsOnly)

内置守卫的消息:组织只允许自己的 Mod。

加载了,但 hook 被跳过或 Mod 被卸载

hook skipped:

原因
hook 抛错、超过 10 秒或返回形状不对。同一事件同一类失败只在界面提示一次,调试日志里每次都有。
处理
修掉报的错。

registered /tally but no command.run hook answered it

原因
没有 command.run hook、matcher 写的是别的命令、hook 返回了 next(e),或 hook 被跳过。
处理
找提到 command.run 的 hook skipped 行。

was unloaded: it crashed the hooks worker

原因
这个 Mod 卡住或弄崩了共用的 worker 线程。
处理
修掉阻塞线程的 hook。

mods that run in the hooks worker are off for this session: it crashed 3 times

原因
worker 停了三次且定位不到元凶,所有非内置 Mod 被卸载。
处理
/reload-plugins。

工具调用被拒

a hook changed this call's input after the model wrote it

原因
auto 模式下,有 hook 在分类器审查之后改了输入。
处理
Claude 会按原样重发一次;仍被拒说明那个 hook 每次都改,关掉它,或退出 auto 模式自己批准。

tried to lift a deny rule in your settings

原因
Mod 的 tool.check 批准了 deny 规则拒绝的调用,守卫保持拒绝。

the deny rules in your settings could not be checked for this call, so it is refused

原因
守卫在检查时失败,于是拒绝了调用(守卫自身失败时拒绝)。

界面不出现或没反应

面板或信息带是空的,或显示的是 Claude Code 原来的内容

原因
hook 返回的元素树没通过校验,记录形如 ui.render (Pane) refused: ...; the engine drew its own。
处理
常见是给了元素不接受的 prop,或用了该应用没有的元素。

$.ui.open 执行了但没有面板

原因
不是由用户操作触发,且终端窄于 144 列(用户自己打开过一次面板之后,门槛降到 110 列)。
处理
从命令或按钮里打开,或检查返回值的 isPlaced。

快捷键没反应

原因
面板没有键盘焦点。
处理
按 Ctrl+X 再按 Tab,或点一下面板;从命令里用 focus: true 打开。

终端里正常,桌面 App 里不显示

原因
那个渲染位或元素在桌面端不可用。
处理
查参考区的渲染位和元素。

改动或数值丢了

改了代码不生效

原因
改的是已安装的插件,Claude Code 运行的是该版本的缓存副本。
处理
用 --plugin-dir 指向工作目录开发。

模块重载后数值归零

原因
模块变量每次重载都会重新初始化。
处理
放进 $.state 或 $.store。

/clear、/resume、/branch 之后数值重置

原因
这些命令把 $.state 重置为默认值,且不再触发 session.start。
处理
在 classic.SessionStart hook 里重新装载。

保存后出现 reload failed, the previous version stays loaded:

原因
这次保存让模块坏了。
处理
上一个可用版本继续运行,按理由修好再保存。

读调试日志

局部片段
claude --debug-file ./mod-debug.log --plugin-dir ./first-mod
tail -f ./mod-debug.log | grep first-mod      # 另开一个终端

想往调试日志里写自己的行:$.ui.log('message', { to: 'debug' })。

参考区

可折叠的速查表,以 v2.1.287 文档为基线。精确字段以本机类型声明文件为准。

hook 函数与 next 的附属成员
  • 入口:export function register(on, options);注册:on(event, matcher?, hook),返回值可接 .catch(handler);hook 形如 async ($, e, next) => ...。
  • next.signal、next.origin(含 plugin、tier)、next.budget(含 ms、remainingMs)。
  • next.to(e, tier):仅 prepend / append 层的 Mod 可用。
  • next.error、next.called:仅在 .catch 处理函数里有。
事件族

工具

tool.call、tool.check、tool.describe

提示词与 Claude 读到的内容

prompt.submit、prompt.fill、prompt.suggest、prompt.edit、prompt.compose、prompt.section、prompt.context、prompt.attachment、skill.prompt、attribution.text

命令与配置

command.run、command.describe、config.set、config.describe

回合

turn.start、turn.step(异步生成器,可换模型)、turn.complete

会话

session.start、session.end、session.compact、session.receive、session.send、session.append、session.attach、session.detach、session.measure

子 Agent

agent.offer、agent.spawn

界面

ui.render、ui.resolve、ui.press、ui.input、ui.select、ui.focus、ui.scroll、ui.close、ui.message

其他 Mod 与引擎

plugin.register、engine.create

遥测

telemetry.log、telemetry.mark。'*' 不含它们,要按名字或 'telemetry.*' 挂。

settings hook 事件

classic.<事件名>,如 classic.Stop、classic.SessionStart;通配 'classic.*'。

每个 mods API 调用

方法名去掉 $. 就是事件名,如 fs.write。组织的策略 Mod 可以据此记录或拒绝其他 Mod 的调用。

$ 的 21 个命名空间

下面的分组是为了方便查找做的编辑归类,不是官方分类。 编辑推导

界面与交互

ui、command、prompt、audio

对话与 Agent

turn、session、agent、tool、mcp

模型

model(complete、fork、classify)

系统

fs、process、http、env、clock

状态与配置

state、store、config、settings

插件与遥测

plugin、telemetry

hooks module 的运行环境是受限的:没有 Node API、没有 setTimeout、没有自带的网络和文件访问,一切经由 $。这是运行环境的形态,不是安全沙箱。

渲染位与元素

渲染位

Pane、AbovePrompt、UserMessage、AssistantMessage、ToolUse、ToolResult、ToolGroup、CommandOutput、AskUserQuestion、Spinner、SessionMode、PromptHint

仅终端的渲染位

ToolProgress、TurnDuration、InfoNotice

元素

Box、Text、Button、Link、Code、Markdown、Input、Select、Client

平台专属元素

仅桌面:Svg。仅终端:Raster、Image。

权限弹窗不是渲染位,Mod 改不了。输入框本身不是「完全不能碰」:有 prompt.fill、prompt.suggest 事件,也有 Input 元素可画自己的输入框。

限制速查

时间

hook 自身 10 秒;.catch 1 秒;session.end 合计 1.5 秒;$.process.run 默认 30 秒、最长 10 分钟;单个测试默认 5 秒、toast 默认显示 4 秒,两者都可用 timeoutMs 改。

大小

$.fs 单文件 4 MiB;$.store 合计 4 MiB JSON;会话消息取最新 4096 条;$.model.complete 的 maxTokens 默认 1024。

频率

重画每秒 10 次;可见的面板和信息带每秒 30 次。

终端宽度

自动打开面板需要至少 144 列;用户自己打开过一次之后是 110 列。这是终端列数,和本页的网页断点无关。

命名

命令、工具、子 Agent 类型和面板的名称不超过 64 个字符,只能用字母、数字、_ 和 -。这条不针对插件名。

组织管控设置

allowManagedModsOnly

内置守卫的选项,写在托管设置的 pluginConfigs["cc-plugin-sec-default@builtin"].options 下。用户自带的 Mod(安装的、--plugin-dir 的、Claude 代写的)都不加载;他们的 settings hook、状态栏、/goal 不受影响。

allowModsToOverrideDenyRules

内置守卫的选项。不设时 deny 规则压过用户的 Mod;设为 true 后用户的 Mod 可以批准 deny 规则拒绝的调用。

allowManagedHooksOnly

范围更大:只有组织的 Mod 和内置 Mod 加载,同时挡掉用户自己设置文件里的 hook。

disableAllHooks

范围最大:写在托管设置里时,停掉所有已安装插件的 Mod(含组织自己的)和所有 settings hook,托管的 PreToolUse hook 也不再拦截;自定义状态栏和 /goal 一起停。

disableSideloadFlags

启动时拒绝 --plugin-dir、--plugin-url,并阻止 Claude 代写的 Mod 加载;同时拒绝 --agents 和 --mcp-config。

prependPlugins / appendPlugins

决定组织 Mod 在链上的位置。设了 prependPlugins 就要把 sec-default@builtin 也列进去,否则守卫不加载。只从托管设置读取;仓库的设置文件无效。

什么算「组织的 Mod」

三个条件同时满足:托管的 enabledPlugins 启用了它;托管设置把它的 marketplace 指到本机一个绝对路径的目录;marketplace 用相对路径列出它(就地加载)。从 GitHub、git、URL、npm 拷进缓存的插件一律算用户的。

守卫保护的内容

用户的 Mod 改不了:托管 hook 收到和决定的内容、系统提示词、托管的 CLAUDE.md 等指令、任何 Mod 读到的设置、托管 MCP server 的工具与描述。除此之外不加限制。守卫读不到托管设置时会拒绝所有用户 Mod。

策略 Mod

放在 prependPlugins 的 Mod 可以挂 plugin.register:每个 Mod 加载前都会把 validate 那份清单(e.uses.calls、e.tier)交给它,返回 { refuse: '理由' } 即可拒绝加载。这个检查同样要加 .catch,否则出错时放行。

环境变量与其他设置名:CLAUDE_CODE_PLUGIN_DIRS、CLAUDE_CODE_PLUGIN_DIR_WATCH、pluginConfigs。具体取值见官方 reference 页的 Settings and environment variables 一节。

命令速查
  • claude plugin validate <目录>(可加 --strict、--json):静态检查并列出 hooks 与 calls。
  • claude plugin test [目录]:跑测试;在无 Mod 的目录里可用来判断 Mod 能否加载。
  • claude --plugin-dir <目录>(可重复):本次会话加载,保存即重载。
  • /reload-plugins:重新加载插件。
  • /plugin:查看、启用、停用;显示 mods active 行。
  • claude --debug、claude --debug-file <路径>:写调试日志。
  • claude --safe-mode:不加载已安装的 Mod 和其他自定义项。

更正记录、时间线与来源

这次研究的起点是一篇流传较广的中文解读(公众号文章的 AI 摘要笔记)。下面逐条对照官方资料。每条标两个维度:依据的来源类型,和结论状态。

更正记录

「旧 hook 只能拦截」

官方实际
settings hook 还能改工具调用的参数和结果、给 Claude 追加上下文。Mod 多出来的是画界面、自定义命令、改回合与模型请求。
来源与状态
官方文档 与文档冲突

「内置 Mod 源码在 mods/ 目录下全部公开」

官方实际
公开的是四个:diff、agents-md、sec-default、telemetry,外加类型文件。plugin-authoring 只有 skill;you-should-know 不在其中。
来源与状态
官方文档 源码仓库 与文档冲突

「claude -p、dontAsk 等情况下不加载」

官方实际
这份清单只针对 Claude 当场写的 Mod(没人批准)。已安装或 --plugin-dir 的 Mod 在 claude -p 和 Agent SDK 里 hook 照常执行,只是不画界面。
来源与状态
官方文档 与文档冲突

「2.1.287 提示词 token 减少 8328 个(-26.6%),提示词文件少 13 个」

核对结果
数字出自第三方账号的统计,官方 changelog 没有这一条;把它归因于 Mods 是文章作者自己的猜测。不能说它是假的,只能说未经官方证实。
来源与状态
二手转述 待核实

「没有沙箱」与教程里的「sandbox of its own」

官方实际
两句话说的是两回事。模块的运行环境受限(无 DOM、无 Node API);但 $.fs、$.process、$.http 以用户权限执行,没有安全沙箱。「没有沙箱」的结论正确。
来源与状态
官方文档 已证实

「Blast Radius 用接管模式,直接返回 deny」

官方实际
它先按住调用、做 dry run、问用户,用户选 Cancel 才拒绝,选 Proceed 就继续。教程强调它是安全网而不是权限系统。
来源与状态
官方博客 表述不完整

「sec-default 保护 deny 规则」

官方实际
只在守卫加载的环境成立(托管设置或 Team/Enterprise 登录),且组织可以放开。其他环境里 Mod 可以批准 deny 规则拒绝的调用。
来源与状态
官方文档 缺少前提

「跨重载存数据用 $.state」

官方实际
成立,但要补一句:$.state 是会话级的,/clear、/resume、/branch 会重置它;跨会话要用 $.store。
来源与状态
官方文档 已证实

「接管」这个译名

官方实际
文档的正式名称是 Answer;官方介绍里也有 take it over 的说法,译成「接管」可以接受。
来源与状态
官方文档 已证实

「issue 标题是 Mods - make Claude 10x more extensible」「两百多条讨论」及团队成员引言

核对结果
提案 issue #91870 存在(2026-09-03 提出,09-09 更名 Claude Mods,10-01 回复已上线)。具体标题措辞、讨论数和引自 X 的发言,这次没有逐条打开一手出处。
来源与状态
二手转述 待核实

「plan mode 也将改成 Mod」「分叉子 Agent」「自定义记忆系统」

核对结果
官方博客的口径只有:计划把更多内置功能迁成 Mod,把 Claude Code 缩成一个小内核。具体到哪个功能,未见一手出处。
来源与状态
二手转述 待核实

「官方演示都录于早期版本」

核对结果
文章作者的说法,没有找到官方表述。
来源与状态
二手转述 待核实

仍未解决的问题 待核实

下面这些在这次打开的官方页面里没有找到明确答案,不要当成事实使用:

  • 卸载一个 Mod 后,它在 $.store 里的数据是否立即清除。
  • 已安装 Mod 的版本锁定与回滚的推荐做法(组织侧有 update policy,个人侧未查)。
  • 同一层里互不依赖的多个用户 Mod,先后顺序由什么决定。
  • hook 是否可能并发重入,next.signal 在链上如何传播。
  • marketplace 上的插件是否有签名校验。

时间线

  • GitHub issue #91870 提出提案。 源码仓库
  • 提案更名为 Claude Mods。 源码仓库
  • v2.1.287 发布,changelog 写道「Added Claude Mods: plugins may now modify deeper behavior」;同版加入内置条目 You should know。官方博客与入门教程同日发布。 发布说明
  • 本页核验官方资料;v2.1.287 仍是最新版本。

来源清单

官方博客

Claude Code Mods 发布文章(2026-10-01)

Getting started with Claude Code Mods(入门教程,2026-10-01)

官方文档:参考与管理

Mods reference(as of v2.1.287)

Troubleshoot a mod

Manage mods for your organization

Configure permissions(Extend permissions with hooks 一节)

二手来源(被核对的对象)

公众号文章(2026-10-01)及其 AI 摘要笔记

这份资料是怎么做的

事实部分全部来自 2026-10-02 实际打开的官方页面;代码示例取自官方文档,只把注释转写成中文,个别做了缩写并已注明。标「编辑推导」的内容是基于官方事实做的归纳,标「二手转述」「待核实」的没有一手出处。全文没有在真实会话里跑过任何一个 Mod。