Plugin Caching & File Resolution:插件不是原地执行,而是物化的版本副本
对应官方文档:claude-code-docs/docs/plugins-reference.md 里的 Plugin caching and file resolution。
它解决了什么问题
在许多插件系统中,插件往往是在其“源位置”(比如 node_modules 或 Git 仓库)直接加载执行的。但在 Claude Code 里,这种“原地执行”模式被彻底摒弃了。
真正支撑 Claude Code 插件运行的是一个三层分离的物化模型:
- 意图层 (Intent):用户在
settings.json的enabledPlugins里声明“我想用这个插件”。 - 登记层 (Registry):
installed_plugins.json记录“这个插件目前在磁盘上的哪个精确版本路径”。 - 物料层 (Materialization):插件被下载或复制到
~/.claude/plugins/cache/下的版本化子目录中。
这意味着插件不是从 Marketplace、npm 或 Git 仓库直接运行,而是先被“克隆”到本地一个受控的版本目录中。这种设计的目的是离线可用、性能优化,以及将插件变成可验证、可多版本共存、且与安装源解耦的本地物料。
代码里的真实逻辑
Claude Code 的插件物化过程主要由 PluginInstallationManager 和 pluginLoader.ts 驱动,分为五个关键步骤:
1. 强制版本化缓存 (Versioned Caching)
当一个 Marketplace 插件被安装时,copyPluginToVersionedCache(...) 负责将其物化。目标路径不是随机的,而是由 getVersionedCachePath(...) 算的: ~/.claude/plugins/cache/{marketplace}/{plugin}/{version}/ 其中 version 可能是一个 semver 字符串,也可能是一个 12 位的 Git SHA。即使是同一个插件的不同版本,在磁盘上也拥有完全独立的物料目录,互不干扰。
2. “安装”与“启用”的解耦
src/utils/plugins/installedPluginsManager.ts 维护的 installed_plugins.json 是真正的“安装登记册”。它记录了每个插件 ID (plugin@marketplace) 对应的 installPath。
- 安装:是指物料进入
cache/并登记在installed_plugins.json。 - 启用:是指
settings.json允许该插件进入当前会话。 这种设计允许系统在不删除物料的情况下,通过修改配置快速切换或禁用插件。
3. 启动时的 Cache-Only 加载
为了保证极速启动,Claude Code 默认执行 loadAllPluginsCacheOnly()。它只读取 installed_plugins.json 里已经物化好的路径。如果物料丢失(例如用户手动删除了 cache 目录),它不会立刻触发网络下载,而是记录一个 plugin-cache-miss 并跳过。只有在执行 /reload-plugins 或显式 refresh 时,才会进入完整的加载链。
4. 磁盘更新与内存视图分离
这是一个非常微妙的实现:installedPluginsManager.ts 在启动时会产生一份 inMemoryInstalledPlugins 会话快照。 当你更新一个插件时,系统会把新版本下载到新的 cache 目录,并更新磁盘上的 installed_plugins.json。但是,当前正在运行的会话仍然引用内存快照里的旧路径。除非重启或执行 /reload-plugins,否则新代码不会生效。这保证了单个会话内插件行为的一致性。
5. 变量替换与路径隔离
插件内部引用的路径通过两个核心变量进行解析:
${CLAUDE_PLUGIN_ROOT}:指向当前版本的安装根目录(即版本化缓存目录)。它是易失的,升级即更换。${CLAUDE_PLUGIN_DATA}:指向~/.claude/plugins/data/{plugin-id}/。这是持久的,跨版本保留。src/utils/plugins/pluginOptionsStorage.ts确保了插件数据不会因为版本切换而丢失,同时也限制了插件不应在 ROOT 目录下写入持久文件。
踩坑指南
- ROOT 是临时工,DATA 是钉子户:不要指望写在插件安装目录(ROOT)里的东西能活过下次升级,只有 DATA 目录才是真正的家。
- 更新不等于生效:看到“插件更新成功”的提示,只是意味着磁盘物料换了。不重启或不 reload,你运行的还是老代码。
- 缓存键是三元组:
Marketplace+Name+Version共同决定了路径。如果你手动修改了installed_plugins.json里的路径,可能会导致插件加载失败或被视为孤儿 (Orphan)。 - Seed 目录是隐藏的预装层:通过
CLAUDE_CODE_PLUGIN_SEED_DIR环境变量,企业可以预装一组“不可变”的插件缓存层,Claude Code 会优先读取它们而无需重新克隆。
相关主题
- 如果你想看版本号是如何从 Git SHA 或
plugin.json里算出来的,请看version-resolution.md。 - 如果你想了解如何绕过这套复杂的缓存机制进行快速本地调试,请看
plugin-local-testing.md。 - 如果你更关心 Marketplace 目录本身是如何被管理和同步的,请看
marketplace-and-discovery.md。
源码锚点
claude-code-opensource/src/utils/plugins/pluginLoader.ts
📄 src/utils/plugins/pluginLoader.ts
type CommandMetadata,
PluginHooksSchema,
PluginIdSchema,
PluginManifestSchema,
type PluginMarketplaceEntry,
type PluginSource,
} from './schemas.js'
import {负责计算 `getVersionedCachePath`、执行 `copyPluginToVersionedCache` 以及区分 `cacheOnly` 加载逻辑。
claude-code-opensource/src/utils/plugins/installedPluginsManager.ts
📄 src/utils/plugins/installedPluginsManager.ts
type InstalledPlugin,
InstalledPluginsFileSchemaV1,
InstalledPluginsFileSchemaV2,
type InstalledPluginsFileV1,
type InstalledPluginsFileV2,
type PluginInstallationEntry,
type PluginScope,
} from './schemas.js'
// Type alias for V2 plugins map
type InstalledPluginsMapV2 = Record<string, PluginInstallationEntry[]>
// Type for persistable scopes (excludes 'flag' which is session-only)
export type PersistableScope = Exclude<PluginScope, never> // All scopes are persistable in the schema
import { getOriginalCwd } from '../../bootstrap/state.js'管理 `installed_plugins.json` 读写,以及维护 `inMemoryInstalledPlugins` 会话快照。
claude-code-opensource/src/utils/plugins/pluginDirectories.ts
📄 src/utils/plugins/pluginDirectories.ts
const PLUGINS_DIR = 'plugins'
const COWORK_PLUGINS_DIR = 'cowork_plugins'
/**
* Get the plugins directory name based on current mode.
* Uses session state (from --cowork flag) or env var.
*
* Priority:
* 1. Session state (set by CLI flag --cowork)
* 2. Environment variable CLAUDE_CODE_USE_COWORK_PLUGINS
* 3. Default: 'plugins'
*/
function getPluginsDirectoryName(): string {
// Session state takes precedence (set by CLI flag)
if (getUseCoworkPlugins()) {
return COWORK_PLUGINS_DIR
}
// Fall back to env var
if (isEnvTruthy(process.env.CLAUDE_CODE_USE_COWORK_PLUGINS)) {
return COWORK_PLUGINS_DIR
}
return PLUGINS_DIR
}定义了插件根目录、缓存目录以及持久数据目录 (`${CLAUDE_PLUGIN_DATA}`) 的路径语义。
claude-code-opensource/src/utils/plugins/pluginOptionsStorage.ts
📄 src/utils/plugins/pluginOptionsStorage.ts
type UserConfigSchema,
type UserConfigValues,
validateUserConfig,
} from './mcpbHandler.js'
import { getPluginDataDir } from './pluginDirectories.js'实现 `${CLAUDE_PLUGIN_ROOT}` 和 `${CLAUDE_PLUGIN_DATA}` 在插件配置中的实时变量替换。