第22b章:插件系统 — 从打包到市场的扩展工程
定位:本章分析 Claude Code 的插件(Plugin)系统——扩展架构的顶层容器,从打包、分发到市场的完整工程。前置依赖:第22章。适用场景:想了解 CC 插件从打包到市场的扩展工程的读者。
为什么这很重要
第 22 章分析了技能系统(Skills)——Claude Code 如何让 Markdown 文件成为模型可执行的指令。但技能只是 Claude Code 扩展机制的冰山一角。当你想把一组技能、几个 Hook、一对 MCP 服务器和一套自定义命令打包成一个可分发的产品时,你需要的不是技能系统,而是插件系统。
插件(Plugin)是 Claude Code 扩展架构的顶层容器。它回答的不是"如何定义一个能力",而是一系列更难的问题:如何发现能力?如何信任它?如何安装、更新、卸载它?如何让一千个用户使用同一个插件而不互相干扰?
这些问题的工程复杂度远超技能本身。Claude Code 用近 1700 行的 Zod Schema 定义插件清单格式,用 25 种 discriminated union 错误类型处理加载失败,用版本化缓存隔离不同插件版本,用安全存储分流敏感配置。这套基础设施使得一个闭源的 AI Agent 产品获得了类似开源生态的扩展能力——而这正是本章要分析的核心设计。
如果说第 22 章分析的是"插件里装了什么",本章分析的是"插件这个容器本身是怎么设计的"。
源码分析
22b.1 插件清单:近 1700 行 Zod Schema 的设计
插件的一切始于 plugin.json——一个 JSON 清单文件,定义了插件的元数据和它提供的所有组件。这个清单的验证 Schema 占了 1681 行(schemas.ts),是 Claude Code 中最大的单个 Schema 定义。
清单的顶层结构由 11 个子 Schema 组合而成:
// restored-src/src/utils/plugins/schemas.ts:884-898
export const PluginManifestSchema = lazySchema(() =>
z.object({
...PluginManifestMetadataSchema().shape,
...PluginManifestHooksSchema().partial().shape,
...PluginManifestCommandsSchema().partial().shape,
...PluginManifestAgentsSchema().partial().shape,
...PluginManifestSkillsSchema().partial().shape,
...PluginManifestOutputStylesSchema().partial().shape,
...PluginManifestChannelsSchema().partial().shape,
...PluginManifestMcpServerSchema().partial().shape,
...PluginManifestLspServerSchema().partial().shape,
...PluginManifestSettingsSchema().partial().shape,
...PluginManifestUserConfigSchema().partial().shape,
}),
)
除了 MetadataSchema 外,其余 10 个子 Schema 都用了 .partial()——意味着插件可以只提供其中任意子集。一个只有 Hook 的插件和一个提供完整工具链的插件共享同一个清单格式,只是填的字段不同。
graph TB
M["plugin.json<br/>PluginManifest"]
M --> Meta["Metadata<br/>name, version, author,<br/>keywords, dependencies"]
M --> Hooks["Hooks<br/>hooks.json 或内联"]
M --> Cmds["Commands<br/>commands/*.md"]
M --> Agents["Agents<br/>agents/*.md"]
M --> Skills["Skills<br/>skills/**/SKILL.md"]
M --> OS["Output Styles<br/>output-styles/*"]
M --> Channels["Channels<br/>MCP 消息注入"]
M --> MCP["MCP Servers<br/>配置或 .mcp.json"]
M --> LSP["LSP Servers<br/>配置或 .lsp.json"]
M --> Settings["Settings<br/>预设值"]
M --> UC["User Config<br/>安装时提示用户"]
style M fill:#e3f2fd
style Meta fill:#fff3e0
style UC fill:#fce4ec
这个设计有三个值得注意的地方。
第一,路径安全验证。 清单中所有文件路径都必须以 ./ 开头,并且不能包含 ..。这防止插件通过路径遍历访问宿主系统的其他文件。
第二,市场名称保留机制。 清单验证对市场名称进行了多层过滤:
// restored-src/src/utils/plugins/schemas.ts:19-28
export const ALLOWED_OFFICIAL_MARKETPLACE_NAMES = new Set([
'claude-code-marketplace',
'claude-code-plugins',
'claude-plugins-official',
'anthropic-marketplace',
'anthropic-plugins',
'agent-skills',
'life-sciences',
'knowledge-work-plugins',
])
验证链包括:不能有空格、不能包含路径分隔符、不能冒充官方名称、不能用保留名 inline(用于 --plugin-dir 会话插件)和 builtin(用于内置插件)。这些验证全部在 MarketplaceNameSchema 中完成(第 216-245 行),使用 Zod 的 .refine() 链式表达。
第三,命令可以内联定义。 除了从文件加载,命令还可以通过 CommandMetadataSchema 内联:
// restored-src/src/utils/plugins/schemas.ts:385-416
export const CommandMetadataSchema = lazySchema(() =>
z.object({
source: RelativeCommandPath().optional(),
content: z.string().optional(),
description: z.string().optional(),
argumentHint: z.string().optional(),
// ...
}),
)
source(文件路径)和 content(内联 Markdown)二选一。这让小型插件可以在 plugin.json 中直接嵌入命令内容,而不必创建额外的 Markdown 文件。
22b.2 生命周期:从发现到组件加载的 5 阶段
一个插件从磁盘上的文件到被 Claude Code 使用,经历 5 个阶段:
flowchart LR
A["发现<br/>marketplace 或<br/>--plugin-dir"] --> B["安装<br/>git clone / npm /<br/>复制到版本化缓存"]
B --> C["验证<br/>Zod Schema<br/>解析 plugin.json"]
C --> D["加载<br/>Hooks / Commands /<br/>Skills / MCP / LSP"]
D --> E["启用<br/>写入 settings.json<br/>组件注册到运行时"]
style A fill:#e3f2fd
style C fill:#fff3e0
style E fill:#e8f5e9
发现阶段有两个来源(按优先级):
// restored-src/src/utils/plugins/pluginLoader.ts:1-33
// Plugin Discovery Sources (in order of precedence):
// 1. Marketplace-based plugins (plugin@marketplace format in settings)
// 2. Session-only plugins (from --plugin-dir CLI flag or SDK plugins option)
安装阶段的关键设计是版本化缓存。每个插件被复制到 ~/.claude/plugins/cache/{marketplace}/{plugin}/{version}/ 目录,而不是直接在原始位置运行。这保证了:同一插件的不同版本互不干扰;卸载只需删除缓存目录;离线场景下可以从缓存启动。
加载阶段使用 memoize 确保每个组件只加载一次。getPluginCommands() 和 getPluginSkills() 都是 memoized 的异步工厂函数。这对 Agent 性能很重要——Hook 可能在每次工具调用时触发,如果每次都重新解析 Markdown 文件,延迟会累积。
组件加载的优先级也值得注意。在 loadAllCommands() 中,注册顺序为:
- Bundled skills(编译时内置)
- Built-in plugin skills(内置插件提供的技能)
- Skill directory commands(用户本地
~/.claude/skills/) - Workflow commands
- Plugin commands(市场安装的插件命令)
- Plugin skills
- Built-in commands
这个顺序意味着:用户本地的自定义技能优先于插件提供的同名命令——用户的定制永远不会被插件覆盖。
22b.3 信任模型:分层信任与安装前审计
插件系统面临一个 Agent 特有的信任难题:插件不只是被动的 UI 扩展——它可以通过 Hook 在工具执行前后注入命令,通过 MCP 服务器提供新工具,甚至通过 skills 影响模型的行为。
Claude Code 的应对策略是分层信任。
第一层:持久化安全警告。 插件管理界面中,PluginTrustWarning 组件始终可见:
// restored-src/src/commands/plugin/PluginTrustWarning.tsx:1-31
// "Make sure you trust a plugin before installing, updating, or using it"
这不是一次性的弹窗确认,而是在 /plugin 管理界面中持续显示的警告。用户每次进入插件管理界面都会看到它——这比"安装时确认一次就再不提"的模式更安全,但又不至于像每次操作都弹窗那样干扰工作流。
第二层:项目级信任。 TrustDialog 组件对项目目录执行安全审计,检查是否存在 MCP 服务器、Hook、bash 权限、API key helper、危险环境变量等。信任状态存储在项目配置的 hasTrustDialogAccepted 字段中,并且沿目录层级向上查找——如果父目录已被信任,子目录继承信任。
第三层:敏感值隔离。 插件选项中标记为 sensitive: true 的值不存储在 settings.json 中,而是存入安全存储(macOS 上是 keychain,其他平台是 .credentials.json):
// restored-src/src/utils/plugins/pluginOptionsStorage.ts:1-13
// Storage splits by `sensitive`:
// - `sensitive: true` → secureStorage (keychain on macOS, .credentials.json elsewhere)
// - everything else → settings.json `pluginConfigs[pluginId].options`
加载时两个来源合并,安全存储优先:
// restored-src/src/utils/plugins/pluginOptionsStorage.ts:56-77
export const loadPluginOptions = memoize(
(pluginId: string): PluginOptionValues => {
// ...
// secureStorage wins on collision — schema determines destination so
// collision shouldn't happen, but if a user hand-edits settings.json we
// trust the more secure source.
return { ...nonSensitive, ...sensitive }
},
)
源码注释揭示了一个实际考量:memoize 不仅是性能优化,还是安全必需——每次从 keychain 读取会触发 security find-generic-password 子进程(约 50-100ms),如果 Hook 在每次工具调用时都触发,不 memoize 会导致明显的延迟。
22b.4 市场系统:发现、安装和依赖解析
插件市场(Marketplace)是一个 JSON 清单,描述了一组可安装的插件。市场源支持 9 种类型:
// restored-src/src/utils/plugins/schemas.ts:906-907
export const MarketplaceSourceSchema = lazySchema(() =>
z.discriminatedUnion('source', [
// url, github, git, npm, file, directory, hostPattern, pathPattern, settings
]),
)
这些类型覆盖了从"直接 URL"到"GitHub 仓库"到"npm 包"到"本地目录"的几乎所有分发方式。hostPattern 和 pathPattern 甚至支持根据用户的主机名或项目路径自动推荐市场——这是企业部署场景的设计。
市场加载使用优雅降级(graceful degradation):
// restored-src/src/utils/plugins/marketplaceHelpers.ts
loadMarketplacesWithGracefulDegradation() // 单个市场失败不影响其他市场
这个函数名本身就是一个设计声明:在多源系统中,任何单一来源的故障都不应该导致整个系统不可用。
依赖解析是另一个重要机制。插件可以在清单中声明依赖:
// restored-src/src/utils/plugins/schemas.ts:313-318
dependencies: z
.array(DependencyRefSchema())
.optional()
.describe(
'Plugins that must be enabled for this plugin to function. Bare names (no "@marketplace") are resolved against the declaring plugin\'s own marketplace.',
),
裸名称(如 my-dep)自动解析到声明插件所在的市场——这避免了强制依赖来自同一市场的插件时冗余书写市场名。
安装作用域分为 4 级:
| 作用域 | 存储位置 | 可见范围 | 典型用途 |
|---|---|---|---|
user | ~/.claude/plugins/ | 所有项目 | 个人常用工具 |
project | .claude/plugins/ | 项目所有协作者 | 团队标准工具 |
local | .claude-code.json | 当前会话 | 临时测试 |
managed | managed-settings.json | 受策略控制 | 企业统一管理 |
这四个作用域的设计与 Git 的配置层级(system → global → local)异曲同工,但增加了 managed 层用于企业策略控制。
22b.5 错误治理:25 种错误变体的类型安全处理
大多数插件系统用字符串匹配处理错误——"if error message contains 'not found'"。Claude Code 用了一种更严格的方式:discriminated union。
// restored-src/src/types/plugin.ts:101-283
export type PluginError =
| { type: 'path-not-found'; source: string; plugin?: string; path: string; component: PluginComponent }
| { type: 'git-auth-failed'; source: string; plugin?: string; gitUrl: string; authType: 'ssh' | 'https' }
| { type: 'git-timeout'; source: string; plugin?: string; gitUrl: string; operation: 'clone' | 'pull' }
| { type: 'network-error'; source: string; plugin?: string; url: string; details?: string }
| { type: 'manifest-parse-error'; source: string; plugin?: string; manifestPath: string; parseError: string }
| { type: 'manifest-validation-error'; source: string; plugin?: string; manifestPath: string; validationErrors: string[] }
// ... 还有 16 种更多变体
| { type: 'marketplace-blocked-by-policy'; source: string; marketplace: string; blockedByBlocklist?: boolean; allowedSources: string[] }
| { type: 'dependency-unsatisfied'; source: string; plugin: string; dependency: string; reason: 'not-enabled' | 'not-found' }
| { type: 'generic-error'; source: string; plugin?: string; error: string }
25 种唯一错误类型(26 个 union 变体,其中 lsp-config-invalid 出现了两次),每一种都有特定于该错误的上下文字段。git-auth-failed 带有 authType(ssh 还是 https),marketplace-blocked-by-policy 带有 allowedSources(允许的来源列表),dependency-unsatisfied 带有 reason(未启用还是未找到)。
源码注释还透露了一个渐进策略:
// restored-src/src/types/plugin.ts:86-99
// IMPLEMENTATION STATUS:
// Currently used in production (2 types):
// - generic-error: Used for various plugin loading failures
// - plugin-not-found: Used when plugin not found in marketplace
//
// Planned for future use (10 types - see TODOs in pluginLoader.ts):
// These unused types support UI formatting and provide a clear roadmap for
// improving error specificity.
先定义完整的类型,再逐步实现——这是一种"类型先行"的演进策略。定义好 22 种错误类型不需要全部立即实现,但一旦定义,新的错误处理代码就有了明确的目标类型,而不是不断添加新的 string case。
22b.6 自动更新与推荐:三种推荐来源
插件系统的"拉"(用户主动安装)和"推"(系统推荐安装)都有完整设计。
自动更新只对官方市场默认启用,但排除了部分市场:
// restored-src/src/utils/plugins/schemas.ts:35
const NO_AUTO_UPDATE_OFFICIAL_MARKETPLACES = new Set(['knowledge-work-plugins'])
更新完成后通过通知系统提示用户执行 /reload-plugins 刷新(详见第 18 章 Hook 系统)。这里有一个精妙的竞态处理:更新可能在 REPL 挂载前完成,所以通知使用了 pendingNotification 队列缓冲。
推荐系统有三个来源:
- Claude Code Hint:外部工具(如 SDK)通过 stderr 输出
<claude-code-hint />标签,CC 解析后推荐对应插件 - LSP 检测:编辑特定扩展名的文件时,如果系统上有对应的 LSP 二进制但未安装相关插件,自动推荐
- 自定义推荐:通过
usePluginRecommendationBase提供的通用状态机实现
三种来源共享一个关键约束:每个插件每次会话最多推荐一次(show-once semantics)。这通过配置持久化实现——已推荐过的插件 ID 记录在配置文件中,跨会话不重复。推荐菜单还有 30 秒自动消失机制,区分用户主动取消和超时取消,用于不同的分析事件。
22b.7 命令迁移模式:从内置到插件的渐进演化
Claude Code 正在将内置命令逐步迁移为插件。createMovedToPluginCommand 工厂函数揭示了这个演化策略:
// restored-src/src/commands/createMovedToPluginCommand.ts:22-65
export function createMovedToPluginCommand({
name, description, progressMessage,
pluginName, pluginCommand,
getPromptWhileMarketplaceIsPrivate,
}: Options): Command {
return {
type: 'prompt',
// ...
async getPromptForCommand(args, context) {
if (process.env.USER_TYPE === 'ant') {
return [{ type: 'text', text: `This command has been moved to a plugin...` }]
}
return getPromptWhileMarketplaceIsPrivate(args, context)
},
}
}
这个函数解决了一个实际问题:如何在市场尚未公开时迁移命令? 答案是按用户类型分流——内部用户(USER_TYPE === 'ant')看到安装插件的指示,外部用户看到原始的内联提示词。当市场公开后,getPromptWhileMarketplaceIsPrivate 参数和分流逻辑就可以移除。
已迁移的命令包括 pr-comments(PR 评论获取)和 security-review(安全审查)。迁移后的命令以 pluginName:commandName 格式命名,保持了命名空间隔离。
这个模式的深层意义在于:Claude Code 正在把自己从一个功能齐全的单体应用演化为一个平台。内置命令变成插件,意味着这些功能可以被社区替换、扩展或重新组合——而不需要 fork 整个项目。
22b.8 插件的 Agent 设计哲学意义
回到更高的视角。为什么一个 AI Agent 需要插件系统?
传统软件的插件系统(如 VS Code、Vim)解决的是"让用户自定义编辑器行为"——本质上是 UI 和功能的扩展。但 AI Agent 的插件系统解决的是一个完全不同的问题:Agent 能力的运行时可组合性。
Claude Code 的 Agent 在每次会话中能做什么,取决于它加载了哪些工具、技能和 Hook。插件系统让这个能力集合变成了动态可调的:
-
能力的可卸载性:用户可以禁用整个插件来关闭一组相关能力。这不是传统的"关闭一个功能"——而是让 Agent 在运行时丢掉某个维度的认知和行为能力。
-
能力的来源多元化:Agent 的能力不再只来自一个组织的开发团队,而是来自市场中的多个提供者。
createMovedToPluginCommand的存在证明了这个方向——连 Anthropic 自己的内置命令都在向插件迁移。 -
能力边界的用户控制:4 级安装作用域(user/project/local/managed)让不同的利益相关者控制不同层级的能力边界。企业管理员通过
managed策略限制允许的市场和插件;项目负责人通过project作用域为团队统一配置;开发者通过user作用域满足个人偏好。 -
信任作为能力的前置条件:在传统插件系统中,信任检查是安装时的一次性确认。在 Agent 上下文中,信任的意义更重——一个被信任的插件可以通过 Hook 在每次工具调用前后执行命令(详见第 18 章),通过 MCP 服务器提供新的工具给模型使用。这就是为什么 Claude Code 的信任模型是分层的、持续的,而不是一次性的。
从这个角度看,PluginManifest 的 11 个子 Schema 不只是"定义了插件能提供什么"——它们定义了Agent 能力的 11 个可插拔维度。
22b.9 开源与闭源之间的第三条路
Claude Code 是一个闭源的商业产品。但它的插件系统创造了一种有趣的中间地带——闭源核心 + 开放生态。
市场名称保留机制(第 22b.1 节)揭示了这个策略的具体实现。8 个官方保留名保护了 Anthropic 的品牌命名空间,但 MarketplaceNameSchema 的验证逻辑有意不拦截间接变体:
// restored-src/src/utils/plugins/schemas.ts:7-13
// This validation blocks direct impersonation attempts like "anthropic-official",
// "claude-marketplace", etc. Indirect variations (e.g., "my-claude-marketplace")
// are not blocked intentionally to avoid false positives on legitimate names.
这是一个经过权衡的设计:严格到足以防止冒充,但宽松到不压制社区使用 "claude" 一词构建自己的市场。
自动更新的差异化策略也反映了这个定位。官方市场默认启用自动更新,社区市场默认关闭——这给了官方市场一个分发优势,但没有阻止社区市场的存在。
安装作用域的 managed 层进一步揭示了商业考量。企业可以通过 managed-settings.json(只读策略文件)控制允许的市场和插件。这满足了企业客户"我的员工只能用经过审批的插件"的需求,同时保留了在审批范围内的扩展灵活性。
graph TB
subgraph Managed["Managed (企业策略)"]
direction TB
Policy["blockedMarketplaces /<br/>strictKnownMarketplaces"]
subgraph Official["Official (Anthropic)"]
direction TB
OfficialFeatures["保留名 + 默认自动更新"]
subgraph Community["Community (社区)"]
CommunityFeatures["自由创建,不自动更新"]
end
end
end
style Managed fill:#fce4ec
style Official fill:#e3f2fd
style Community fill:#e8f5e9
这个三层结构让 Claude Code 在商业和开放之间找到了平衡点:
- 对 Anthropic:保持核心产品闭源,通过官方市场控制质量和安全
- 对社区:提供完整的插件 API 和市场机制,允许第三方分发
- 对企业:通过策略层提供治理能力,满足合规需求
对 Agent 生态构建者的启示是:不需要开源你的核心来获得生态效应。只需要开放扩展接口、提供分发基础设施(市场)、以及建立治理机制(信任 + 策略),社区就能围绕你的 Agent 构建价值。
但这个模式有一个固有风险:生态依赖平台的善意。如果平台方收紧插件 API、限制市场准入或改变分发规则,生态参与者没有 fork 的退路——这是闭源核心相对于开源基金会治理的本质劣势。Claude Code 目前通过开放的清单格式和多源市场机制降低了这个风险,但长期的生态健康仍然取决于平台方的治理承诺。
模式提炼
模式一:清单即契约(Manifest as Contract)
解决的问题:扩展系统如何在不引入运行时错误的情况下验证第三方贡献?
代码模板:用 Schema 验证库(如 Zod)定义完整的清单格式,每个字段带有类型、约束和描述。清单验证在加载阶段完成,验证失败产生结构化错误而非运行时异常。所有文件路径必须以 ./ 开头,不允许 .. 遍历。
前置条件:扩展系统接受来自不可信来源的配置文件。
模式二:类型先行演进(Type-First Evolution)
解决的问题:如何在大型系统中渐进地改善错误处理,而不需要一次性重构所有错误站点?
代码模板:先定义完整的 discriminated union 错误类型(22 种),但只在少数站点实际使用(2 种),其余标记为"planned for future use"。新代码有了明确的目标类型,老代码可以逐步迁移。
前置条件:团队愿意容忍暂时未使用的类型定义,把它视为"类型路线图"而非"死代码"。
模式三:敏感值分流(Sensitive Value Shunting)
解决的问题:插件配置中的 API key、密码等敏感值如何安全存储?
代码模板:Schema 中每个配置字段标记 sensitive: true/false。存储时按标记分流——敏感值写入系统安全存储(如 macOS keychain),非敏感值写入普通配置文件。读取时合并两个来源,安全存储优先。使用 memoize 缓存避免重复的安全存储访问。
前置条件:目标平台提供安全存储 API(keychain、credential manager 等)。
模式四:闭源核心开放生态(Closed Core, Open Ecosystem)
解决的问题:闭源产品如何获得开源生态的扩展效应?
核心做法:开放扩展清单格式 + 多源市场发现 + 分层策略控制(详见 22b.9 的完整分析)。关键设计:保留品牌命名空间但不限制社区使用品牌词;官方市场有分发优势但不排斥第三方市场。
风险:生态健康依赖平台方的治理承诺,缺少 fork 退路。
前置条件:产品已有足够的用户基数使生态有吸引力。
用户能做什么
-
构建自己的插件:创建
plugin.json,在commands/、skills/、hooks/中放入组件文件,用claude plugin validate验证清单格式。从最小可用的单 Hook 插件开始,逐步添加组件。 -
设计插件的信任边界:如果你的插件需要 API key,在
userConfig中标记sensitive: true。不要在命令字符串中硬编码敏感值——使用${user_config.KEY}模板变量,让 Claude Code 的存储系统处理安全性。 -
利用安装作用域管理团队工具:把团队标准工具安装到
project作用域(.claude/plugins/),个人偏好工具安装到user作用域。这样.claude/plugins/可以提交到 Git,团队成员自动获得统一工具集。 -
为你的 Agent 设计插件系统时参考 Claude Code 的分层:清单验证(防御第三方输入)+ 版本化缓存(隔离)+ 安全存储分流(保护敏感值)+ 策略层(企业治理)。这四层是最小可用的插件基础设施。
-
考虑"命令迁移"策略:如果你的 Agent 有内置功能计划开放给社区维护,参考
createMovedToPluginCommand的分流模式——内部用户先迁移测试,外部用户保持现有体验,市场公开后统一切换。