源码路径 : src/plugins/ (内置插件注册), src/skills/ (技能加载核心), src/skills/bundled/ (内置技能)核心文件 : loadSkillsDir.ts (1087行), bundledSkills.ts (221行), builtinPlugins.ts (160行)本质 : 一套基于 Markdown frontmatter 的声明式扩展系统
一、插件 vs 技能 — 概念区分 这两个系统容易混淆,先厘清:
维度
Plugin (插件)
Skill (技能)
定义
一个可启用/禁用的功能包
一个特定任务的prompt模板
粒度
包含多个skills + hooks + MCP servers
单个markdown文件
管理
/plugin UI 开关
/skills 目录放文件即生效
来源
{name}@builtin 或 marketplace
项目/.claude/skills/ 或用户/全局
源码
src/plugins/builtinPlugins.ts
src/skills/loadSkillsDir.ts
简单说:Plugin是容器,Skill是内容。 一个Plugin可以包含多个Skills。
二、技能系统核心 (skills/loadSkillsDir.ts, 1087行) 2.1 技能是什么 一个Skill就是一个 带frontmatter的Markdown文件 ,放在约定的目录中:
<!-- 示例: .claude/skills/fix-tests.md --> --- description: 自动修复失败的测试 allowed-tools: - Bash - Edit - Readwhen_to_ use: 当用户要求修复测试或CI失败时 argument-hint: "[test file or pattern]" --- 当用户要求修复测试时,按以下步骤操作: 1. 运行 `npm test` 获取失败信息2. 分析每个失败的测试3. 修复代码或测试 4. 重新运行确认通过
2.2 技能加载来源 function getSkillsPath (source : SettingSource | 'plugin' , dir : 'skills' | 'commands' ): string { switch (source) { case 'policySettings' : return join (getManagedFilePath (), '.claude' , dir) case 'userSettings' : return join (getClaudeConfigHomeDir (), dir) case 'projectSettings' : return '.claude/skills' case 'plugin' : return 'plugin' } }
加载优先级(高→低):
1. 管理员技能 /etc/claude-code/.claude/skills/ (IT管理员部署) 2. 用户技能 ~/.claude/skills/ (个人全局) 3. 项目技能 .claude/skills/ (项目级, 可git提交) 4. 内置技能 编译到binary中 (Anthropic提供) 5. MCP技能 MCP服务器注册 (远程工具扩展)
2.3 Frontmatter 完整字段定义 从 parseSkillFrontmatterFields() 提取的完整字段列表:
{ displayName : string | undefined description : string hasUserSpecifiedDescription : boolean allowedTools : string [] argumentHint : string | undefined argumentNames : string [] whenToUse : string | undefined version : string | undefined model : string | undefined disableModelInvocation : boolean userInvocable : boolean hooks : HooksSettings | undefined executionContext : 'fork' | undefined agent : string | undefined effort : EffortValue | undefined shell : FrontmatterShell | undefined }
2.4 Skill 到 Command 对象的转换 createSkillCommand() 是关键的转换函数,它将解析出的frontmatter + markdown body转换为运行时 Command 对象:
export function createSkillCommand ({ ... } ): Command { return { type : 'prompt' , name : skillName, description, allowedTools, whenToUse, async getPromptForCommand (args, toolUseContext ) { let finalContent = baseDir ? `Base directory for this skill: ${baseDir} \n\n${markdownContent} ` : markdownContent finalContent = substituteArguments (finalContent, args, true , argumentNames) if (baseDir) { const skillDir = process.platform === 'win32' ? baseDir.replace (/\\/g , '/' ) : baseDir finalContent = finalContent.replace (/\$\{CLAUDE_SKILL_DIR\}/g , skillDir) } finalContent = finalContent.replace ( /\$\{CLAUDE_SESSION_ID\}/g , getSessionId () ) if (loadedFrom !== 'mcp' ) { finalContent = await executeShellCommandsInPrompt (finalContent, ...) } return [{ type : 'text' , text : finalContent }] }, } }
2.5 变量替换系统 技能markdown中支持的变量:
变量
来源
用途
$ARGUMENTS
用户输入
/skill-name 这里是参数
$1, $2…
命名参数
frontmatter中的 arguments 字段
${CLAUDE_SKILL_DIR}
技能文件目录
引用技能附带的脚本/配置文件
${CLAUDE_SESSION_ID}
当前session
日志/跟踪
!`command`
shell执行
在prompt中内联执行shell命令(仅本地技能)
2.6 去重机制 async function getFileIdentity (filePath : string ): Promise <string | null > { try { return await realpath (filePath) } catch { return null } }
为什么需要去重: 用户可能有 ~/.claude/skills/test.md 和 ./claude/skills/test.md 指向同一个文件(通过符号链接)。realpath() 解析到规范路径后去重。
2.7 路径匹配 (条件化技能) function parseSkillPaths (frontmatter : FrontmatterData ): string [] | undefined { if (!frontmatter.paths ) return undefined const patterns = splitPathInFrontmatter (frontmatter.paths ) .map (pattern => pattern.endsWith ('/**' ) ? pattern.slice (0 , -3 ) : pattern) .filter (p => p.length > 0 ) if (patterns.every (p => p === '**' )) return undefined return patterns }
这使得技能可以声明”只在特定目录下生效”,例如:
三、内置技能 (skills/bundled/, 17个文件) 3.1 注册流程 export function initBundledSkills ( ): void { registerUpdateConfigSkill () registerKeybindingsSkill () registerVerifySkill () registerDebugSkill () registerLoremIpsumSkill () registerSkillifySkill () registerRememberSkill () registerSimplifySkill () registerBatchSkill () registerStuckSkill () if (feature ('KAIROS' ) || feature ('KAIROS_DREAM' )) registerDreamSkill () if (feature ('AGENT_TRIGGERS' )) registerLoopSkill () if (feature ('BUILDING_CLAUDE_APPS' )) registerClaudeApiSkill () if (shouldAutoEnableClaudeInChrome ()) registerClaudeInChromeSkill () }
3.2 BundledSkillDefinition 结构 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 []> }
3.3 附带文件提取机制 某些内置技能需要引用额外文件(如脚本、配置):
export function registerBundledSkill (definition : BundledSkillDefinition ): void { const { files } = definition 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) } } }
安全写入:
const SAFE_WRITE_FLAGS = process.platform === 'win32' ? 'wx' : fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW async function safeWriteFile (p : string , content : string ): Promise <void > { const fh = await open (p, SAFE_WRITE_FLAGS , 0o600 ) try { await fh.writeFile (content, 'utf8' ) } finally { await fh.close () } }
3.4 典型内置技能实例: /stuck
四、插件系统 (plugins/builtinPlugins.ts, 160行) 4.1 插件注册 const BUILTIN_PLUGINS : Map <string , BuiltinPluginDefinition > = new Map ()export const BUILTIN_MARKETPLACE_NAME = 'builtin' export function registerBuiltinPlugin (definition : BuiltinPluginDefinition ): void { BUILTIN_PLUGINS .set (definition.name , definition) }
4.2 插件启用/禁用逻辑 export function getBuiltinPlugins ( ): { enabled : LoadedPlugin []; disabled : LoadedPlugin [] } { const settings = getSettings_DEPRECATED () const enabled : LoadedPlugin [] = [] const disabled : LoadedPlugin [] = [] for (const [name, definition] of BUILTIN_PLUGINS ) { if (definition.isAvailable && !definition.isAvailable ()) continue const pluginId = `${name} @${BUILTIN_MARKETPLACE_NAME} ` const userSetting = settings?.enabledPlugins ?.[pluginId] const isEnabled = userSetting !== undefined ? userSetting === true : (definition.defaultEnabled ?? true ) const plugin : LoadedPlugin = { name, manifest : { name, description : definition.description , version : definition.version }, path : BUILTIN_MARKETPLACE_NAME , source : pluginId, repository : pluginId, enabled : isEnabled, isBuiltin : true , hooksConfig : definition.hooks , mcpServers : definition.mcpServers , } if (isEnabled) enabled.push (plugin) else disabled.push (plugin) } return { enabled, disabled } }
4.3 Plugin vs Bundled Skill 的区别
五、MCP技能桥接 (skills/mcpSkillBuilders.ts) MCP服务器可以通过 prompts/list 端点暴露prompt模板,Claude Code 将它们转换为 Skill:
MCP服务器声明: prompts/list → [{ name: "analyze-code", description: "...", arguments: [...] }] Claude Code转换: MCP prompt → createSkillCommand({ skillName: "mcp-server-name/analyze-code", loadedFrom: 'mcp', source: 'mcp', // 注意: MCP技能不执行内联shell命令 (安全) })
六、完整的技能执行流 用户输入: /fix-tests src/api/ 或模型判断 whenToUse 匹配自动调用 │ ▼ ┌─────────────────────────┐ │ 查找 Command by name │ │ (skills + bundled + │ │ managed + mcp) │ └──────────┬──────────────┘ │ ┌──────────┴──────────────┐ │ getPromptForCommand() │ │ 1. substituteArguments │ ← $ARGUMENTS = "src/api/" │ 2. ${CLAUDE_SKILL_DIR} │ ← 替换为技能文件目录 │ 3. ${CLAUDE_SESSION_ID} │ ← 替换为session ID │ 4. executeShellCommands │ ← 执行 !`...` 内联命令 │ 5. return ContentBlock │ └──────────┬──────────────┘ │ ┌──────────┴──────────────┐ │ context == 'fork'? │ │ yes → runForkedAgent() │ ← 在子agent中执行 │ no → 直接注入主循环 │ ← 作为user message └─────────────────────────┘