- Published on
Claude Code 技能系统源码深度解析
- Authors
- Name
- 大聪明
- @wooluoo
引言
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.ts 的 getSkillDirCommands() 函数。启动时,系统并行扫描以下目录:
- 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(),支持以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
description | string | 技能描述,用于发现和匹配 |
when_to_use | string | 详细的触发条件描述 |
allowed-tools | string[] | 技能允许使用的工具白名单 |
arguments | string|string[] | 参数定义 |
argument-hint | string | 参数提示文本 |
model | string | 模型覆盖(如 opus) |
effort | string | 思考强度(如 high) |
context | 'fork' | 执行上下文(fork 模式在子代理中运行) |
agent | string | 代理类型覆盖 |
user-invocable | boolean | 用户是否可通过 /skill-name 调用 |
disable-model-invocation | boolean | 禁止模型自动调用 |
hooks | object | 技能级别的 Hook 定义 |
paths | string[] | 条件激活的路径匹配规则 |
shell | object | Shell 执行配置 |
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 实现了细粒度的权限系统:
- deny 规则检查:匹配的技能直接拒绝
- allow 规则检查:匹配的技能自动放行
- 安全属性白名单:如果技能只包含"安全"属性(如
description、model、effort),自动放行 - 默认行为:需要用户确认,并提供建议规则
安全属性白名单是一个精心设计的集合——如果技能有任何不在白名单中的属性(如 allowedTools、hooks),就需要用户授权。这确保了新添加的属性默认需要权限确认,遵循最小权限原则。
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 控制(如 loop、claudeApi),通过 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 处理两种不同的刷新场景:
- 磁盘变更(watcher):完整缓存清除 + 磁盘重扫
- 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 系统时借鉴。