前面十三章讲的是内核。本章讲外部怎么往里加东西。四条通道:技能(提示词包)、插件(打包分发)、MCP(外部工具与说明)、命令(用户操作面)。核心判断:它们最终都收敛成同一个 Command 对象,而"模型知道自己有哪些扩展"才是这套生态真正起作用的原因。
┌──────────────┐
.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:
两条路走的是同一个 Command 对象、同一份 markdown 内容。区别只在谁触发和参数怎么给。
commands.ts 里的加载函数把它们全合并 [源码 src/commands.ts:449]:
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 章的判据)。
技能内容里可以用占位符 [源码 src/skills/loadSkillsDir.ts:356-372]:
插件里的技能还多两个 [源码 src/utils/plugins/loadPluginCommands.ts:339-374]:
五个变量:CLAUDE_SKILL_DIR(这个技能的目录)、CLAUDE_PLUGIN_ROOT(插件根)、CLAUDE_PLUGIN_DATA(插件数据目录)、CLAUDE_SESSION_ID、user_config.X(插件的用户配置项)。
技能因此可以携带脚本:${CLAUDE_SKILL_DIR}/scripts/foo.py。本仓的 35+ 个技能大量用了这个模式。
allowed-tools 的替换顺序有个细节:
先替换再解析——因为 allowed-tools 里可能写 Bash(${CLAUDE_PLUGIN_ROOT}/bin/tool:*) 这种带路径的规则。
[源码 src/skills/loadSkillsDir.ts:97]
只算 frontmatter,因为正文是按需加载的。 这就是第 6 章那个"技能清单只占上下文 1%"的计量基础——清单里只有名字和描述,完整内容要等 Skill 工具真的被调用。
这是 progressive disclosure(渐进披露)在这个系统里最标准的一个实现:先给一行描述,需要时才给全文。
[源码 src/skills/loadSkillsDir.ts:887]
被 gitignore 的目录里的技能不会被加载。 否则装了一个带 .claude/skills/ 的 npm 包,它的技能就自动进了你的技能列表——这是个供应链风险面。
第 6 章讲过它们在技能清单预算里享有特权:超预算时其它技能的描述被截断,bundled 的保持完整。
对照本会话 本教程写作时这个会话的技能列表里就有 simplify、loop、update-config、keybindings-help、claude-api、run、init、security-review、fewer-permission-prompts —— 全部是 bundled。它们和本仓自己的 35+ 个技能混在同一个列表里,但描述长度上的待遇不同。
[源码 src/types/plugin.ts:48]
一个插件可以提供七类东西:命令、agent、技能、输出风格、钩子、MCP 服务器、LSP 服务器。
src/utils/plugins/ 有 39 个文件,涉及:marketplace(市场)、dependencyResolver(依赖解析)、pluginVersioning(版本)、pluginBlocklist(黑名单)、pluginAutoupdate(自动更新)、zipCache(打包缓存)、orphanedPluginFilter(孤儿清理)、managedPlugins(管理端下发)、officialMarketplace*(官方市场)。
这已经是一个完整的包管理器。
[源码 src/types/plugin.ts:19]
产品自带的能力也被包装成插件,出现在 /plugin 界面里,用户可以关掉。
isAvailable 那条注释很实际:"不可用的插件完全隐藏"——比如一个依赖某个系统能力的插件,在没有那个能力的机器上根本不显示,而不是显示成灰色。
第 12 章提到过 isSourceAdminTrusted()。在 MCP 锁定场景下:
三档信任:管理端下发(policySettings)> 插件/内建 > 用户自己写的。
这个分级在企业场景下是必需的:管理员想控制"只能用我批准的 MCP 服务器",但又不能因此把管理员自己批准的插件带的 MCP 也拦掉。
[源码 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)、令牌刷新。这是接入第三方服务的真实成本。
[源码 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 附件。
mcpSkillBuilders.ts 只有 44 行,全是注释解释一个循环依赖问题 [源码 src/skills/mcpSkillBuilders.ts:5]:
三种方案全试过,全有问题:
最后选了第四种:一个只依赖类型的叶子模块 + 运行时写一次的注册表。
这 44 行是全仓最好的"为什么这个看起来多余的间接层是必需的"文档范本。它把三条被否决的路都写下来了。
可迁移的判断 ㉘ 当一个间接层的存在理由不是显而易见的,就把被否决的替代方案连同否决理由一起写进注释。
否则下一个人(或者下一个 AI)会"顺手简化"掉它,然后重新踩一遍三个坑。判断标准很简单:如果你花了超过半小时才找到这个方案,那就值得写下来。
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。一个系统把这些做成命令而不是"改配置文件重启",说明它认为这些东西会被频繁调整。
[源码 src/commands.ts:417]
命令可以声明自己在什么条件下可用(订阅类型、入口、平台、feature gate)。不满足就不出现——和插件的 isAvailable 是同一个思路:不可用的东西完全隐藏,而不是显示成禁用。
第 3 章那段队列过滤:
[源码 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 |
七条通道,全部指向同一件事:让模型对自己的能力面有准确的、当下的认知。
[源码 src/tools/SkillTool/prompt.ts:158]
"永远不要提到一个技能却不真的调用它" —— 这条治的是一个非常具体的失败:模型说"我可以用 xxx 技能来做这件事",然后自己动手做了。
对用户来说这是最糟的一种失败:技能存在、模型知道它存在、模型还提到了它,但就是没用。 用户以为技能生效了。
"BLOCKING REQUIREMENT"(阻塞性要求)这个词很重——它要求模型在生成任何关于这个任务的其它回复之前先调用技能。
可迁移的判断 ㉙ 扩展生态的价值不取决于"能装多少扩展",取决于"模型对当前可用扩展的认知有多准确、多及时"。
三个必要条件:
- 列表要在上下文里(而且要有预算,不能无限膨胀)
- 列表要能增量更新(连上一个 MCP 服务器不能等到下次会话才生效)
- 要有强制执行的语义("匹配到就必须调用",而不是"你可以考虑用")
少任何一条,扩展就退化成"装了但用不上"。
| 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 文件就是一个技能),但你没法改变它的循环结构、压缩策略、缓存边界。而这些恰恰是它花了最大力气优化的地方——不让你改,某种意义上正是它的产品承诺。
本机侧:
下一章收尾:四种 harness 哲学的完整对照,可迁移判断汇总,以及对本仓的具体启示。