用 Claude Code 有段时间了,一直有个好奇:Claude Code 的 Skills 到底是怎么工作的?翻了一遍源码,发现背后有一套完整的机制——从磁盘上发现 SKILL.md 文件,到最终把指令塞进模型上下文,中间经历了整整 8 个层次。
苏米注:这篇是源码层面的完整梳理,适合在用 Claude Code 的同学、想自己写 Skill 的朋友,或者只是对「AI 工具内部怎么运转」感兴趣的人。
整个 Skill 系统由 8 个层组成:发现 → 注册 → 系统提示 → 调用 → 内容加载 → Hook 注册 → 持久化 → 注入
1. 发现层(Discovery)
Skills 从多个来源被发现:
~/.claude/skills/— 用户级 skills./.claude/skills/— 项目级 skillssrc/skills/bundledSkills.ts— 内置 skills(commit、review 等)- Plugins — 插件 skills
目录结构要求:/skills/ 目录只支持 skill-name/SKILL.md 格式,旧版 /commands/ 同时支持目录格式和单 .md 文件。多来源通过 realpath() 去重,可跟踪符号链接。
条件 Skills(Path-Filtered)
Skills 可通过 frontmatter 的 paths: 字段实现条件激活,使用 gitignore 风格匹配,只在操作匹配文件时激活,一旦激活整个会话可用。
动态发现
文件操作(Read/Write/Edit)时自动触发动态发现:从文件所在目录向上遍历至 CWD,在每一级检查 .claude/skills/,按深度排序后加载,模型在下一轮对话中即可看到新 skills。
2. 注册层(Registration)— 惰性加载
注册时只解析 frontmatter 元数据,不加载 SKILL.md 正文内容。
Command 对象包含:name、description、allowedTools(元数据,加载时解析)以及 getPromptForCommand()(惰性加载器,调用时才读取全文)。
来源优先级:Bundled skills → 内置插件 skills → 目录 skills → Workflow → 插件 → 内置命令。
3. 系统提示注入 — 仅元数据
系统提示中只注入名称 + 描述,预算为上下文窗口的 1%(约 8000 字符),描述截断至最多 250 字符,内置 skills 不截断。
苏米注:完整内容仅在 skill 被调用时才按需加载,避免浪费上下文窗口。这是典型的惰性加载设计。
4. 调用层 — 两条路径
路径 A:用户直接输入 /skill
解析斜杠命令 → 查找 Command 对象 → 检查 context 字段 → "fork" 则在子 agent 中运行(隔离预算),否则内联处理。
路径 B:模型通过 Skill Tool 调用
模型生成 tool_use → 验证 + 权限检查 → SkillTool.call() → 返回 newMessages(包含 skill 内容)+ contextModifier(提升工具权限)→ 注入对话。
5. 内容加载层 — 按需加载 + 变量替换
内容转换管线:
- 解析 frontmatter,提取 markdown body
- 添加
Base directory:前缀 - 替换
CLAUDE_SKILL_DIR变量 → 实际 skill 目录路径 - 替换
CLAUDE_SESSION_ID变量 → 当前会话 ID - 执行内联 shell 命令(感叹号 + 反引号包裹),在 allowedTools 权限下运行
- 替换参数变量 → 用户提供的参数值
安全说明:MCP skills 永不执行内联 shell 命令;Base directory 帮助解析相对路径引用。
6. 内容如何进入模型上下文
完整消息流:
- System Prompt:包含 skill 列表元数据
- 对话历史
- 用户消息:"帮我提交代码"
- Assistant 调用 Skill tool
- Tool Result:success
- 注入 UserMessage:"Run /commit -m 'Fix'"
- 注入 UserMessage:SKILL.md 全部内容
模型看到完整 skill 内容,生成响应,同时拥有提升后的工具权限。
核心设计:完整内容作为 UserMessage 注入,而非放在系统提示,成为模型的直接输入。
7. 持久化层 — 压缩存活
每次 skill 被调用后,内容存入 STATE.invokedSkills Map(key 为 "agentId:skillName")。
压缩(compact)触发时,buildPostCompactMessages() 从 invokedSkills 重建 skill 内容并重新注入,确保 skill 指令在长对话压缩中不会丢失。
8. Hook 系统
Skills 可在 frontmatter 中声明 hooks,支持以下事件类型:
on_file_read— 文件即将被读取on_file_write— 文件即将被写入on_tool_call— 工具即将被调用on_message_submit— 用户消息即将发送
每个 hook 支持 patterns(gitignore 风格)和 commands 配置,匹配时自动执行对应命令。
核心设计思想
| 设计原则 | 说明 |
|---|---|
| 惰性加载 | 注册时只解析元数据,调用时才读 SKILL.md 全文,节省启动时间和内存 |
| 双路径调用 | 用户直接 /skill 或模型通过 Skill tool,统一汇入相同的内容加载管线 |
| 系统提示只放元数据 | 完整内容作为 UserMessage 按需注入,避免浪费上下文窗口 |
| 预算控制 | skill 列表限制在上下文窗口的 1%,描述超长时截断 |
| 压缩存活 | invokedSkills Map 确保长对话压缩后 skill 指令不丢失 |
| 动态发现 | 文件操作时自动沿目录树发现新 skills,无需重启会话 |
| 安全模型 | allowed-tools 限制 skill 权限,MCP skills 不执行 shell |
| 缓存优化 | getSkillDirCommands() 按 cwd 缓存,动态发现时清除缓存重建 |
苏米注:翻完这 8 层代码,有几个设计细节让人印象深刻——系统提示里只放名字和描述,完整内容等真正调用时才加载,不是偷懒,是在省上下文窗口,每个 token 都有成本。压缩存活机制(invokedSkills)解决了一个实际问题:对话太长被压缩后,模型不会「忘掉」自己在执行什么 Skill,会自动重新注入。动态发现也挺实用——读文件时沿目录往上找 .claude/skills/,不用重启就能加载新 Skill,天然适合团队把规范沉淀进项目里。