Claude Code 源码解剖

Claude Code 的 Skill 与 Plugin 扩展系统

从 BundledSkillDefinition、loadSkillsDir、SkillTool、PromptCommand 白名单、MCP 技能桥接和 PluginManifest 拆解 Claude Code 如何把 Markdown、Hook、MCP、命令和市场打包成可治理的 Agent 能力扩展层。

第 16 章 22 分钟

Claude Code 的 Skill 与 Plugin 扩展系统

这一章要解决的问题是:Claude Code 怎样让一个 Markdown 文件变成模型可调用的能力?又怎样把多个技能、命令、Hook、MCP Server 和配置打包成可安装、可更新、可审计的插件?

源码里的答案不是“读一个目录里的 md 文件”这么简单。Skill 是可调用的提示词模板,Plugin 是扩展能力的容器。前者影响模型如何行动,后者决定能力从哪里来、是否可信、如何安装、如何卸载、是否能触发 Hook 或提供新工具。

核心直觉

Skill 解决“能力如何被模型发现和调用”,Plugin 解决“能力如何被分发和治理”。Claude Code 把扩展系统做成多层信任模型:内置技能、用户技能、项目技能、MCP 远程技能、插件技能和企业托管技能拥有不同的加载、预算和权限边界。

先看源码入口

源码定位关键符号负责什么
skills/bundledSkills.tsBundledSkillDefinitionregisterBundledSkill()内置技能注册,把定义转换为标准 Command
skills/bundled/index.tsinitBundledSkills()按 Feature Flag 和用户类型注册内置技能
skills/loadSkillsDir.tsgetSkillDirCommands()parseSkillFrontmatterFields()createSkillCommand()加载用户、项目、策略、附加目录和旧 commands 技能
tools/SkillTool/SkillTool.tsSkillTool.call()checkPermissions()validateInput()模型调用 Skill 的入口,处理 inline/fork、远程技能和权限
tools/SkillTool/prompt.tsSKILL_BUDGET_CONTEXT_PERCENTformatCommandsWithinBudget()控制技能列表注入上下文的预算和截断
skills/mcpSkillBuilders.tsregisterMCPSkillBuilders()打破 MCP 技能加载和普通技能加载之间的循环依赖
utils/hooks/skillImprovement.tsinitSkillImprovement()createSkillImprovementHook()从用户纠正中自动改进项目级技能
utils/skills/skillChangeDetector.tsFILE_STABILITY_THRESHOLD_MSskillsChanged监听技能文件变更,防抖后清缓存热重载
utils/plugins/schemas.tsPluginManifestSchema插件清单 Zod Schema,定义插件能提供的 11 类组件
utils/plugins/pluginLoader.tsmarketplace plugins、session plugins插件发现、安装缓存和组件加载
utils/plugins/pluginOptionsStorage.tsloadPluginOptions()敏感配置进 secure storage,非敏感配置进 settings

这些入口形成两条链路:Skill 从文件到模型上下文;Plugin 从市场或本地目录到运行时组件注册。

总流程图

flowchart TD A["启动或重载"] --> B["initBundledSkills()"] A --> C["getSkillDirCommands()"] A --> D["getPluginSkills() / getPluginCommands()"] C --> E["loadSkillsFromSkillsDir()<br/>policy/user/project/add-dir"] C --> F["parseSkillFrontmatterFields()"] F --> G["createSkillCommand()"] D --> H["读取 plugin.json"] H --> I["PluginManifestSchema 验证"] I --> J["加载 commands / skills / hooks / MCP / LSP"] B --> K["Command 注册表"] G --> K J --> K K --> L["formatCommandsWithinBudget()<br/>1% 上下文预算"] L --> M["system-reminder 技能列表"] M --> N["模型选择 SkillTool"] N --> O{"checkPermissions()"} O -->|安全属性| P["自动授权"] O -->|allowedTools / hooks 等敏感字段| Q["用户确认"] P --> R{"context"} Q --> R R -->|inline| S["提示词注入主对话"] R -->|fork| T["executeForkedSkill()<br/>隔离上下文执行"] S --> U["技能文件 watcher 监听变更"] T --> U U --> V["clearSkillCaches()<br/>重新加载"]

这张图里的关键点是:技能列表只是发现层,完整技能内容在调用时加载;插件只是来源之一,最终也要变成 Command、Skill、Hook 或 MCP 组件才能进入 Agent。

技能的本质:提示词命令

内置技能通过 BundledSkillDefinition 注册。这个类型本身已经把 Skill 的能力边界暴露出来。

export type BundledSkillDefinition = {
  name: string
  description: string
  aliases?: string[]
  whenToUse?: string
  argumentHint?: string
  allowedTools?: string[]
  model?: string
  disableModelInvocation?: boolean
  userInvocable?: boolean
  isEnabled?: () => boolean
  hooks?: HooksSettings
  context?: 'inline' | 'fork'
  agent?: string
  files?: Record<string, string>
  getPromptForCommand: (
    args: string,
    context: ToolUseContext,
  ) => Promise<ContentBlockParam[]>
}

这段代码证明:Skill 不只是 Markdown 文本。它可以声明调用名、触发时机、允许工具、模型、执行上下文、附带文件和 Hook。但它的核心仍然是 getPromptForCommand(),也就是生成一段要注入模型上下文的内容。

几个字段尤其重要:

字段说明工程影响
whenToUse告诉模型什么时候该主动使用技能进入技能发现列表,影响模型选择
allowedTools技能执行时自动授权的工具敏感字段,需要权限确认
contextinlinefork决定污染主上下文还是隔离执行
disableModelInvocation禁止模型主动调用大规模操作只能用户显式触发
files技能附带参考文件首次调用时提取到磁盘
hooks技能附带 Hook能影响工具执行链,必须当作敏感能力

内置技能注册:记忆化 Promise 防竞态

registerBundledSkill() 会把定义转换为 Command。如果技能带 files,源码会延迟提取文件,并用同一个 Promise 防止并发重复写入。

if (files && Object.keys(files).length > 0) {
  skillRoot = getBundledSkillExtractDir(definition.name)
  let extractionPromise: Promise<string | null> | undefined
  const inner = definition.getPromptForCommand
  getPromptForCommand = async (args, ctx) => {
    extractionPromise ??= extractBundledSkillFiles(definition.name, files)
    const extractedDir = await extractionPromise
    const blocks = await inner(args, ctx)
    if (extractedDir === null) return blocks
    return prependBaseDir(blocks, extractedDir)
  }
}

这段代码证明:内置技能的参考文件不是启动时全部落盘,而是首次调用时惰性提取。extractionPromise ??= 保证多个并发调用等待同一个提取结果,避免两个调用者同时写同一批文件。

内置文件写入还用了 O_NOFOLLOW | O_EXCL、owner-only 权限和 per-process nonce。原文注释说明威胁模型:nonce 是主防线,O_NOFOLLOWO_EXCL 是防符号链接预创建的补充防线。这和权限系统一样,是 fail-closed 的扩展加载策略。

用户技能加载:四层来源与 frontmatter

用户自定义技能通常长这样:

.claude/skills/
  my-skill/
    SKILL.md
    reference.ts

SKILL.md 用 YAML frontmatter 声明元数据:

---
description: My custom skill
when_to_use: When the user asks for X
allowed-tools: Read, Grep, Bash
context: fork
model: opus
effort: high
arguments: [target, scope]
paths: src/components/**
---

getSkillDirCommands() 从多种来源并行加载:

const [
  managedSkills,
  userSkills,
  projectSkillsNested,
  additionalSkillsNested,
  legacyCommands,
] = await Promise.all([
  loadSkillsFromSkillsDir(managedSkillsDir, 'policySettings'),
  loadSkillsFromSkillsDir(userSkillsDir, 'userSettings'),
  // project / add-dir / legacy commands
])

这段代码证明:技能来源不是单一目录,而是策略管理、用户全局、项目本地、附加目录和旧版 commands 的组合。

来源目录开关或限制
策略管理@@INLINE_0@@/.claude/skills/企业策略,除非 CLAUDE_CODE_DISABLE_POLICY_SKILLS
用户全局~/.claude/skills/userSettingsskillsLocked 控制
项目本地.claude/skills/projectSettingsskillsLocked 控制
--add-dir@@INLINE_0@@/.claude/skills/附加目录
旧版 commands.claude/commands/兼容路径,已废弃

skillsLocked 来自 isRestrictedToPluginOnly(&#39;skills&#39;)。如果企业策略要求只能使用插件技能,本地技能加载会被跳过。这说明 Skill 系统本身已经接入组织治理,不是纯个人脚本目录。

元数据解析不是随意读字段

解析入口是 parseSkillFrontmatterFields()

export function parseSkillFrontmatterFields(
  frontmatter: FrontmatterData,
  markdownContent: string,
  resolvedName: string,
): {
  displayName: string | undefined
  description: string
  allowedTools: string[]
  argumentHint: string | undefined
  whenToUse: string | undefined
  model: ReturnType<typeof parseUserSpecifiedModel> | undefined
  disableModelInvocation: boolean
  hooks: HooksSettings | undefined
  executionContext: 'fork' | undefined
  agent: string | undefined
  effort: EffortValue | undefined
  shell: FrontmatterShell | undefined
}

这段代码证明:Skill 元数据有固定解析边界。effortmodelhooksagentshell 都不是普通文本,而是会影响运行时行为的字段。

effort 解析采用宽容策略:无效值会被忽略并记录调试日志,而不是让整个技能加载失败。这说明技能加载对用户编辑错误保持容错,但对敏感能力仍然通过权限链控制。

技能调用:变量替换、shell 执行与 MCP 降权

createSkillCommand()getPromptForCommand 会在调用时处理 Markdown。

flowchart TD A["原始 SKILL.md 正文"] --> B["添加 Base directory 前缀"] B --> C["参数替换<br/>$1 / $2 / 命名参数"] C --> D["${CLAUDE_SKILL_DIR}<br/>技能目录路径"] D --> E["${CLAUDE_SESSION_ID}<br/>当前会话"] E --> F{"loadedFrom === 'mcp' ?"} F -->|否| G["执行 !`command` / ```!``` shell"] F -->|是| H["禁止执行 shell"] G --> I["返回 ContentBlockParam[]"] H --> I

安全边界在源码里很明确:

// Security: MCP skills are remote and untrusted - never execute inline
// shell commands (!`…` / ```! … ```) from their markdown body.
if (loadedFrom !== 'mcp') {
  finalContent = await executeShellCommandsInPrompt(...)
}

这段代码证明:远程 MCP 技能可以提供 Markdown 指令,但不能让它的 Markdown 触发本地 shell。Claude Code 没有把所有技能来源等价对待,而是按来源降低执行能力。

条件技能:路径触发,而不是全部塞进上下文

Skill 可以通过 paths 声明只在特定文件路径相关时激活。

---
paths: src/components/**, src/hooks/**
---

加载时,带 paths 的技能不会直接进入活跃列表,而是放入 conditionalSkills

for (const skill of deduplicatedSkills) {
  if (
    skill.type === 'prompt' &&
    skill.paths &&
    skill.paths.length > 0 &&
    !activatedConditionalSkillNames.has(skill.name)
  ) {
    newConditionalSkills.push(skill)
  } else {
    unconditionalSkills.push(skill)
  }
}

这段代码证明:条件技能默认不占技能列表预算。只有当用户通过 Read、Write、Edit 等工具触碰匹配路径,activateConditionalSkillsForPaths() 才会用 gitignore 风格匹配把它激活。

一旦激活,activatedConditionalSkillNames 在清缓存时不会重置。这是“会话内保持激活”的语义:触摸过相关文件后,模型接下来都能看到该技能。

Monorepo 还有动态目录发现:操作深层文件时,系统会从文件目录向上走到 cwd,在每一级检查 .claude/skills/,并跳过 gitignore 路径。这个设计让包级技能可以随代码位置动态进入会话。

技能列表预算:只给 1% 上下文

技能发现列表会注入到 system-reminder,但这个列表不能无限增长。tools/SkillTool/prompt.ts 里有硬预算。

export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000
export const MAX_LISTING_DESC_CHARS = 250

这段代码证明:技能列表预算是上下文窗口的 1%。以 200K token 窗口估算,约 8,000 字符。每个技能描述还受 250 字符硬上限。

formatCommandsWithinBudget() 使用三级降级:

层级输出触发条件
Level 1技能名 + 完整描述总大小在预算内
Level 2内置技能完整描述,非内置技能截短描述超预算但还能保留至少 20 字符描述
Level 3内置技能完整描述,非内置技能仅名称描述预算太小

原文注释给出关键判断:列表只是 discovery,真正执行时 SkillTool 会加载完整内容。冗长 whenToUse 会浪费首轮 cache_creation tokens,却不一定提高匹配率。

容易误解

技能列表被截断,不代表技能内容被截断。列表负责让模型知道“有哪些能力”,调用后才加载完整 SKILL.md。

技能工具权限:安全属性白名单

并非所有技能调用都需要确认。SkillTool.checkPermissions 用白名单判断“只含安全属性”的技能可以自动授权。

const SAFE_SKILL_PROPERTIES = new Set([
  'type', 'progressMessage', 'contentLength', 'model', 'effort',
  'source', 'name', 'description', 'isEnabled', 'isHidden',
  'aliases', 'argumentHint', 'whenToUse', 'paths', 'version',
  'disableModelInvocation', 'userInvocable', 'loadedFrom',
])

这段代码证明:自动授权是白名单模式,不是黑名单模式。新增字段默认不安全,直到明确加入白名单。allowedToolshooks 这类字段不在白名单内,因此会触发用户确认。

权限规则支持精确匹配和前缀通配:

const ruleMatches = (ruleContent: string): boolean => {
  const normalizedRule = ruleContent.startsWith('/')
    ? ruleContent.substring(1)
    : ruleContent
  if (normalizedRule === commandName) return true
  if (normalizedRule.endsWith(':*')) {
    const prefix = normalizedRule.slice(0, -2)
    return commandName.startsWith(prefix)
  }
  return false
}

这段代码证明:用户可以写 Skill(review:*) allow 之类的规则,一次性授权命名空间下的技能。权限规则不只是 UI 开关,而是能表达组织化命名空间。

内联与分叉:技能执行的两种上下文

SkillTool.call() 的关键分支是 command.context === &#39;fork&#39;

if (command?.type === 'prompt' && command.context === 'fork') {
  return executeForkedSkill(...)
}
// inline 执行路径

这段代码证明:技能可以选择污染主对话,或在隔离上下文中执行。

模式行为适合
inline技能提示词进入主消息流,后续对话继续携带这些指令用户希望模型直接按流程继续操作
fork启动子 Agent 执行,返回摘要或结果大量搜索、审查、生成,不希望主上下文膨胀

inline 模式通过 contextModifier 注入临时工具授权和模型覆盖,而不是永久修改全局状态。这一点很重要:Skill 的影响范围应当绑定在本次调用,而不是悄悄改变整个会话环境。

远程技能桥接:用注册器断开循环依赖

MCP 技能需要复用普通技能的 createSkillCommand()parseSkillFrontmatterFields(),但直接互相 import 会形成循环依赖。源码用一次性注册模式拆开。

export type MCPSkillBuilders = {
  createSkillCommand: typeof createSkillCommand
  parseSkillFrontmatterFields: typeof parseSkillFrontmatterFields
}

let builders: MCPSkillBuilders | null = null

export function registerMCPSkillBuilders(b: MCPSkillBuilders): void {
  builders = b
}

export function getMCPSkillBuilders(): MCPSkillBuilders {
  if (!builders) {
    throw new Error(
      'MCP skill builders not registered - loadSkillsDir.ts has not been evaluated yet',
    )
  }
  return builders
}

这段代码证明:MCP 技能不是另一套解析器,而是复用普通技能构建器,只是通过注册器避免模块加载环。原文还提到不能简单用动态 import(),因为 Bun 的 bunfs 虚拟文件系统和 dependency-cruiser 都会带来额外问题。

远程技能使用 _canonical_@@INLINE_0@@ 前缀,在 SkillTool.validateInput() 中绕过本地命令注册表,直接查找本会话已发现的远程技能。远程技能可自动授权,但这个授权放在 deny 规则之后,所以用户仍然可以配置 Skill(_canonical_:*) deny

技能改进:从用户纠正回写 SKILL.md

SKILL_IMPROVEMENT 是技能系统最像“学习闭环”的部分。初始化受构建时和运行时双重门控。

export function initSkillImprovement(): void {
  if (
    feature('SKILL_IMPROVEMENT') &&
    getFeatureValue_CACHED_MAY_BE_STALE('tengu_copper_panda', false)
  ) {
    registerPostSamplingHook(createSkillImprovementHook())
  }
}

这段代码证明:即使代码被编进内部构建,也要经过 GrowthBook flag 才运行。技能自动改写会改变用户文件,必须能远程关闭。

触发条件也很窄:

  • 只分析项目级技能,通常带 projectSettings: 前缀。
  • 每 5 轮用户消息触发一次。
  • 只看上次检查后的新增对话片段。
  • 检测请求添加、修改、删除步骤、偏好表达和纠正。
  • 应用阶段用独立侧信道 LLM 改写 .claude/skills/@@INLINE_0@@/SKILL.md
  • 改写时保留 frontmatter,不删除已有内容,除非用户明确替换。

这说明 Skill Improvement 不是把聊天记录随便追加到技能末尾,而是把用户纠正当作结构化更新信号。

文件改写后,skillChangeDetector.ts 用 chokidar 监听变化。Bun 环境里改用 polling,因为原生 fs.watch() 存在死锁问题。变更检测还有 1 秒稳定阈值和 300ms 防抖,避免模型或编辑器写一半时就重载。

插件:扩展能力的容器

Skill 解决单个能力的定义。Plugin 解决的是分发和治理。插件清单 plugin.json 的 Schema 很大,原文指出 schemas.ts 约 1681 行,是 Claude Code 中最大的单个 Schema 定义之一。

顶层结构由 11 个子 Schema 组合:

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,
  }),
)

这段代码证明:插件不是“技能包”的同义词。它可以提供 Hook、Command、Agent、Skill、Output Style、Channel、MCP Server、LSP Server、Settings 和 User Config。除 Metadata 外,其余子 Schema 都 .partial(),说明插件可以只提供任意子集。

flowchart TD A["plugin.json"] --> B["Metadata"] A --> C["Hooks"] A --> D["Commands"] A --> E["Agents"] A --> F["Skills"] A --> G["Output Styles"] A --> H["Channels"] A --> I["MCP Servers"] A --> J["LSP Servers"] A --> K["Settings"] A --> L["User Config"]

清单路径有安全约束:文件路径必须以 ./ 开头,不能包含 ..。市场名也有保留机制,阻止插件市场冒充官方名称,同时刻意不过度拦截间接变体,降低误伤社区市场的概率。

插件生命周期:发现、安装、验证、加载、启用

插件加载的来源按优先级分为 marketplace-based plugins 和 session-only plugins。

1. Marketplace-based plugins
2. Session-only plugins from --plugin-dir or SDK plugins option

典型生命周期:

flowchart LR A["发现<br/>marketplace / --plugin-dir"] --> B["安装<br/>复制或拉取到版本化缓存"] B --> C["验证<br/>PluginManifestSchema"] C --> D["加载组件<br/>Hook / Command / Skill / MCP / LSP"] D --> E["启用<br/>settings.json / 运行时注册"]

版本化缓存的路径类似:

~/.claude/plugins/cache/{marketplace}/{plugin}/{version}/

这证明插件不会直接从原始位置随意运行。缓存按 marketplace、plugin 和 version 隔离,允许同一插件不同版本并存,也让卸载和离线启动更可控。

组件加载使用 memoize。getPluginCommands()getPluginSkills() 这类函数不会在每次工具调用时重新解析插件文件。对 Hook 尤其重要,因为 Hook 可能在每次工具执行前后触发,重复读 keychain 和文件会变成明显延迟。

插件信任模型:持续警告、项目信任、敏感值隔离

插件比普通技能更危险,因为它可以带 Hook、MCP Server 和 settings。Claude Code 的策略是分层信任。

第一层是插件管理界面的持续警告。PluginTrustWarning 不是安装时闪一下,而是在 /plugin 管理界面持续展示“安装、更新或使用前确认信任”。这说明信任不是一次性动作,而是管理插件时反复提醒的前置条件。

第二层是项目级信任。TrustDialog 会审计项目目录里是否存在 MCP Server、Hook、bash 权限、API key helper、危险环境变量等。信任状态沿目录层级向上查找,父目录信任可以覆盖子目录。

第三层是敏感配置隔离。

// sensitive: true  -> secureStorage
// everything else  -> settings.json pluginConfigs[pluginId].options

加载时,安全存储覆盖普通配置:

return { ...nonSensitive, ...sensitive }

这段代码证明:如果用户手动在 settings 里写了同名值,secure storage 仍然优先。源码注释还指出 memoize 是性能和安全共同需要,因为 macOS keychain 读取会触发 security find-generic-password 子进程,频繁读会拖慢 Hook。

插件市场与依赖解析

市场源支持 URL、GitHub、git、npm、本地文件、目录、hostPattern、pathPattern、settings 等多种来源。加载函数采用 graceful degradation:一个市场失败不影响其他市场。

依赖字段也在 schema 里明确:

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.',
  )

这段代码证明:插件依赖可以写裸名称,默认解析到声明插件所在市场。这减少了同市场依赖的冗余,也避免跨市场依赖被意外解析。

安装作用域有四级:

作用域存储位置可见范围用途
user~/.claude/plugins/所有项目个人常用扩展
project.claude/plugins/项目协作者团队标准能力
local.claude-code.json当前本地会话临时测试
managedmanaged-settings.json策略控制企业统一管理

这和技能来源一样,体现出 Claude Code 的扩展系统不是个人玩具,而是要进入团队和企业环境。

错误治理:PluginError 是 discriminated union

原文列出的 PluginError 类型包含 25 种左右错误变体。它不是字符串匹配。

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[] }
  | { type: 'dependency-unsatisfied'; source: string; plugin: string; dependency: string; reason: 'not-enabled' | 'not-found' }
  | { type: 'generic-error'; source: string; plugin?: string; error: string }

这段代码证明:插件系统希望错误能被 UI 和遥测结构化处理。git-auth-failedauthType,依赖缺失带 reason,市场被策略阻止带 allowed sources。这样的错误可以指导用户下一步,而不是只给一个“加载失败”。

源码注释还说明部分错误类型先定义、后逐步接入生产。这是一种类型先行的演进策略:先把错误空间建模出来,再逐渐替换泛化错误。

状态和数据结构

数据结构关键字段行为影响
BundledSkillDefinitionallowedToolscontextfileshooksgetPromptForCommand决定技能如何被调用、是否提取文件、是否需要确认
PromptCommandsourceloadedFromwhenToUsepaths统一内置、用户、MCP 和插件技能
SAFE_SKILL_PROPERTIES安全字段白名单新增未知字段默认需要权限
conditionalSkillsMap@@INLINE_0@@路径匹配前不注入上下文
activatedConditionalSkillNamesSet@@INLINE_0@@会话内一旦激活就保持可见
PluginManifest11 个组件子 Schema插件能力的完整声明面
PluginOptionValuessensitive / non-sensitive敏感配置进 secure storage
PluginErrortype 判别字段错误 UI、遥测和恢复路径可结构化

设计取舍

第一,Skill 是提示词,不是插件脚本。 Skill 的中心是 getPromptForCommand() 和 Markdown 正文。它可以声明工具、模型和上下文,但执行主体仍然是 LLM。这让最佳实践能以文本形式快速迭代。

第二,发现内容和执行内容分离。 技能列表只占 1% 上下文,调用时再加载完整内容。这个设计同时保护上下文预算和 Prompt Cache。

第三,安全用白名单而不是黑名单。 SAFE_SKILL_PROPERTIES 没有把危险字段列出来,而是只列安全字段。未来新增字段默认进入确认路径,这是扩展系统里很重要的保守默认。

第四,远程来源降权。 MCP 技能可以被发现和调用,但不能执行 Markdown 里的 shell。插件可以带 MCP 和 Hook,但需要项目信任和插件信任提醒。

第五,Plugin 是能力治理边界。 插件清单的 11 个子 Schema 不是“配置项很多”,而是在定义 Agent 能力的 11 个可插拔维度。安装作用域、版本缓存、依赖和错误类型都是为了让这个能力集合可治理。

读源码抓手

读源码抓手

读扩展系统时,不要先看某个具体技能写了什么。先追“一个技能如何进入模型上下文”和“一个插件如何变成多个运行时组件”。

建议路线:

  • 先读 skills/bundledSkills.ts,理解内置技能如何被转换成 Command。
  • 再读 skills/loadSkillsDir.tsparseSkillFrontmatterFields()createSkillCommand(),看用户技能如何解析、替换变量和执行 shell。
  • 接着看 tools/SkillTool/prompt.ts,理解技能列表为什么只有 1% 上下文预算。
  • 然后读 tools/SkillTool/SkillTool.tscheckPermissions(),重点看 SAFE_SKILL_PROPERTIES 和规则匹配。
  • 再读 skills/mcpSkillBuilders.ts,看 MCP 技能如何复用普通技能构建器但降低能力。
  • 最后进入 utils/plugins/schemas.tsutils/plugins/pluginLoader.ts,把 PluginManifest、版本缓存和组件加载串起来。

小结

小结

- Skill 是可调用的提示词模板,Plugin 是可分发、可治理的能力容器。 - 技能发现列表受 1% 上下文预算控制,完整内容在调用时才加载。 - SAFE_SKILL_PROPERTIES 让新增敏感能力默认需要确认,这是扩展系统的 fail-closed 核心。 - MCP 技能和插件技能不是“低配本地技能”,而是来自不同信任域,需要不同执行边界。 - PluginManifest 的 11 个组件子 Schema 定义了 Agent 能力可插拔的完整表面。