Published on

Claude Code 技能系统源码深度解析

Authors

引言

Claude Code 是 Anthropic 推出的终端 AI 编程助手,其核心设计理念之一是将复杂能力封装为可复用的"技能"(Skills)。技能系统让 Claude 能够按需加载领域知识,而非在每次对话中塞入所有可能的指令。

本文基于 Claude Code 源码,深入剖析其技能系统的完整架构——从定义与发现、格式规范、工具实现,到热加载与 Hook 交互。

1. 技能的定义与发现机制

Claude Code 的技能来自多个来源,按优先级排列:

1.1 来源层级

Managed(策略管理)→ User(~/.claude/skills)→ Project(.claude/skills)→ Plugin → Bundled → MCP

加载逻辑位于 loadSkillsDir.tsgetSkillDirCommands() 函数。启动时,系统并行扫描以下目录:

  • Managed 目录:策略下发的技能,路径为 managedFilePath/.claude/skills
  • 用户目录~/.claude/skills,存放用户全局技能
  • 项目目录.claude/skills,支持从当前目录向上遍历到 home 目录
  • 额外目录:通过 --add-dir 指定的附加路径
  • Legacy commands:兼容旧版 .claude/commands/ 目录

1.2 目录格式

技能仅支持目录格式:skill-name/SKILL.md。每个技能是一个子目录,核心文件为 SKILL.md

.claude/skills/
├── commit/
│   └── SKILL.md
├── review-pr/
│   └── SKILL.md
└── pdf/
    └── SKILL.md

加载时,系统通过 realpath 解析符号链接实现去重,确保同一文件不会重复注册。

1.3 动态发现

除了启动时加载,系统还支持动态技能发现。当用户操作文件时,系统会从文件路径向上遍历,查找子目录中的 .claude/skills

// discoverSkillDirsForPaths: 从文件路径向上发现技能目录
async function discoverSkillDirsForPaths(filePaths: string[], cwd: string) {
  for (const filePath of filePaths) {
    let currentDir = dirname(filePath)
    while (currentDir.startsWith(resolvedCwd + pathSep)) {
      const skillDir = join(currentDir, '.claude', 'skills')
      // 检查并加载...
      currentDir = dirname(currentDir)
    }
  }
}

这意味着深层子目录中可以放置局部技能,只在操作该目录下的文件时才激活——一个精巧的按需加载策略。

1.4 条件技能

技能可以通过 paths frontmatter 字段声明路径匹配规则(使用 gitignore 风格),只有当用户操作的文件匹配这些路径时才激活:

---
paths:
  - 'src/**/*.ts'
---

系统使用 ignore 库进行匹配,条件技能在文件操作时按需激活,不会在启动时占用上下文窗口。

2. SKILL.md 格式规范

每个技能的核心是 SKILL.md 文件,由 YAML frontmatter 和 Markdown 正文组成。

2.1 Frontmatter 字段

解析逻辑位于 parseSkillFrontmatterFields(),支持以下字段:

字段类型说明
descriptionstring技能描述,用于发现和匹配
when_to_usestring详细的触发条件描述
allowed-toolsstring[]技能允许使用的工具白名单
argumentsstring|string[]参数定义
argument-hintstring参数提示文本
modelstring模型覆盖(如 opus
effortstring思考强度(如 high
context'fork'执行上下文(fork 模式在子代理中运行)
agentstring代理类型覆盖
user-invocableboolean用户是否可通过 /skill-name 调用
disable-model-invocationboolean禁止模型自动调用
hooksobject技能级别的 Hook 定义
pathsstring[]条件激活的路径匹配规则
shellobjectShell 执行配置

2.2 正文内容

Markdown 正文是技能的实际指令,会被注入到对话上下文中。支持以下特殊功能:

  • Shell 注入!command 语法可以在加载时执行 shell 命令并插入结果
  • 变量替换${CLAUDE_SKILL_DIR}(技能目录)和 ${CLAUDE_SESSION_ID}(会话 ID)
  • 参数替换${ARGUMENTS} 会被用户传入的参数替换

2.3 示例

---
description: '创建规范的 git commit'
when_to_use: '用户要求提交代码时使用'
allowed-tools: ['Read', 'Write', 'Bash']
---

# Git Commit

请按照以下步骤创建 commit:

1. 运行 `git status``git diff --staged` 查看变更
2. 分析变更内容,生成符合 Conventional Commits 规范的 commit message
3. 执行 `git commit -m "<message>"`

3. SkillTool 的实现

SkillTool 是 Claude Code 的核心工具之一,允许模型调用技能。实现位于 src/tools/SkillTool/SkillTool.ts

3.1 工具接口

export const SkillTool = buildTool({
  name: 'Skill',
  inputSchema: z.object({
    skill: z.string().describe('The skill name'),
    args: z.string().optional().describe('Optional arguments'),
  }),
  // ...
})

工具接收技能名和可选参数,返回执行结果。

3.2 执行模式

技能支持两种执行模式:

Inline 模式(默认):技能的 prompt 被注入当前对话上下文,Claude 在同一会话中执行技能指令。返回新的消息和上下文修改器:

return {
  data: { success: true, commandName, allowedTools, model },
  newMessages, // 注入到对话中的新消息
  contextModifier(ctx) {
    // 修改工具权限、模型、effort 等
  },
}

Fork 模式:技能在独立的子代理中执行,拥有独立的 token 预算:

if (command.context === 'fork') {
  return executeForkedSkill(command, commandName, args, context, ...)
}

子代理通过 runAgent() 运行,支持进度报告和结果提取。

3.3 权限控制

SkillTool 实现了细粒度的权限系统:

  1. deny 规则检查:匹配的技能直接拒绝
  2. allow 规则检查:匹配的技能自动放行
  3. 安全属性白名单:如果技能只包含"安全"属性(如 descriptionmodeleffort),自动放行
  4. 默认行为:需要用户确认,并提供建议规则

安全属性白名单是一个精心设计的集合——如果技能有任何不在白名单中的属性(如 allowedToolshooks),就需要用户授权。这确保了新添加的属性默认需要权限确认,遵循最小权限原则。

3.4 Prompt 构建

技能的 prompt 生成支持 token 预算控制。系统为技能列表分配上下文窗口的 1%,内置技能的描述不会被截断,而非内置技能的描述会按预算截断:

export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01
export const MAX_LISTING_DESC_CHARS = 250

4. 内置技能(Bundled Skills)

内置技能随 CLI 二进制一起分发,通过 registerBundledSkill() 注册。位于 src/skills/bundled/

4.1 注册机制

// bundledSkills.ts
const bundledSkills: Command[] = []

export function registerBundledSkill(definition: BundledSkillDefinition): void {
  // 处理 files 字段的懒提取
  if (files && Object.keys(files).length > 0) {
    let extractionPromise: Promise<string | null> | undefined
    getPromptForCommand = async (args, ctx) => {
      extractionPromise ??= extractBundledSkillFiles(name, files)
      // ...
    }
  }
  bundledSkills.push(command)
}

4.2 文件提取

内置技能可以携带参考文件(如脚本、配置模板),在首次调用时提取到磁盘。这是一个精心设计的安全机制:

  • 使用进程级 nonce 目录防止符号链接攻击
  • 文件权限设为 0o700(目录)和 0o600(文件)
  • 使用 O_NOFOLLOW | O_EXCL 标志防止竞态条件
  • 路径规范化防止目录穿越
const SAFE_WRITE_FLAGS =
  process.platform === 'win32'
    ? 'wx'
    : fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | O_NOFOLLOW

4.3 技能示例

当前注册的内置技能包括:

技能名功能
remember审查自动记忆条目,提议提升到 CLAUDE.md
verify验证代码变更是否正常工作
debug调试辅助
skillify将当前工作流转化为可复用技能
simplify简化代码
batch批量操作
stuck当模型卡住时的恢复策略
updateConfig更新配置

部分技能受 Feature Flag 控制(如 loopclaudeApi),通过 feature() 动态加载。

5. 技能与 Hook 的交互

5.1 技能级 Hook

技能可以在 frontmatter 中定义 Hook,与全局 Hook 系统集成:

---
hooks:
  PreToolUse:
    - matcher: Write
      hooks:
        - type: command
          command: "echo 'About to write file'"
---

Hook 定义通过 Zod schema 验证(HooksSchema),与全局 Hook 使用相同的类型定义。

5.2 配置变更 Hook

技能文件的变更会触发 ConfigChange Hook。在 skillChangeDetector.ts 中:

const results = await executeConfigChangeHooks('skills', changedPath)
if (hasBlockingResult(results)) {
  return // Hook 阻止了重载
}

这意味着外部系统可以在技能文件变更时执行自定义逻辑,甚至阻止技能重载。

5.3 GrowthBook 集成

技能的 isEnabled() 回调可以读取 GrowthBook Feature Flag,决定技能是否对当前用户可见。useSkillsChange Hook 监听 GrowthBook 刷新事件,只清除 memoization 缓存(不触发磁盘重扫),确保 Feature Flag 变更能实时生效。

6. 技能热加载机制

热加载是技能系统最精巧的部分之一,由 skillChangeDetector.ts 实现。

6.1 文件监听

系统使用 chokidar 监听技能目录变化,但有一个重要的平台适配:

const USE_POLLING = typeof Bun !== 'undefined'

由于 Bun 的 fs.watch() 存在 PathWatcherManager 死锁问题,在 Bun 运行时下自动切换为 stat 轮询模式(2 秒间隔)。

6.2 防抖策略

当大量文件同时变更时(如 git 操作),防抖机制将多个事件合并为一次重载:

const RELOAD_DEBOUNCE_MS = 300

function scheduleReload(changedPath: string) {
  pendingChangedPaths.add(changedPath)
  if (reloadTimer) clearTimeout(reloadTimer)
  reloadTimer = setTimeout(async () => {
    // 批量处理所有变更路径
    clearSkillCaches()
    clearCommandsCache()
    skillsChanged.emit()
  }, RELOAD_DEBOUNCE_MS)
}

6.3 信号机制

整个系统基于自定义的 createSignal() 实现发布-订阅:

  • skillsLoaded:动态技能加载完成时触发
  • skillsChanged:文件变更导致技能重载时触发

useSkillsChange React Hook 将这些信号连接到 UI 更新,实现技能列表的实时刷新。

6.4 双触发器设计

useSkillsChange 处理两种不同的刷新场景:

  1. 磁盘变更(watcher):完整缓存清除 + 磁盘重扫
  2. GrowthBook 刷新(Feature Flag):仅清除 memoization 缓存,重新过滤已有技能

这种分离避免了不必要的磁盘 I/O,同时确保 Feature Flag 变更能立即反映。

架构总结

┌─────────────────────────────────────────────────────┐
│                    SkillTool                         │
│  (模型调用技能的入口,权限控制,执行模式选择)          │
├─────────────────────────────────────────────────────┤
│              技能注册表 (Command[])                   │
│  ┌─────────┬──────────┬──────────┬────────┬───────┐ │
│  │ Managed │   User   │ Project  │ Plugin │Bundled│ │
│  └────┬────┴────┬─────┴────┬─────┴───┬────┴───┬───┘ │
│       │         │          │         │        │     │
│  ┌────┴─────────┴──────────┴─────────┴────────┴───┐ │
│  │         loadSkillsDir (发现 + 解析)              │ │
│  │    ┌──────────────┐ ┌──────────────────────┐   │ │
│  │    │动态发现      │ │条件技能(路径匹配)     │   │ │
│  │    └──────────────┘ └──────────────────────┘   │ │
│  └──────────────────────┬──────────────────────────┘ │
│                         │                           │
│  ┌──────────────────────┴──────────────────────────┐ │
│  │         skillChangeDetector (热加载)              │ │
│  │    chokidar/polling → 防抖 → Hook → 信号通知     │ │
│  └─────────────────────────────────────────────────┘ │
│                                                     │
│  ┌─────────────────────────────────────────────────┐ │
│  │         Hook 系统                                │ │
│  │    技能级 Hook / ConfigChange Hook / GB 刷新     │ │
│  └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

Claude Code 的技能系统是一个设计精良的插件架构。它通过多层来源实现策略控制与用户自由的平衡,通过条件激活和动态发现优化 token 使用,通过热加载确保开发体验的流畅性,通过安全机制(权限白名单、文件提取保护)防止滥用。这种"按需加载、最小权限、实时响应"的设计哲学,值得在构建 AI Agent 系统时借鉴。