Skip to content
源码分析手册

Plugin Caching & File Resolution:插件不是原地执行,而是物化的版本副本

对应官方文档:claude-code-docs/docs/plugins-reference.md 里的 Plugin caching and file resolution

它解决了什么问题

在许多插件系统中,插件往往是在其“源位置”(比如 node_modules 或 Git 仓库)直接加载执行的。但在 Claude Code 里,这种“原地执行”模式被彻底摒弃了。

真正支撑 Claude Code 插件运行的是一个三层分离的物化模型

  1. 意图层 (Intent):用户在 settings.jsonenabledPlugins 里声明“我想用这个插件”。
  2. 登记层 (Registry)installed_plugins.json 记录“这个插件目前在磁盘上的哪个精确版本路径”。
  3. 物料层 (Materialization):插件被下载或复制到 ~/.claude/plugins/cache/ 下的版本化子目录中。

这意味着插件不是从 Marketplace、npm 或 Git 仓库直接运行,而是先被“克隆”到本地一个受控的版本目录中。这种设计的目的是离线可用、性能优化,以及将插件变成可验证、可多版本共存、且与安装源解耦的本地物料

代码里的真实逻辑

Claude Code 的插件物化过程主要由 PluginInstallationManagerpluginLoader.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 目录下写入持久文件。

踩坑指南

  1. ROOT 是临时工,DATA 是钉子户:不要指望写在插件安装目录(ROOT)里的东西能活过下次升级,只有 DATA 目录才是真正的家。
  2. 更新不等于生效:看到“插件更新成功”的提示,只是意味着磁盘物料换了。不重启或不 reload,你运行的还是老代码。
  3. 缓存键是三元组Marketplace + Name + Version 共同决定了路径。如果你手动修改了 installed_plugins.json 里的路径,可能会导致插件加载失败或被视为孤儿 (Orphan)。
  4. 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.tsL109-116 of 3303
typescript
  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.tsL28-43 of 1269
typescript
  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.tsL22-44 of 179
typescript
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.tsL25-29 of 401
typescript
  type UserConfigSchema,
  type UserConfigValues,
  validateUserConfig,
} from './mcpbHandler.js'
import { getPluginDataDir } from './pluginDirectories.js'
实现 `${CLAUDE_PLUGIN_ROOT}` 和 `${CLAUDE_PLUGIN_DATA}` 在插件配置中的实时变量替换。

基于 Claude Code v2.1.88 开源快照的深度分析