第14章:扩展面 —— Skills / Plugins / MCP / Commands
约 12 分钟 · 更新于 2026-09-01
第14章:扩展面 —— Skills / Plugins / MCP / Commands
前面十三章讲的是内核。本章讲外部怎么往里加东西。四条通道:技能(提示词包)、插件(打包分发)、MCP(外部工具与说明)、命令(用户操作面)。核心判断:它们最终都收敛成同一个 Command 对象,而"模型知道自己有哪些扩展"才是这套生态真正起作用的原因。
一、四条通道的关系
text
┌──────────────┐
.claude/skills/ ──────┤ │
插件里的 skills ──────┤ │
bundled skills ──────┤ Command ├──── Skill 工具(模型调用)
MCP 提供的 skills ──────┤ 统一对象 │
.claude/commands/ ─────┤ ├──── /xxx(用户敲)
插件里的 commands ──────┤ │
workflow 脚本 ──────┤ │
└──────────────┘
MCP 服务器 ──── 工具(进工具池)+ instructions(进系统提示词或 delta 附件)
插件 ────────── skills / commands / agents / hooks / MCP 配置 / output styles / LSP
关键结构:技能和命令是同一个东西的两个投影。
一个 .claude/skills/foo/SKILL.md:
- 模型可以通过 Skill 工具调用它(skill: "foo")
- 用户可以敲 /foo
两条路走的是同一个 Command 对象、同一份 markdown 内容。区别只在谁触发和参数怎么给。
commands.ts 里的加载函数把它们全合并 [源码 src/commands.ts:449]:
text
内建命令(71) + 插件命令 + skill 目录 + bundled skills
+ 内建插件 skills + MCP skills + workflow 命令
↓
meetsAvailabilityRequirement 过滤
二、Skills:frontmatter 决定一切
2.1 十七个 frontmatter 字段
parseSkillFrontmatterFields() 解析出的字段 [源码 src/skills/loadSkillsDir.ts:185]:
| 字段 | 类型 | 作用 |
|---|
| name | string | 显示名(可与目录名不同) |
| description | string | 模型看到的触发描述(缺省时从正文首段提取) |
| when_to_use | string | 补充触发说明,拼在 description 后 |
| allowed-tools | 工具列表 | 这个技能执行时的工具白名单 |
| disable-model-invocation | bool | 模型不能自动调,只能用户敲 / |
| user-invocable | bool(默认 true) | 用户能不能敲 / |
| model | string / inherit | 用哪个模型跑 |
| effort | 枚举/整数 | 推理强度 |
| agent | string | 用哪个 agent 跑 |
| context | 'fork' | 在 fork 里跑而不是主线程 |
| hooks | HooksSettings | 技能自带钩子 |
| paths | 路径模式 | 按路径触发(和 CLAUDE.md 规则同格式) |
| argument-hint | string | 参数提示 |
| arguments | string[] | 具名参数 |
| shell | 枚举 | 用哪个 shell |
| version | string | 版本 |
disable-model-invocation 和 user-invocable 是一对正交开关,四种组合:
| disable-model-invocation | user-invocable | 效果 |
|---|
| false | true | 默认:模型能调,用户能敲 |
| true | true | 只能用户敲(本仓 obsidian-cli 曾经用过这个) |
| false | false | 只能模型调(不出现在斜杠命令里) |
| true | false | 谁都调不了(等于禁用) |
context: 'fork' 很有意思:一个技能可以声明"我应该在 fork 里跑"——适合那些会产生大量中间输出的技能(第 12 章的判据)。
2.2 三个运行时变量
技能内容里可以用占位符 [源码 src/skills/loadSkillsDir.ts:356-372]:
ts
// Replace ${CLAUDE_SKILL_DIR} with the skill's own directory so bash
// commands can reference bundled scripts
finalContent = finalContent.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir)
// Replace ${CLAUDE_SESSION_ID} with the current session ID
finalContent = finalContent.replace(/\$\{CLAUDE_SESSION_ID\}/g, sessionId)
插件里的技能还多两个 [源码 src/utils/plugins/loadPluginCommands.ts:339-374]:
ts
// Replace ${CLAUDE_PLUGIN_ROOT} and ${CLAUDE_PLUGIN_DATA} with their paths
// Replace ${user_config.X} with saved option values. Sensitive keys …
// Replace ${CLAUDE_SKILL_DIR} with this specific skill's directory.
// Distinct from ${CLAUDE_PLUGIN_ROOT}: a plugin can contain multiple
// skills, so CLAUDE_PLUGIN_ROOT points to the plugin root while
// CLAUDE_SKILL_DIR points to the individual skill's subdirectory.
五个变量:CLAUDE_SKILL_DIR(这个技能的目录)、CLAUDE_PLUGIN_ROOT(插件根)、CLAUDE_PLUGIN_DATA(插件数据目录)、CLAUDE_SESSION_ID、user_config.X(插件的用户配置项)。
技能因此可以携带脚本:${CLAUDE_SKILL_DIR}/scripts/foo.py。本仓的 35+ 个技能大量用了这个模式。
allowed-tools 的替换顺序有个细节:
ts
// Substitute ${CLAUDE_PLUGIN_ROOT} in allowed-tools before parsing
先替换再解析——因为 allowed-tools 里可能写 Bash(${CLAUDE_PLUGIN_ROOT}/bin/tool:*) 这种带路径的规则。
2.3 技能的上下文成本被单独计算
ts
/**
* Estimates token count for a skill based on frontmatter only
*/
function estimateSkillTokens(skill) {
const frontmatterText = [skill.name, skill.description, skill.whenToUse].filter(Boolean).join(' ')
return roughTokenCountEstimation(frontmatterText)
}
[源码 src/skills/loadSkillsDir.ts:97]
只算 frontmatter,因为正文是按需加载的。 这就是第 6 章那个"技能清单只占上下文 1%"的计量基础——清单里只有名字和描述,完整内容要等 Skill 工具真的被调用。
这是 progressive disclosure(渐进披露)在这个系统里最标准的一个实现:先给一行描述,需要时才给全文。
2.4 .gitignore 也参与技能发现
ts
// … is gitignored — blocks e.g. node_modules/pkg/.claude/skills from
[源码 src/skills/loadSkillsDir.ts:887]
被 gitignore 的目录里的技能不会被加载。 否则装了一个带 .claude/skills/ 的 npm 包,它的技能就自动进了你的技能列表——这是个供应链风险面。
2.5 bundled skills:产品自带的技能
text
src/skills/bundled/
batch.ts claudeApi.ts claudeInChrome.ts debug.ts keybindings.ts
loop.ts loremIpsum.ts remember.ts scheduleRemoteAgents.ts
simplify.ts skillify.ts stuck.ts updateConfig.ts verify.ts …
第 6 章讲过它们在技能清单预算里享有特权:超预算时其它技能的描述被截断,bundled 的保持完整。
对照本会话
本教程写作时这个会话的技能列表里就有 simplify、loop、update-config、keybindings-help、claude-api、run、init、security-review、fewer-permission-prompts —— 全部是 bundled。它们和本仓自己的 35+ 个技能混在同一个列表里,但描述长度上的待遇不同。
三、Plugins:把一切打包
ts
export type LoadedPlugin = {
name: string
manifest: PluginManifest
path: string
source: string
repository: string
enabled?: boolean
isBuiltin?: boolean
sha?: string // git commit SHA 版本锁定
commandsPath?: string; commandsPaths?: string[]
commandsMetadata?: Record<string, CommandMetadata>
agentsPath?: string; agentsPaths?: string[]
skillsPath?: string; skillsPaths?: string[]
outputStylesPath?: string; outputStylesPaths?: string[]
hooksConfig?: HooksSettings
mcpServers?: Record<string, McpServerConfig>
lspServers?: Record<string, LspServerConfig>
settings?: Record<string, unknown>
}
[源码 src/types/plugin.ts:48]
一个插件可以提供七类东西:命令、agent、技能、输出风格、钩子、MCP 服务器、LSP 服务器。
src/utils/plugins/ 有 39 个文件,涉及:marketplace(市场)、dependencyResolver(依赖解析)、pluginVersioning(版本)、pluginBlocklist(黑名单)、pluginAutoupdate(自动更新)、zipCache(打包缓存)、orphanedPluginFilter(孤儿清理)、managedPlugins(管理端下发)、officialMarketplace*(官方市场)。
这已经是一个完整的包管理器。
3.1 内建插件
ts
export type BuiltinPluginDefinition = {
name: string
description: string
version?: string
skills?: BundledSkillDefinition[]
hooks?: HooksSettings
mcpServers?: Record<string, McpServerConfig>
/** Whether this plugin is available (e.g. based on system capabilities). Unavailable plugins are hidden entirely. */
isAvailable?: () => boolean
/** Default enabled state before the user sets a preference (defaults to true) */
defaultEnabled?: boolean
}
[源码 src/types/plugin.ts:19]
产品自带的能力也被包装成插件,出现在 /plugin 界面里,用户可以关掉。
isAvailable 那条注释很实际:"不可用的插件完全隐藏"——比如一个依赖某个系统能力的插件,在没有那个能力的机器上根本不显示,而不是显示成灰色。
3.2 插件的信任分级
第 12 章提到过 isSourceAdminTrusted()。在 MCP 锁定场景下:
ts
// When MCP is locked to plugin-only, skip frontmatter MCP servers for
// USER-CONTROLLED agents only. Plugin, built-in, and policySettings agents
// are admin-trusted — their frontmatter MCP is part of the admin-approved
// surface.
三档信任:管理端下发(policySettings)> 插件/内建 > 用户自己写的。
这个分级在企业场景下是必需的:管理员想控制"只能用我批准的 MCP 服务器",但又不能因此把管理员自己批准的插件带的 MCP 也拦掉。
四、MCP:不只是工具桥
4.1 六种传输方式
ts
z.enum(['stdio', 'sse', 'sse-ide', 'http', 'ws', 'sdk'])
[源码 src/services/mcp/types.ts:24]
加上代码里出现的 ws-ide 和 claudeai-proxy,第 9 章那个 McpServerType 枚举有 8 个值。
src/services/mcp/ 有 23 个文件、12238 行,其中 client.ts 3348 行、auth.ts 2465 行。
认证占了五分之一 —— OAuth 流程、端口回调(oauthPort.ts)、IdP 登录(xaaIdpLogin.ts)、令牌刷新。这是接入第三方服务的真实成本。
4.2 MCP 能注入行为说明
ts
function getMcpInstructions(mcpClients: MCPServerConnection[]): string | null {
const connectedClients = mcpClients.filter(c => c.type === 'connected')
const clientsWithInstructions = connectedClients.filter(c => c.instructions)
if (clientsWithInstructions.length === 0) return null
const instructionBlocks = clientsWithInstructions
.map(client => `## ${client.name}\n${client.instructions}`)
.join('\n\n')
return `# MCP Server Instructions
The following MCP servers have provided instructions for how to use their tools and resources:
${instructionBlocks}`
}
[源码 src/constants/prompts.ts:579]
MCP 协议允许服务器返回一段 instructions,那段文字会直接进模型的上下文。
这让 MCP 的价值远超"工具注册表":服务器不只说"我有这些工具",还说"该怎么用它们"。
你本会话里就有两个例子
本教程写作时这个会话的系统提示词里有:
## outline
Document markdown content must not begin with a top-level heading (H1) — the title is stored as a separate field… Document and collection markdown support @mentions using the syntax: @Display Name…
## claude.ai Context7
Use this server to fetch current documentation whenever the user asks about a library… Use even when you think you know the answer — your training data may not reflect recent changes. Prefer this over web search for library docs.
第二段尤其典型:它不是在描述工具,它是在纠正模型的默认行为。 这种指令只有服务器作者知道该写什么。
第 5 章讲过它的缓存代价:它是唯一一个 DANGEROUS_uncachedSystemPromptSection,理由是 "MCP servers connect/disconnect between turns"。而第 6 章讲了解法:改走 mcp_instructions_delta 附件。
4.3 MCP 也能提供技能
text
feature('MCP_SKILLS') → src/services/mcp/… + src/skills/mcpSkillBuilders.ts
mcpSkillBuilders.ts 只有 44 行,全是注释解释一个循环依赖问题 [源码 src/skills/mcpSkillBuilders.ts:5]:
ts
/**
* Write-once registry for the two loadSkillsDir functions that MCP skill
* discovery needs. This module is a dependency-graph leaf: it imports nothing
* but types, so both mcpSkills.ts and loadSkillsDir.ts can depend on it
* without forming a cycle (client.ts → mcpSkills.ts → loadSkillsDir.ts → … → client.ts).
*
* The non-literal dynamic-import approach ("await import(variable)") fails at
* runtime in Bun-bundled binaries — the specifier is resolved against the
* chunk's /$bunfs/root/… path, not the original source tree, yielding "Cannot
* find module './loadSkillsDir.js'". A literal dynamic import works in bunfs
* but dependency-cruiser tracks it, and because loadSkillsDir transitively
* reaches almost everything, the single new edge fans out into many new cycle
* violations in the diff check.
*/
三种方案全试过,全有问题:
- 直接 import → 循环依赖
- 变量动态 import → Bun 打包后运行时找不到模块(路径被解析到 /$bunfs/root/…)
- 字面量动态 import → 运行时能跑,但 dependency-cruiser 会追踪它,导致一堆新的循环违规
最后选了第四种:一个只依赖类型的叶子模块 + 运行时写一次的注册表。
这 44 行是全仓最好的"为什么这个看起来多余的间接层是必需的"文档范本。它把三条被否决的路都写下来了。
可迁移的判断 ㉘
当一个间接层的存在理由不是显而易见的,就把被否决的替代方案连同否决理由一起写进注释。
否则下一个人(或者下一个 AI)会"顺手简化"掉它,然后重新踩一遍三个坑。判断标准很简单:如果你花了超过半小时才找到这个方案,那就值得写下来。
五、Commands:用户的操作面
COMMANDS() 里 71 个内建命令 [源码 src/commands.ts:258],按功能分:
| 组 | 命令 |
|---|
| 上下文 | /clear /compact /context /resume /branch |
| 配置 | /config /model /effort /permissions /hooks /keybindings /color /output-style |
| 生态 | /mcp /plugin /skills /agents /memory |
| 工作流 | /plan /review /tasks /init /diff |
| 诊断 | /doctor /status /cost /context /heapDump |
| 集成 | /ide /chrome /desktop /mobile /installGitHubApp /installSlackApp |
| 其它 | /help /exit /copy /files /addDir /fast /advisor /btw |
注意 /hooks /permissions /mcp /plugin /skills /agents 这六个 —— 它们是治理面的 UI。一个系统把这些做成命令而不是"改配置文件重启",说明它认为这些东西会被频繁调整。
5.1 可用性过滤
ts
export function meetsAvailabilityRequirement(cmd: Command): boolean
[源码 src/commands.ts:417]
命令可以声明自己在什么条件下可用(订阅类型、入口、平台、feature gate)。不满足就不出现——和插件的 isAvailable 是同一个思路:不可用的东西完全隐藏,而不是显示成禁用。
5.2 斜杠命令不能当文本喂给模型
第 3 章那段队列过滤:
ts
// Slash commands are excluded from mid-turn drain — they must go through
// processSlashCommand after the turn ends (via useQueueProcessor), not be
// sent to the model as text.
if (isSlashCommand(cmd)) return false
[源码 src/query.ts:1573]
用户在模型跑的时候敲了 /compact,这条不能作为文本插进对话。 它得等这一轮结束,走命令处理器。
而技能类的斜杠命令是另一回事——它们会被展开成完整的提示词。第 5 章那条系统提示词说明了这个区别:
/<skill-name> (e.g., /commit) is shorthand for users to invoke a user-invocable skill. When executed, the skill gets expanded to a full prompt. Use the Skill tool to execute them. IMPORTANT: Only use Skill for skills listed in its user-invocable skills section - do not guess or use built-in CLI commands.
"不要猜,不要把内建 CLI 命令当技能调" —— 因为模型会把 /help、/clear 也当成技能去调 Skill 工具。
SkillTool 自己的描述里也重复了一遍 [源码 src/tools/SkillTool/prompt.ts:170]:
- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
- If you see a <command-name> tag in the current conversation turn, the skill has ALREADY been loaded - follow the instructions directly instead of calling this tool again
最后那条防的是一个很具体的循环:用户敲 /foo,系统把技能内容展开注入对话,模型看到内容后又去调一次 Skill 工具。判别信号是 <command-name> 标签。
六、为什么这套生态真的起作用
第 1 章那份研究报告里有个判断,本教程完全同意:
很多系统也有插件,也有工具,也有外部协议,但模型本身不知道:有哪些扩展、什么时候该用、怎么用。
Claude Code 让模型知道这三件事的手段,散落在前面各章:
| 模型怎么知道 | 机制 | 章节 |
|---|
| 有哪些技能 | skill_listing 附件(预算 1%,超了三级降级) | 第 6 章 |
| 有哪些 agent | agent_listing_delta 附件 | 第 6 章 |
| 有哪些延迟工具 | deferred_tools_delta 附件 | 第 6、8 章 |
| MCP 工具该怎么用 | mcp_instructions_delta 附件 / 系统提示词 | 第 5、6 章 |
| 什么时候该用技能 | 系统提示词 session guidance + SkillTool 描述 + skill_discovery 附件 | 第 5、6 章 |
| 什么时候该派 agent | 系统提示词 + AgentTool 描述里的委派协议 | 第 5、12 章 |
| 匹配到技能必须执行 | SkillTool 描述:"BLOCKING REQUIREMENT" | 本章 §6.1 |
七条通道,全部指向同一件事:让模型对自己的能力面有准确的、当下的认知。
6.1 "匹配到就必须执行"
text
Important:
- Available skills are listed in system-reminder messages in the conversation
- When a skill matches the user's request, this is a BLOCKING REQUIREMENT:
**invoke the relevant Skill tool BEFORE generating any other response about the task**
- **NEVER mention a skill without actually calling this tool**
- Do not invoke a skill that is already running
[源码 src/tools/SkillTool/prompt.ts:158]
"永远不要提到一个技能却不真的调用它" —— 这条治的是一个非常具体的失败:模型说"我可以用 xxx 技能来做这件事",然后自己动手做了。
对用户来说这是最糟的一种失败:技能存在、模型知道它存在、模型还提到了它,但就是没用。 用户以为技能生效了。
"BLOCKING REQUIREMENT"(阻塞性要求)这个词很重——它要求模型在生成任何关于这个任务的其它回复之前先调用技能。
可迁移的判断 ㉙
扩展生态的价值不取决于"能装多少扩展",取决于"模型对当前可用扩展的认知有多准确、多及时"。
三个必要条件:
- 列表要在上下文里(而且要有预算,不能无限膨胀)
- 列表要能增量更新(连上一个 MCP 服务器不能等到下次会话才生效)
- 要有强制执行的语义("匹配到就必须调用",而不是"你可以考虑用")
少任何一条,扩展就退化成"装了但用不上"。
七、和另外三个 harness 的扩展面对照
| Pi | dsh | Codex | Claude Code |
|---|
| 主通道 | TS 扩展文件 | cordis 插件 + 四层 patch | MCP / Skills / Hooks / Plugins / Code Mode | Skills / Plugins / MCP / Commands / Output Styles |
| 扩展能改内核吗 | 有限 | 能(连 agent loop 都是配置) | 有限 | 有限 |
| 技能是什么 | —— | 插件 | markdown 包 | markdown 包 + 17 个 frontmatter 字段 |
| 技能能带钩子吗 | —— | —— | —— | 能(hooks: frontmatter) |
| 技能能指定 agent / 模型 / fork 吗 | —— | —— | —— | 能(agent: / model: / context: fork) |
| 插件生态 | 无 | npm 包 | 有 | 完整包管理器(市场、版本锁、依赖、黑名单、自动更新、管理端下发) |
| 模型知道有哪些扩展吗 | 部分 | 部分 | 是 | 是,且是增量更新的 |
Claude Code 的扩展面最"厚",但它不允许扩展改内核——这和 dsh 是相反的取舍。
dsh 说"一切皆插件,连 agent loop 都能换";Claude Code 说"内核我来定,但你可以在每个明确的接缝上加东西"。
代价和收益都很清楚:Claude Code 的扩展写起来简单(一个 markdown 文件就是一个技能),但你没法改变它的循环结构、压缩策略、缓存边界。而这些恰恰是它花了最大力气优化的地方——不让你改,某种意义上正是它的产品承诺。
八、动手复核
bash
cd claude-code-deep-dive/extracted-source
# 1. 技能的 17 个 frontmatter 字段
sed -n '181,265p' src/skills/loadSkillsDir.ts
# 2. 五个运行时变量
grep -n 'CLAUDE_SKILL_DIR\|CLAUDE_PLUGIN_ROOT\|CLAUDE_PLUGIN_DATA\|CLAUDE_SESSION_ID\|user_config' \
src/skills/loadSkillsDir.ts src/utils/plugins/loadPluginCommands.ts
# 3. 技能 token 估算只算 frontmatter
sed -n '95,106p' src/skills/loadSkillsDir.ts
# 4. 插件能提供什么
sed -n '15,80p' src/types/plugin.ts
ls src/utils/plugins/ | wc -l
# 5. MCP:认证占了多少
wc -l src/services/mcp/*.ts | sort -rn | head
# 6. MCP instructions 注入
grep -n -A25 'function getMcpInstructions' src/constants/prompts.ts
# 7. 那 44 行「三条路都被否决」的注释
cat src/skills/mcpSkillBuilders.ts
# 8. SkillTool 的 BLOCKING REQUIREMENT
sed -n '155,175p' src/tools/SkillTool/prompt.ts
本机侧:
bash
/skills # 技能列表
/plugin # 插件(含内建插件)
/mcp # MCP 服务器与认证状态
/agents # agent 列表
九、总结
- 技能和斜杠命令是同一个 Command 对象的两个投影——模型走 Skill 工具,用户敲 /xxx
- 技能的 17 个 frontmatter 字段里,disable-model-invocation × user-invocable 是正交开关;context: 'fork'、agent:、hooks:、paths: 让技能远超"一段提示词"
- 五个运行时变量让技能能携带脚本;allowed-tools 里的变量要先替换再解析
- 技能的 token 成本只算 frontmatter,正文按需加载——这是渐进披露最标准的实现
- gitignore 的目录里的技能不加载——挡住 node_modules/*/.claude/skills 这条供应链风险
- 插件是一个完整的包管理器:市场、版本锁(commit SHA)、依赖解析、黑名单、自动更新、管理端下发;能提供七类东西
- 信任分三档:管理端 > 插件/内建 > 用户自写;MCP 锁定时按来源区分
- MCP 能注入 instructions,那段文字直接进上下文——它不只是工具注册表;MCP 模块里认证占了五分之一(2465 行)
- mcpSkillBuilders.ts 那 44 行注释是"为什么这个间接层是必需的"的范本——三条被否决的路全写下来了
- 模型对扩展的认知靠七条通道,核心是"列表在上下文里 + 能增量更新 + 有强制执行语义"
- "永远不要提到一个技能却不真的调用它" 是 BLOCKING REQUIREMENT——防的是最糟的那种失败:用户以为技能生效了
- Claude Code 的扩展面最厚,但不允许扩展改内核——和 dsh 相反的取舍,而"不让你改"某种意义上正是它的产品承诺
下一章收尾:四种 harness 哲学的完整对照,可迁移判断汇总,以及对本仓的具体启示。
- 第13章-内建Agent-专业化分工与对抗式验证
- 第15章-设计精华-四种harness哲学对照
- 第6章-附件与system-reminder-第二条注入通道 —— 技能清单预算与 delta 三兄弟
- 第5章-系统提示词-一个可编排的装配架构 —— MCP instructions 的位置与缓存代价
- 第11章-钩子系统-27个事件的治理层 —— 技能自带钩子
- Codex 教程第 13 章 —— 五条扩展通道的对照
- dsh 教程第 12 章 —— 全插件化的对照