深入分析 Claude Code CLI 如何发现、加载和激活 skills。基于源代码 package/cli.js 的实现研究。
- Skills 发现(Discovery Phase)
- Skills 加载(Load Phase)
- Frontmatter 解析
- 重复处理(Deduplication)
- Skills 激活(Activation)
- 优先级和覆盖规则
- 实战示例
- 关键代码位置
Claude Code 启动时扫描三个位置的 skills,按优先级从高到低:
# 优先级:
1. Managed Settings → /Library/Application Support/ClaudeCode/skills/
2. User Settings → ~/.claude/skills/
3. Project Settings → .claude/skills/ (项目根目录)关键代码 (源代码第 256530 行):
let K = S4A(T8(), "skills"), // managed
q = S4A(Wf(), ".claude", "skills"), // user
Y = U86("skills", A); // project
let [z, w, H] = await Promise.all([
fw1(q, "policySettings"), // 扫描 managed
w$("userSettings") ? fw1(K, "userSettings") : ...,
w$("projectSettings") ? ... fw1(Z, "projectSettings") : ...
])┌─────────────────────────────────────────┐
│ 1. 扫描三个 skills 目录 │
│ - Managed (系统级) │
│ - User (~/.claude/skills/) │
│ - Project (.claude/skills/) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. 对每个目录执行 fw1() 函数 │
│ - 列出所有子目录 │
│ - 查找 SKILL.md 文件 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. 并行加载所有 skills │
│ (Promise.all) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. 合并并去重 │
│ - 检查 inode 避免重复 │
│ - 应用优先级规则 │
└─────────────────────────────────────────┘
fw1 函数(源代码第 256290 行)负责从目录加载 skills:
async function fw1(A, K) {
let q = hA(),
Y = [];
try {
let z = q.readdirSync(A); // 读取目录
for (let w of z) {
if (w.isDirectory() || w.isSymbolicLink()) {
let H = S4A(A, w.name),
J = S4A(H, "SKILL.md"); // 查找 SKILL.md
try {
let X = q.readFileSync(J, {encoding: "utf-8"}),
{frontmatter: O, content: $} = _$(X); // 解析 frontmatter{
skill: {
// 基本信息
skillName: "_", // 目录名称(技术名)
displayName: O.name, // frontmatter 中的 name(展示名)
description: Z, // frontmatter 中的 description
markdownContent: $, // SKILL.md 的主体内容
// 权限和工具
allowedTools: G, // allowed-tools 列表
argumentHint: O["argument-hint"],
// 配置参数
whenToUse: O.when_to_use,
version: O.version,
model: M, // 可指定 LLM 模型
userInvocable: W, // 用户是否可直接调用
disableModelInvocation: D, // Claude 是否可自动触发
// 执行上下文
source: K, // 来源:policySettings/userSettings/projectSettings
baseDir: H,
loadedFrom: "skills",
hooks: j, // hook 配置
executionContext: P, // fork 或继承
agent: V // 指定的 agent 类型
},
filePath: J // SKILL.md 文件路径
}- 读取目录 →
readdirSync(A) - 查找 SKILL.md →
S4A(H, "SKILL.md") - 读取文件 →
readFileSync(J, {encoding: "utf-8"}) - 解析 YAML →
_$(X)分离 frontmatter 和 content - 构建对象 → 包含所有配置的 skill 对象
SKILL.md 中的 YAML frontmatter 被解析为配置:
---
# 基本信息(必需)
name: analyze-claude-code
description: 分析 Claude Code CLI 源码
# 功能配置(可选)
allowed-tools: [Bash, Read, Grep, Task] # 限制此 skill 可用的工具
user-invocable: true # 允许用户直接调用(/analyze-claude-code)
disable-model-invocation: false # Claude 可自动检测并触发此 skill
model: inherit # 继承当前模型,或指定:gpt-4, claude-opus 等
context: inherit # fork 或 inherit(继承当前上下文)
# 文档信息(可选)
when_to_use: |
当用户想要分析 Claude Code 内部实现时触发
version: 1.0.0
author: Kai Chen
# 高级配置(可选)
agent: general-purpose # 使用哪个 agent 执行
argument-hint: "[source_code_path]" # 参数提示
hooks: # Hook 配置
- event: SessionStart
path: hooks/check-version.sh
---
# Markdown 内容从这里开始
[Skill 的执行指令]let {
frontmatter: O, // YAML 配置对象
content: $ // Markdown 主体内容
} = _$(X); // _$ 是 YAML 解析函数
// 从 frontmatter 提取值
let displayName = O.name;
let description = O.description ?? bg($, "Skill"); // 若无 name,从 Markdown 提取
let allowedTools = wR(O["allowed-tools"]);
let userInvocable = O["user-invocable"] === void 0 ? true : Tw1(O["user-invocable"]);
let disableModelInv = Tw1(O["disable-model-invocation"]);如果不同位置有相同的 skill,通过 inode 判断是否为同一文件(源代码第 256546 行):
let O = new Map,
$ = [];
for (let { skill: Z, filePath: G } of X) {
let W = C9Y(G); // 获取文件的 inode
if (W === null) {
$.push(Z);
continue;
}
let D = O.get(W);
if (D !== void 0) {
// 已加载过此 inode,跳过
u(`Skipping duplicate skill '${Z.name}' from ${Z.source} (same inode already loaded from ${D})`);
continue;
}
O.set(W, Z.source);
$.push(Z);
}- 同一文件的符号链接 → 检测到重复,使用已加载版本
- 不同位置的副本 → 根据优先级,较低优先级的被跳过
- 示例:
~/.claude/skills/my-skill/SKILL.md(inode: 123456).claude/skills/my-skill/SKILL.md(inode: 123456 - 符号链接)- → 只加载一次,使用更高优先级的版本
当用户输入触发某个 skill 时的激活流程:
if (!D) { // disable-model-invocation === false
// Claude 可自动检测用户输入与 skill 的 description/when_to_use 的关联
// 自动加载并注入 skill 内容到系统提示
}条件:disable-model-invocation: false 或未指定(默认 false)
if (W) { // user-invocable === true
// 用户可通过 /skill-name 直接调用
}命令格式:/analyze-claude-code 或 /plugin-name:skill-name
当 skill 被激活时,markdownContent 被注入到当前消息中(源代码第 326808 行):
{
skillContent: z, // = skill.markdownContent
modifiedGetAppState: H,
baseAgent: O,
promptMessages: [
J6({ content: z }) // 内容作为消息内容
]
}// 根据 executionContext 选择执行方式
let P = O.context === "fork" ? "fork" : void 0;
if (P === "fork") {
// 独立的 agent 执行,上下文隔离
// 有独立的:
// - getAppState(获取应用状态)
// - readFileState(文件读取状态)
// - toolPermissionContext(工具权限)
} else {
// 在当前 agent 中执行,继承所有上下文
}用户输入
↓
┌─────────────────────────────────────┐
│ 1. 匹配 Skill │
│ - 手动:/skill-name │
│ - 自动:匹配 description/when_to_use
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 2. 检查权限 │
│ - allowed-tools │
│ - model(若有指定) │
│ - hooks(触发前置 hook) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 3. 注入内容 │
│ - 将 markdownContent 加入消息 │
│ - 设置 agent(若指定) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 4. 执行 Skill │
│ - fork:独立上下文 │
│ - inherit:共享上下文 │
└─────────────────────────────────────┘
↓
完成
| 优先级 | 来源 | 路径 | 说明 |
|---|---|---|---|
| 1️⃣ Highest | Managed | /Library/Application Support/ClaudeCode/skills/ |
IT 部门或系统管理员设置,强制适用 |
| 2️⃣ Medium | User | ~/.claude/skills/ |
用户全局 skills |
| 3️⃣ Lowest | Project | .claude/skills/ |
项目级 skills,同名会覆盖前两级 |
情景 1:三个位置都有 my-skill/SKILL.md
├─ /Library/Application Support/ClaudeCode/skills/my-skill/ (优先级最高)
├─ ~/.claude/skills/my-skill/
└─ .claude/skills/my-skill/
结果:使用 /Library/Application Support 版本,其他两个被跳过
情景 2:项目和用户级都有 my-skill,但 inode 相同(符号链接)
├─ ~/.claude/skills/my-skill/ (inode: 123456)
└─ .claude/skills/my-skill/ → 符号链接到 ~/.claude/skills/my-skill/ (inode: 123456)
结果:只加载一次,inode 重复检测会跳过符号链接版本
1. Claude Code 启动
↓
2. 扫描 ~/.claude/skills/ 和 .claude/skills/
↓
3. 发现 plugins/analyze-claude-code/skills/analyze-claude-code/SKILL.md
↓
4. 读取并解析 frontmatter:
{
name: "analyze-claude-code",
description: "Analyze Claude Code CLI source code...",
user-invocable: true,
disable-model-invocation: false // ← Claude 可自动触发
}
↓
5. 将 SKILL.md 的完整内容加载到内存
↓
6. 当用户提到 Claude Code 内部实现时:
a. Claude 自动检测到匹配 skill 的 description
b. 激活 analyze-claude-code
c. 将 SKILL.md 内容注入到系统提示
d. 现在 Claude 可以:
- 使用 Bash、Read、Grep、Task 工具
- 参考 SKILL.md 中的指导(搜索模式、代码位置等)
- 理解如何提取提示、工具定义等
↓
7. 如果你添加了 WebFetch 指导:
Claude 可以在分析时使用 WebFetch 访问官方文档
例如:https://code.claude.com/docs/llms.txt
## Official Documentation
**Access official docs via WebFetch tool:**
...
- URL: `https://code.claude.com/docs/llms.txt`加载后:
1. SKILL.md 的这一部分被加载到 markdownContent
↓
2. 当 skill 激活时,整个 markdownContent(包括此部分)被注入
↓
3. Claude 现在在系统提示中看到:
"Use the WebFetch tool to query Claude Code's official documentation..."
↓
4. Claude 可以主动使用 WebFetch 工具来查询官方文档
↓
5. 这改进了分析质量,因为 Claude 可以交叉参考源代码和官方文档
| 功能 | 行号 | 函数/操作 | 说明 |
|---|---|---|---|
| Skills 路径扫描 | 256530 | fw1() 调用 |
扫描三个 skills 目录 |
| Skills 加载 | 256290 | async function fw1(A, K) |
从目录加载单个 skill |
| Frontmatter 解析 | 256309 | _$(X) |
YAML frontmatter 解析 |
| Skill 对象构建 | 256318-256338 | G37({...}) |
构建 skill 配置对象 |
| 重复检测 | 256546 | inode 比对 | 去重逻辑 |
| 内容注入 | 326808 | skillContent: z |
将 skill 内容加入消息 |
# 在 Claude Code 源代码中查找 skills 相关代码
grep -n "SKILL\.md\|fw1\|loadSkill" package/cli.js
# 查找 skill 的应用点
grep -n "skillContent\|markdownContent" package/cli.js
# 查找 skill 的触发条件
grep -n "userInvocable\|disable-model-invocation" package/cli.jsClaude Code 使用渐进式加载策略:
-
启动时 → 只加载 skill 的元数据(~100 tokens)
- name
- description
- allowed-tools
- 其他 frontmatter 配置
-
触发时 → 加载完整内容(<5k tokens)
- 完整的 Markdown 内容注入到系统提示
- Claude 可以参考 skill 中的所有指导和示例
这样既保持了启动速度,又在需要时获得完整的上下文。
作者:Kai Chen 日期:2026-01-23 基于:Claude Code CLI 源代码分析