Claude Code 源码分析第 03 课 · 第 02
第 03 课

技能系统

Claude Code 如何发现、加载、解析并执行可复用工作流:SKILL.md、frontmatter、fork 模式、条件技能与 MCP 技能。

1. 概览

在 Claude Code 里,技能(Skill)不是一段普通提示词,而是一种可发现、可复用、可版本化的工作流封装。它既可以是你仓库里的 SKILL.md 文件,也可以是编译进 CLI 的内置技能,或者来自 MCP 服务器的远程技能。

对用户来说,技能通常表现为一条斜杠命令;对系统来说,技能其实是一种受控提示注入机制,外加参数替换、工具权限、模型覆盖、子代理执行和生命周期钩子。

为什么是技能而不是简单的提示? 技能是 versioned, shareable (将它们签入您的存储库), discoverable (克劳德看到技能菜单和自动调用),并且 composable (一项技能可以调用其他技能)。 它们还带有权限提示、模型覆盖和生命周期挂钩。
斜杠命令 技能.md 文件 Bun进入CLI 通过 MCP 提供

2. 技能生命周期

每个技能从Claude Code开始到克劳德发挥作用都会经历六个阶段。

flowchart LR D["Discovery
Walk managed / user /
project / --add-dir paths.
Read dir entries.
Resolve symlinks."] L["Load
Read SKILL.md.
Parse YAML frontmatter.
Estimate token budget."] P["Parse
Extract all frontmatter
fields into Command
object. Validate hooks,
paths, effort, shell."] S["Substitute
Replace $ARGUMENTS,
$1/$name, shell !backtick,
${CLAUDE_SKILL_DIR},
${CLAUDE_SESSION_ID}."] E["Execute
Run inline (expand into
conversation) or fork
(isolated sub-agent with
own token budget)."] I["Inject
Tool result enters
conversation. Allowed
tools and model override
applied for this turn."] D --> L --> P --> S --> E --> I style D fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style L fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style P fill:#22201d,stroke:#6e9468,color:#b8b0a4 style S fill:#22201d,stroke:#c47a50,color:#b8b0a4 style E fill:#22201d,stroke:#8e82ad,color:#b8b0a4 style I fill:#22201d,stroke:#6e9468,color:#b8b0a4

Discovery

启动时 getSkillDirCommands() 并行行走四个位置并加载旧版 .claude/commands/ 目录。 符号链接通过以下方式解析 realpath() 因此,在任何技能到达克劳德之前,重复的文件(相同的内容,不同的路径)都会被删除。

Load

Each skill-name/SKILL.md 文件被读取。 令牌数为 仅根据前文估计 (名称+描述+whenToUse)——完整的正文在启动时不会被标记化。 即使有数百个技能,这也可以使技能列表保持快速。 列表本身的预算上限为 上下文窗口的 1%.

Parse

frontmatter 被解析为 Command 目的。 所有字段(描述、允许的工具、参数提示、模型覆盖、挂钩、路径、工作量、shell)均在此处进行验证。 技能与 paths 场成为 条件技能 — 它们会被存储,但不会向 Claude 展示,直到用户打开匹配的文件。

Substitute

调用时,技能体按以下顺序进行参数替换:

  1. 命名参数: $foo, $bar (按位置映射 arguments frontmatter)
  2. 索引参数: $ARGUMENTS[0], $0, $1
  3. 完整参数字符串: $ARGUMENTS
  4. 如果没有找到占位符并且参数存在,它们将被附加为 ARGUMENTS: ...
  5. 外壳注入: !`command` or ```! 封锁(仅限本地技能 — MCP 技能被封锁)
  6. 特殊变量: ${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID}

Execute

技能以以下两种模式之一运行: context: fork 在前面的内容中:

  • 内联(默认): 扩展的提示作为用户消息注入到当前对话中。 克劳德在同一上下文窗口中处理它。
  • Forked: 该技能在隔离的子代理中运行(runAgent())有自己的代币预算。 父对话仅接收最终的文本输出。 独立任务的理想选择。

Inject

技能工具返回一个 ToolResult。 对于内联技能:结果携带 allowedTools 和一个可选的 model 覆盖适用于本轮后续工具调用的内容。 对于分叉技能: Done 显示署名,并且子代理的输出作为上下文反馈。

3.四种技能来源及优先顺序

技能可以源自四个不同的来源。 当两个来源定义同名技能时, 第一个加载的获胜 (托管>用户>项目>捆绑)。 重复数据删除是通过解析的文件路径进行的,而不是通过名称进行的,因此符号链接的技能可以隐藏真实的技能。

优先级1

管理/政策

managed/.claude/skills/

由 IT 部署的企业控制技能。 可以通过锁定以防止用户覆盖 CLAUDE_CODE_DISABLE_POLICY_SKILLS.

优先级2

用户(个人)

~/.claude/skills/

您的跨项目个人技能库。 在运行 Claude Code 的任何地方都可用。 通过 chokidar 观看实时变化。

优先级3

Project

.claude/skills/

存储库范围的技能已签入存储库。 Claude从项目根往上走,这样嵌套 .claude/skills/ 当文件打开时,目录会被动态发现。

优先级4

Bundled

编译成 CLI

技能如 /simplify, /loop, /remember 与二进制文件一起发布。 注册通过 registerBundledSkill() 在启动时。 有些带有功能标记。

最高优先级 Managed User Project Bundled 最低优先级

第五个来源:MCP 技能

当 MCP 服务器暴露技能时(由 loadedFrom === 'mcp'),它们在连接时获取并添加到命令注册表中。 MCP 技能遵循相同的 frontmatter 模式,但是 shell 注入被阻止!backtick and ```! 永远不会对远程、不受信任的内容执行块。

Legacy: .claude/commands/

年龄较大的 /commands/ 仍然支持目录(loadedFrom: 'commands_DEPRECATED')。 它接受单 .md 文件和 skill-name/SKILL.md 目录格式。 新作品应该使用 .claude/skills/.

实时重新加载

The skillChangeDetector 模块使用 chokidar 监视技能目录。 当有任何 SKILL.md 变化、去抖动(300 毫秒)、触发 ConfigChange 挂钩,然后清除所有记忆缓存,以便下次调用看到新技能。 在 Bun 上,使用统计轮询代替 FSWatcher 以避免已知的 Bun 死锁错误。

4. SKILL.md 格式和 Frontmatter

技能文件是纯 Markdown,带有可选的 YAML frontmatter 块,由 ---。 该文件必须位于 <skill-name>/SKILL.md (目录名称成为斜杠命令名称)。

--- name: 《我的工作流程》 # 显示名称(覆盖目录名称) description: 《一行总结》 # 显示在/技能菜单+预算列表中 when_to_use: “当 X 时使用。示例:...” # 克劳德的自动调用提示 allowed-tools: # 最低权限(附加) - Bash(gh:*) - Read - Write argument-hint: “[分支][消息]” # 显示在自动完成用户界面中 arguments: 分支消息 # 命名参数 → $branch, $message context: fork # 'fork' = 独立的子代理; 省略内联 model: claude-opus-4-5 # 仅覆盖此技能的模型 effort: high # 低 | 中等| 高| 整数 version: "1.0.0" # 任意版本字符串(信息性) user-invocable: true # false → 从/技能菜单中隐藏 paths: src/payments/** # 有条件:仅对匹配文件有效 hooks: # 前/后生命周期挂钩 PreToolUse: - 匹配器:... agent: code # 分叉执行的代理类型 shell: # !backtick 注入的 Shell 配置 interpreter: bash --- # 我的工作流程正文。 使用 $branch and $message 用于命名 arg 替换。 或者 $ARGUMENTS 对于原始参数字符串。 或内联外壳: !`git log --oneline -5`

现场参考

FieldTypeDescription
namestring覆盖源自目录的显示名称
descriptionstring技能列表中显示一行摘要。 回到正文的第一段。
when_to_usestring克劳德的详细触发说明。 结合清单中的描述。
allowed-toolslist在此技能期间授予的工具权限模式。 使用最窄的模式: Bash(gh:*) not Bash.
argument-hintstring在 CLI 自动完成中显示为占位符提示。
arguments字符串或列表命名参数标识符。 按位置映射到 $name 体内的替代品。
contextfork作为独立的子代理运行技能。 省略内联(默认)。
modelstring执行此技能时使用的模型别名。 inherit 表示使用会话默认值。
effortlow/medium/high/int运行此技能时所应用的思考预算。
versionstring信息版本标签; 无运行时效果。
user-invocableboolDefault true。 放 false 躲避 /skills 菜单(仅限代理技能)。
paths全局字符串有条件激活——技能仅在打开匹配文件时出现。
hooksobject工具使用前/后生命周期挂钩。 在加载时使用 HooksSchema 进行验证。
agentstring分叉时使用的代理类型标识符。 例如。 code, browser.
shellobjectShell 解释器配置 !backtick 技能体内的执行。
disable-model-invocationboolIf true,该技能无法通过技能工具调用(只能通过斜杠命令)。

5. 高级技能模式

条件技能(基于路径的激活)

一项技能具有 paths frontmatter 场是 条件技能。 它在启动时加载,但是 not 直到用户打开或编辑路径与 glob 模式之一匹配的文件时,Claude 才会发现这一点。

这使您可以保持特定于支付、特定于基础设施或特定于 iOS 的工作流程不可见,直到它们真正相关为止,从而避免在不相关工作的技能列表中出现噪音。

---
paths: src/payments/**
description: "Stripe refund workflow"
---

# Stripe Refund
Steps to issue a refund via the Stripe API...

模式使用与以下相同的语法 .gitignore /CLAUDE.md 条件规则。 一个图案 ** (匹配所有)被视为 unconditional (与省略相同 paths).

条件技能也与 动态技能发现:当模型在项目树中更深入地读取文件时,Claude Code 会向上走 cwd 并且可能会发现更多 .claude/skills/ 启动时未找到目录。 跳过 Gitignored 目录。

Bundled 技能(编译到 CLI 中)

Bundled 技能包含在 Claude Code 二进制文件中,并在启动时通过以下方式注册 registerBundledSkill()。 他们遵循相同的 BundledSkillDefinition 界面作为基于文件的技能,但提供 getPromptForCommand(args, context) 函数而不是 SKILL.md 文件。

众所周知的捆绑技能包括:

  • /simplify — 产生三个并行审查代理(重用、质量、效率)
  • /loop — 解析间隔 + 提示并创建 cron 作业(带有功能标记)
  • /remember, /verify, /debug, /stuck
  • /skillify — 采访您有关当前会话的信息并编写 SKILL.md(Anthropic-内部)

Bundled 技能还可以通过以下方式包含参考文件 files 财产。 这些在第一次调用时被提取到每个进程的随机数目录中(使用 O_EXCL | O_NOFOLLOW | 0o600 防止符号链接攻击的标志),以及 Base directory for this skill: ... 前缀添加到提示符之前,以便 Claude 可以读取/Grep 它们。

一些捆绑技能以功能标志为条件(feature('AGENT_TRIGGERS'), feature('KAIROS'))或运行时检查(isKairosCronEnabled())。 无需修改磁盘上的 SKILL.md 即可添加或删除它们。

// Registering a bundled skill
registerBundledSkill({
  name: 'simplify',
  description: 'Review changed code for reuse, quality, and efficiency.',
  userInvocable: true,
  async getPromptForCommand(args) {
    return [{ type: 'text', text: SIMPLIFY_PROMPT }]
  },
})
MCP 技能(通过模型上下文协议提供)

MCP 服务器除了工具之外还可以公开技能。 当 Claude Code 使用以下命令连接到 MCP 服务器时 MCP_SKILLS 功能启用,它调用 fetchMcpSkillsForClient(),它使用相同的方法解析技能的 frontmatter parseSkillFrontmatterFields() and createSkillCommand() 用于基于文件的技能的函数。

MCP 技能出现在 SkillsMenu 在他们自己的“MCP 技能”组下。 他们的名字遵循惯例 server-name:skill-name.

与本地技能的主要区别:

  • 无外壳注入: !backtick and ```! 块被默默地跳过。 代码注释说: “安全性:MCP 技能是远程且不受信任的 - 切勿从其 Markdown 主体执行内联 shell 命令。”
  • ${CLAUDE_SKILL_DIR} 没有意义: MCP 技能没有本地目录,因此不替换该变量。
  • 单独注册:MCP 技能存在于 AppState.mcp.commands,而不是本地技能注册表。 技能工具通过以下方式合并它们 getAllCommands() 在调用时。
  • 注册表桥: mcpSkillBuilders.ts 是一个依赖图叶模块,包含对 createSkillCommand and parseSkillFrontmatterFields,解决之间的循环导入问题 client.ts → mcpSkills.ts → loadSkillsDir.ts.
// How getAllCommands() merges MCP and local skills
const mcpSkills = context.getAppState().mcp.commands
  .filter(cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp')
const localCommands = await getCommands(getProjectRoot())
return uniqBy([...localCommands, ...mcpSkills], 'name')
技能许可和自动允许逻辑

每次技能工具调用都会经过 checkPermissions()。 该决定遵循以下瀑布:

  1. 拒绝规则:如果一个 deny 规则与技能名称(或带有前缀 :*),立即封锁。
  2. 远程规范技能:自动允许(仅限蚂蚁实验功能)。
  3. 允许规则: 如果明确 allow 规则匹配,无需询问即可继续。
  4. 安全属性检查:如果技能仅使用“安全”属性(没有允许的工具、没有模型覆盖、没有钩子、没有路径等),则它会自动允许,而不提示用户。
  5. Ask:否则,系统会提示用户添加精确或前缀的建议(:*) 允许本地设置规则。

这意味着简单的信息技能无需任何权限对话框即可运行,而获得 Bash 访问权限或覆盖模型的技能必须得到明确批准。

深度参数替换

参数解析使用 shell-quote 因此带引号的字符串可以用作单个标记:

/myskill "hello world" foo  →  ["hello world", "foo"]

支持三种替换模式,按顺序处理:

Pattern它决心做什么
$foo, $bar按位置命名 arg(需要 arguments: foo bar frontmatter)
$ARGUMENTS[0], $0索引位置 arg
$ARGUMENTS完整的原始参数字符串

如果技能体包含 no 占位符和用户提供的参数,它们会自动附加: ARGUMENTS: <args>。 这可以防止在未声明占位符的简单技能中悄悄删除参数。

6. 要点

  • 📁 技能住在 <skill-name>/SKILL.md。 目录名称是斜杠命令。 YAML frontmatter 控制所有元数据; Markdown 主体是提示。
  • 🔍 克劳德从四个来源发现技能,按优先顺序排列: 托管→用户→项目→捆绑。 MCP 技能在调用时单独合并。
  • 技能跑 inline (默认,相同的对话上下文)或 forked (context: fork,孤立的子代理)。 Fork 更适合独立任务; 当您需要中间过程转向时,内联会更好。
  • 🔒 MCP 技能 从不执行 shell 注入。 仅本地(托管/用户/项目/捆绑)技能可以运行 !backtick 块。 这是源代码中的硬安全边界。
  • 🎯 paths frontmatter 是一种技能 conditional — 在打开匹配文件之前不可见。 仅当相关时才使用它来显示特定于域的工作流程。
  • 💡 技能列表的预算上限为 上下文窗口的 1%。 Bundled 技能描述永远不会被截断; 如果列表超出预算,自定义技能描述可能会缩短。
  • 🔄 技能文件是 观看了直播。 保存 SKILL.md 会触发去抖重新加载(300 毫秒)。 无需重新启动。
  • 简单的技能(没有允许的工具,没有模型覆盖) auto-approved 没有任何权限提示。 更强大的技能需要明确的用户允许规则。

知识检查

Q1. 如果同名技能同时出现在用户技能目录和项目技能目录里,默认哪一个会生效?
正确! 技能来源有明确优先顺序:托管 > 用户 > 项目 > Bundled。也就是说,用户级技能会盖住项目里同名技能。
Q2. 如果你希望技能在隔离上下文里作为子代理运行,应该使用哪个 frontmatter 字段?
正确! context: fork 会让技能在独立子代理中运行,而不是把展开后的提示直接塞进当前对话。
Q3. 为什么 MCP 技能里的 !`...````! shell 片段不会真正执行?
正确! 这是安全边界,不是能力缺失。Claude Code 明确禁止远程 MCP 技能触发本地 shell 注入,避免把不受信任文本变成本机执行。
Q4. paths frontmatter 的主要作用是什么?
正确! paths 不是注释信息,而是可见性门。只有当当前打开或操作的文件命中这些模式时,这个技能才会进入可发现集合。
Q5. 如果技能正文里没有任何 $ARGUMENTS 占位符,但用户仍然传了参数,系统会怎样处理?
正确! 这是技能系统的一层容错设计。即便作者没写占位符,用户传入的参数也不会被静默吞掉,而是会附加到最终提示里。
0/5