插件系统
市场、版本化缓存、依赖项解析、生命周期挂钩 — Claude Code 如何将 Git 存储库转变为一流的功能。
插件系统是 Claude Code 的可扩展性支柱。 它允许第三方作者(和 Anthropic 本身)发布斜杠命令、MCP 服务器、AI 代理、生命周期挂钩、LSP 集成、技能和输出样式 - 所有这些都来自 Git 存储库或 npm 包 - 而无需触及核心二进制文件。
utils/plugins/ — schemas.ts·pluginLoader.ts·marketplaceManager.ts·dependencyResolver.ts·pluginVersioning.ts·zipCache.ts·pluginAutoupdate.ts·pluginBlocklist.ts·pluginPolicy.ts·reconciler.ts
services/plugins/ — PluginInstallationManager.ts ·pluginOperations.ts ·pluginCliCommands.ts
有 五个概念层 你需要了解:
市场来源
GitHub 存储库、git URL、npm 包、本地目录、URL 托管的 JSON — 插件的来源。
清单架构
plugin.json 声明插件导出的内容:命令、挂钩、MCP 服务器、LSP、技能、代理。
版本化缓存
~/.claude/plugins/cache/{mkt}/{plugin}/{version}/ — 具有种子回退功能的不可变的每个版本快照。
依赖解析
安装时 DFS 关闭行走; 加载时定点降级传递。 跨市场部门被封锁。
Lifecycle
后台协调 → 自动更新 → 加载 → 钩子注册 → 命令注册 → MCP 连接。
该图跟踪了从用户意图到活动功能的完整插件旅程:
A marketplace 是一个目录——一个 marketplace.json 列出插件的文件。 Claude Code 支持六种源类型来获取该目录:
GitHub 存储库
Clones owner/repo 通过 SSH(或远程模式下的 HTTPS)。 官方市场用途 anthropics/claude-plugins-official.
任意 Git URL
HTTPS、SSH(git@), 或者 file://。 在克隆之前进行验证以防止注入。
Monorepo子目录
部分克隆(--filter=tree:0) + 稀疏结帐。 版本包含路径哈希以防止冲突。
HTTP/HTTPS 网址
直接获取市场 JSON。 官方市场的 GCS 后备通过 officialMarketplaceGcs.ts.
npm 包
安装到共享 npm-cache/node_modules/ 然后复制。 zip 缓存模式不支持。
本地路径
直接指向本地目录或 JSON 文件。 从 zip 缓存中排除(路径位于缓存目录之外)。
官方市场(claude-plugins-official) 是 隐式声明 — 如果任何启用的插件引用它,Claude Code 自动克隆 anthropics/claude-plugins-official 首次启动时。 无需用户配置。
保留名称保护
该架构强制执行两层模拟防御。 这
BLOCKED_OFFICIAL_NAME_PATTERN 正则表达式会阻止名称,例如
official-claude-plugins or anthropic-marketplace-v2。 对于允许列表中的保留名称(例如 claude-plugins-official),源组织必须是 anthropics/ 在 GitHub 上。
深入探讨——名称验证代码
// schemas.ts — blocked pattern for impersonation attempts
export const BLOCKED_OFFICIAL_NAME_PATTERN =
/(?:official[^a-z0-9]*(anthropic|claude)|(?:anthropic|claude)[^a-z0-9]*official|^(?:anthropic|claude)[^a-z0-9]*(marketplace|plugins|official))/i
// Non-ASCII blocks homograph attacks (Cyrillic 'а' ≠ Latin 'a')
const NON_ASCII_PATTERN = /[^\u0020-\u007E]/
export function validateOfficialNameSource(name, source): string | null {
// Reserved name? Must come from github.com/anthropics/
if (source.source === 'github') {
const repo = source.repo || ''
if (!repo.toLowerCase().startsWith(`anthropics/`)) {
return `The name '${name}' is reserved for official Anthropic marketplaces.`
}
}
return null
}
plugin.json)
每个插件都可以选择提供一个 plugin.json 在其根源。 Claude Code 使用严格的 Zod 模式解析此内容。 未知的顶级键被默默地剥离(对未来字段的弹性); 嵌套对象内的未知键,例如
userConfig or channels 仍然验证失败。
| Field | Type | Notes |
|---|---|---|
| name | string | 烤肉串盒,没有空格。 用于命名空间命令,如 /plugin:cmd. |
| version | 细绳? | 塞姆弗。 最高优先级版本源 — 覆盖 git SHA。 |
| description | 细绳? | 面向用户的描述如图所示 /plugin list. |
| dependencies | 细绳[]? | 裸名称继承声明插件的市场。 默认情况下阻止跨市场。 |
| commands | 路径| 路径[] | 记录<名称,元数据> | Supplement commands/ 目录。 对象形式支持内联内容。 |
| hooks | 路径| 钩子配置 | 大批 | Supplement hooks/hooks.json。 支持所有 20 多个生命周期事件。 |
| mcpServers | 路径| Mcpb路径| 记录| 大批 | Supports .mcpb/.dxt 捆绑包或内联服务器配置对象。 |
| lspServers | 路径| 记录| 大批 | 每个服务器: command, extensionToLanguage 地图、传输、环境、超时。 |
| agents | 路径| 小路[] | 附加代理 .md 文件超出 agents/ directory. |
| skills | 路径| 小路[] | 附加技能目录。 每个必须包含 SKILL.md. |
| outputStyles | 路径| 小路[] | 用于自定义渲染的输出样式定义。 |
| channels | ChannelDecl[] | 消息传递渠道(Telegram、Slack 等)。 绑定 MCP 服务器 + userConfig 提示。 |
| userConfig | 记录<键、选项> | 启用时提示用户可配置的值。 敏感值进入钥匙串。 |
| settings | 记录? | 启用插件时要合并的设置。 仅保留允许列表中的密钥(当前: agent). |
| 作者/主页/存储库/许可证/关键字 | metadata | 发现和归因字段。 |
完整注释的plugin.json示例
{
"name": "my-plugin",
"version": "1.2.0",
"description": "Example plugin showing all capability types",
"author": { "name": "Acme Corp", "url": "https://acme.example" },
"license": "MIT",
// Declare a dependency — bare name inherits this plugin's marketplace
"dependencies": ["shared-utils"],
// Commands: directory is auto-scanned + extra file + inline content
"commands": {
"hello": { "content": "Say hello to ${CLAUDE_PLUGIN_ROOT}" },
"deploy": { "source": "./docs/deploy.md", "argumentHint": "[env]" }
},
// Hooks: inline or path
"hooks": "./hooks/extra.json",
// MCP server via .mcpb bundle (pre-packaged binary)
"mcpServers": "./server.mcpb",
// LSP server for TypeScript
"lspServers": {
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"],
"extensionToLanguage": { ".ts": "typescript", ".tsx": "typescriptreact" }
}
},
// User-configurable values (prompted at enable time)
"userConfig": {
"API_KEY": {
"type": "string",
"title": "API Key",
"description": "Your service API key",
"sensitive": true, // → stored in keychain, not settings.json
"required": true
}
}
}
没有任何 plugin.json 字段,插件仍然有效。 Claude Code 自动发现
commands/*.md, agents/*.md, skills/*/SKILL.md,
hooks/hooks.json, 和 .mcp.json 按照惯例。 仅当您想要选择非常规路径或声明元数据时,清单才需要存在。
插件安装到不可变的内容寻址缓存中
~/.claude/plugins/cache/。 路径结构为:
# Path format
~/.claude/plugins/cache/{marketplace}/{plugin}/{version}/
# Example
~/.claude/plugins/cache/claude-plugins-official/sql-helper/1.3.2/
~/.claude/plugins/cache/acme-mkt/deploy-tool/a3f8c1d94b12/
~/.claude/plugins/cache/acme-mkt/monorepo-plugin/a3f8c1d94b12-e5a7c3f1/
^^ path hash for git-subdir
版本优先顺序
版本字符串的计算方式为 pluginVersioning.ts 以此优先级:
- plugin.json
versionfield — 显式 semver,最高权限 - 提供版本 从市场进入(例如,固定在
marketplace.json) - 预解析的 git SHA — 在克隆被丢弃之前捕获(git-subdir 情况)
- Git 提交 SHA 读自
.git/HEAD在安装路径中(前 12 个字符) 'unknown'作为最后的手段——仍然创建有效的缓存路径
对于 monorepo 插件 (source: "git-subdir"),同一提交的不同子目录中的两个插件将在同一 SHA 路径上发生冲突。 版本变成
{sha12}-{pathHash8} 其中路径哈希是子目录的规范化 SHA-256。 此规范化与 squashfs cron 逐字节匹配:反斜杠→正斜杠,条带前导 ./, 条带尾随 /.
种子缓存回退
企业部署可以预先填充只读种子目录(通过设置
CLAUDE_CODE_PLUGIN_SEED_DIR)。 安装时,加载程序会在执行任何网络获取之前探测种子。 这可以实现首次运行时零网络的场景。
压缩缓存模式
When CLAUDE_CODE_PLUGIN_USE_ZIP_CACHE=1 设置(无头/容器部署),插件存储为 .zip 文件而不是已安装文件存储上的目录。 在会话开始时,它们被提取到 claude-plugin-session-{hex}/ 临时目录并在退出时清理。 Unix 执行位通过 ZIP 中央目录保存
external_attr 字段 - 对于需要的钩子和脚本至关重要 +x.
深入研究——版本化路径计算
// pluginLoader.ts
export function getVersionedCachePathIn(
baseDir: string,
pluginId: string,
version: string,
): string {
const { name: pluginName, marketplace } = parsePluginIdentifier(pluginId)
// Sanitize each segment to prevent path traversal
const sanitizedMarketplace = (marketplace || 'unknown').replace(/[^a-zA-Z0-9\-_]/g, '-')
const sanitizedPlugin = (pluginName || pluginId).replace(/[^a-zA-Z0-9\-_]/g, '-')
const sanitizedVersion = version.replace(/[^a-zA-Z0-9\-_.]/g, '-')
return join(baseDir, 'cache', sanitizedMarketplace, sanitizedPlugin, sanitizedVersion)
}
// pluginVersioning.ts — git-subdir path hash
const normPath = source.path
.replace(/\\/g, '/') // backslash → forward slash
.replace(/^\.\//, '') // strip leading ./
.replace(/\/+$/, '') // strip trailing /
const pathHash = createHash('sha256').update(normPath).digest('hex').substring(0, 8)
const v = `${shortSha}-${pathHash}`
插件系统有两个具有不同语义的依赖解析通道,实现于 dependencyResolver.ts:
Install-time: resolveDependencyClosure()
递归 DFS 遍历,计算要安装的插件的完整传递闭包。 它强制执行三个规则:
- 循环检测 — 如果插件出现在当前 DFS 堆栈上,则返回
{ reason: 'cycle' } - 跨市场区块 — 来自市场 X 的插件 A 无法自动安装来自市场 Y 的插件 B(除非 Y 在 X 上)
allowCrossMarketplaceDependenciesOnallowlist) - 已启用跳过 — 已在设置中的 deps 被跳过以避免破坏版本引脚,但根永远不会被跳过(处理“缓存已清除但仍在设置中”的情况)
安装时 DFS walk — 源代码摘录
export async function resolveDependencyClosure(
rootId: PluginId,
lookup: (id: PluginId) => Promise<DependencyLookupResult | null>,
alreadyEnabled: ReadonlySet<PluginId>,
allowedCrossMarketplaces: ReadonlySet<string> = new Set(),
): Promise<ResolutionResult> {
const closure: PluginId[] = []
const visited = new Set<PluginId>()
const stack: PluginId[] = [] // DFS stack for cycle detection
async function walk(id, requiredBy) {
// Skip already-enabled DEPS (not root) to avoid stomping version pins
if (id !== rootId && alreadyEnabled.has(id)) return null
// Cross-marketplace security gate
const idMkt = parsePluginIdentifier(id).marketplace
if (idMkt !== rootMarketplace && !allowedCrossMarketplaces.has(idMkt)) {
return { ok: false, reason: 'cross-marketplace', dependency: id, requiredBy }
}
if (stack.includes(id)) return { ok: false, reason: 'cycle', chain: [...stack, id] }
if (visited.has(id)) return null
visited.add(id)
const entry = await lookup(id)
if (!entry) return { ok: false, reason: 'not-found', missing: id, requiredBy }
stack.push(id)
for (const rawDep of entry.dependencies ?? []) {
const dep = qualifyDependency(rawDep, id) // inherit marketplace if bare name
const err = await walk(dep, id)
if (err) return err
}
stack.pop()
closure.push(id) // post-order: deps come before dependents
return null
}
const err = await walk(rootId, rootId)
if (err) return err
return { ok: true, closure }
}
Load-time: verifyAndDemote()
从仅缓存加载集开始在每个会话上运行。 它是一个 定点循环:降级插件 A(因为缺少其 dep)可能会发现插件 B 依赖于 A,因此 B 也必须被降级。 重复该循环,直到没有发生任何变化。
Claude Code 的依赖模型受到 Debian apt 的启发:依赖关系是 存在保证,而不是模块导入。 插件 B 依赖于插件 A 意味着“当 B 运行时,A 的命名空间 MCP 服务器、命令和代理必须可用”——而不是 B 的代码导入 A 的代码。
命令和技能
命令位于 commands/*.md。 每个文件变成一个名为的斜杠命令
/plugin-name:command-name。 子目录创建命名空间:
commands/ci/build.md → /my-plugin:ci:build.
技能是包含以下内容的目录 SKILL.md。 当 Claude Code 找到时
SKILL.md 在子目录中 skills/,它将父目录名称注册为技能名称并注入 ${CLAUDE_SKILL_DIR}
所以该技能可以参考自己的支持文件。
命令和技能中的变量替换
在运行时,这些变量在命令/技能内容中被替换:
| Variable | 决心 |
|---|---|
| ${CLAUDE_PLUGIN_ROOT} | 插件安装目录的绝对路径 |
| ${CLAUDE_PLUGIN_DATA} | 插件的可写数据目录 |
| ${CLAUDE_SKILL_DIR} | 该技能的特定子目录(仅限技能模式) |
| ${CLAUDE_SESSION_ID} | 当前会话标识符 |
| ${user_config.KEY} | 用户配置选项(敏感键替换为占位符) |
Hooks
插件挂钩转换为 PluginHookMatcher[] 对象并与全局钩子配置一起注册。 支持每个钩子事件:
// All 20+ hook events a plugin can subscribe to:
PreToolUse | PostToolUse | PostToolUseFailure | PermissionDenied
Notification | UserPromptSubmit | SessionStart | SessionEnd | Stop | StopFailure
SubagentStart | SubagentStop | PreCompact | PostCompact
PermissionRequest | Setup | TeammateIdle
TaskCreated | TaskCompleted | Elicitation | ElicitationResult
ConfigChange | WorktreeCreate | WorktreeRemove
InstructionsLoaded | CwdChanged | FileChanged
钩子加载订阅 settingsChangeDetector。 什么时候
enabledPlugins 设置更改(例如,用户启用/禁用插件而不重新启动),插件挂钩会自动重新加载。 快照比较使用 JSON 序列化来检测更改。
政策封锁(managed-settings.json)
企业管理员可以通过设置强制禁用任何插件
enabledPlugins["name@marketplace"] = false in managed-settings.json。 这 isPluginBlockedByPolicy() 检查在安装阻塞点、启用操作和 UI 中运行 - 无法被用户或项目设置覆盖的单一事实来源。
删除和自动卸载
当市场设定时 forceRemoveDeletedPlugins: true, Claude Code 比较 installed_plugins.json 与启动时当前的市场清单相对应。 不再列出的插件会自动从所有用户控制的范围中卸载,并记录在标记插件文件中,以防止重新安装循环。
安装范围
插件可以安装在四个范围内。 只有前三个是用户可安装的:
用户范围
~/.claude/settings.json — 活跃于该用户的所有项目中。
项目范围
.claude/settings.json 在当前项目中——致力于回购。
本地范围
.claude/settings.local.json ——特定于项目,不承诺。
管理范围
由组织管理员通过设置 managed-settings.json。 对用户只读。
卸载警告:反向依赖检测
// dependencyResolver.ts
export function findReverseDependents(
pluginId: PluginId,
plugins: readonly LoadedPlugin[],
): string[] {
const { name: targetName } = parsePluginIdentifier(pluginId)
return plugins
.filter(p =>
p.enabled &&
p.source !== pluginId &&
(p.manifest.dependencies ?? []).some(d => {
const qualified = qualifyDependency(d, p.source)
// Bare dep from @inline plugin: match by name only
return parsePluginIdentifier(qualified).marketplace
? qualified === pluginId
: qualified === targetName
}),
)
.map(p => p.name)
}
// Result shown as: "warning: required by plugin-a, plugin-b"
启动时, autoUpdateMarketplacesAndPluginsInBackground() 静默运行,不会阻塞 REPL。 该流程分为三个阶段:
- 确定哪些市场有
autoUpdate: true— 官方市场默认为true,第三方默认为false - Run
refreshMarketplace()(git pull / re-fetch)每个自动更新市场 - Call
updatePluginOp()对于这些市场中每个已安装的插件
更新内容是 non-in-place:新版本缓存在新版本路径中,但运行会话继续使用旧路径。 REPL 通过以下方式通知
onPluginsAutoUpdated() 并显示重启提示。
自动更新完成时,可能尚未安装 REPL。 该模块将更新通知存储在 pendingNotification 并立即交付 onPluginsAutoUpdated() 最终由 REPL 调用。 这可以防止“更新在任何人收听之前完成”静默下降。
要点
- 插件是一个目录(或 ZIP),带有
plugin.json显现; 清单是可选的——基于约定的自动发现可以处理大多数情况。 - 市场是目录。 支持六种源类型:github、git、git-subdir、url、npm、目录/文件。 官方 Anthropic 市场是从引用它的任何插件中隐式声明的。
- 版本化缓存位于
~/.claude/plugins/cache/{mkt}/{plugin}/{ver}/每个版本都是不可变的。 git-subdir 插件在版本中编码路径哈希以防止 monorepo 冲突。 - 依赖关系解析使用两个通道:安装时的 DFS 闭包遍历(默认情况下阻止跨市场)和加载时的定点降级通道,以在会话开始时捕获损坏的依赖项。
- 插件是命名空间的:命令变成
/plugin-name:command,钩子标记为pluginId, MCP 服务器名称带有前缀。 这可以防止插件之间的冲突。 - 政策封锁(
managed-settings.json) 在安装时、启用时和 UI 中强制执行 - 它不能被用户或项目设置覆盖。 - 自动更新仅在后台进行且非阻塞。 当前会话看到旧代码; 新版本将被缓存以供下次重新启动。
- Sensitive
userConfig值(标记为sensitive: true)转到操作系统钥匙串,而不是settings.json。 它们在 MCP 服务器环境中可用,但从未替换为发送到模型的技能/代理内容。
知识检查
"dependencies": ["shared-utils"] 并且插件本身来自市场 acme-mkt,解析的依赖 ID 是什么?plugins/my-plugin/ 在 monorepo 中位于 git SHA abc123def456 子目录路径是 ./plugins/my-plugin。 版本是什么样的?"sensitive": true 在一个 userConfig 字段称为 API_KEY。 数值存储在哪里,能出现在技能内容中吗?anthropic-marketplace-v2 免遭第三方注册?