Files
cs-note/claude-code-best/docs/extensibility/skills.md
T

239 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [Skills, SkillTool, 技能系统, Prompt, Claude-Code]
create time: 2026-06-09 22:30
---
# Skills 技能系统 - Prompt 即能力的架构哲学
## 概述
Claude Code 的 Skills 系统将复杂任务的经验封装为可复用的 Markdown 文件——核心洞见是"复杂任务的关键不在代码逻辑,而在 Prompt 质量"。本文揭示 Skill 从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行(inline/fork)到动态发现的完整生命周期。
## 正文
### Tool vs Skill:本质差异
| | Tool | Skill |
|---|---|---|
| 粒度 | 单个原子操作(读文件、执行命令) | 一套完整的工作流(代码审查、创建 PR) |
| 触发方式 | AI 自主选择 | 用户 `/skill-name` 或 AI 通过 `SkillTool` 自动匹配 |
| 本质 | TypeScript 执行逻辑 | **Prompt + 权限配置**的声明式封装 |
| 注册位置 | `src/tools.ts` -> `getTools()` | `src/commands.ts` -> `getCommands()` |
| 执行器 | 各 Tool 的 `call()` 方法 | `SkillTool.call()` -> 两条分支(inline / fork) |
> [!info] 核心洞见
> 一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
### Skill 的五个来源与加载链路
#### 1. 内置命令(Built-in Commands)
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
#### 2. Bundled Skills(编译时打包)
通过 `registerBundledSkill()`(`src/skills/bundledSkills.ts:53`)在模块初始化时注册。关键特性:
- **延迟文件提取**:如果 Skill 声明了 `files`(参考文件),首次调用时才解压到临时目录(`getBundledSkillExtractDir()`),使用 `O_NOFOLLOW | O_EXCL` 防止符号链接攻击
- **闭包级 memoize**:并发调用共享同一个 extraction promise,避免竞态写入
- 来源标记为 `source: 'bundled'`,在 Prompt 预算中享有**不可截断**的特权
#### 3. 磁盘 Skills(`.claude/skills/`)
由 `loadSkillsFromSkillsDir()`(`src/skills/loadSkillsDir.ts:407`)加载,这是最重要的加载路径:
```text
管理策略: $MANAGED_DIR/.claude/skills/ (policySettings)
用户全局: ~/.claude/skills/ (userSettings)
项目级: .claude/skills/ (projectSettings, 向上遍历至 home)
附加目录: --add-dir 指定的路径下 .claude/skills/
```
**加载协议**:只识别 `skill-name/SKILL.md` 目录格式,不再支持单文件 `.md`。
加载流程:
1. `readdir` 扫描目录 -> 仅保留 `isDirectory()` 或 `isSymbolicLink()` 的条目
2. 在每个子目录中查找 `SKILL.md`,未找到则跳过
3. `parseFrontmatter()` 解析 YAML 头部,提取 `whenToUse`、`allowedTools`、`context` 等字段
4. `parseSkillFrontmatterFields()`(第 185 行)统一解析 16 个 frontmatter 字段
5. `createSkillCommand()`(第 270 行)构造 `Command` 对象
**去重机制**:使用 `realpath()` 解析符号链接获得规范路径(`getFileIdentity`,第 118 行),避免通过符号链接或重叠父目录导致的重复加载。
#### 4. MCP Skills(动态发现)
通过 `registerMCPSkillBuilders()` 注册构建器,MCP Server 的 prompt 被 `mcpSkillBuilders.ts` 转换为 `Command` 对象。标记为 `loadedFrom: 'mcp'`。
> [!warning] 安全边界
> MCP Skills 的 Prompt 内容**禁止执行内联 shell 命令**(`loadSkillsDir.ts:374` 的 `loadedFrom !== 'mcp'` 守卫),因为远程内容不可信。
#### 5. Legacy Commands(`/commands/` 目录)
向后兼容的旧格式,由 `loadSkillsFromCommandsDir()`(第 566 行)加载。同时支持 `SKILL.md` 目录格式和单 `.md` 文件格式。
### Frontmatter 字段全景
一个 `SKILL.md` 的完整 frontmatter(`parseSkillFrontmatterFields`,第 185 行):
```yaml
---
name: code-review # 显示名称(覆盖目录名)
description: 系统性代码审查 # 描述(或从 Markdown 首段提取)
when_to_use: "用户说审查代码、找 bug" # AI 自动匹配依据
allowed-tools: # 工具白名单
- Read
- Grep
- Glob
argument-hint: "<file-or-directory>" # 参数提示
arguments: [path] # 声明式参数名(用于 $ARGUMENTS 替换)
model: opus # 模型覆盖
effort: high # 努力级别
context: fork # 执行模式:inline(默认)| fork
agent: code-reviewer # 指定 Agent 定义文件
user-invocable: true # 用户是否可 /调用
disable-model-invocation: false # 禁止 AI 自主调用
version: "1.0" # 版本号
paths: # 条件激活的文件路径模式
- "src/**/*.ts"
hooks: # Hook 配置
PreToolUse:
- command: ["echo", "checking"]
shell: ["bash"] # Shell 执行环境
---
```
解析后有 16 个字段被提取,其中 `allowedTools`、`model`、`effort` 在执行时动态修改 `toolPermissionContext`。
### 两条执行路径:Inline vs Fork
SkillTool(`packages/builtin-tools/src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
#### Inline 模式(默认)
Skill 的 Prompt 内容被注入为 **UserMessage**,在主对话流中继续执行:
1. `processPromptSlashCommand()` 处理参数替换(`$ARGUMENTS`)和 shell 命令展开(`` !`...` ``)
2. `${CLAUDE_SKILL_DIR}` 被替换为 Skill 所在目录的绝对路径
3. `${CLAUDE_SESSION_ID}` 被替换为当前会话 ID
4. 返回 `newMessages`(注入到对话流)+ `contextModifier`(修改权限上下文)
`contextModifier`(第 776 行)做了三件事:
- **工具白名单注入**:将 `allowedTools` 合并到 `alwaysAllowRules.command`
- **模型切换**:`resolveSkillModelOverride()` 处理模型覆盖
- **努力级别覆盖**:修改 `effortValue`
#### Fork 模式(`context: fork`)
Skill 在**独立子 Agent** 中执行(`executeForkedSkill`,第 122 行):
1. `prepareForkedCommandContext()` 构建隔离的 Agent 定义和 Prompt
2. `runAgent()` 启动子 Agent 循环,拥有独立的 token 预算
3. 通过 `onProgress` 回调报告工具使用进度
4. 结果通过 `extractResultText()` 提取,子 Agent 的全部消息在提取后被释放(`agentMessages.length = 0`)
5. 最终通过 `clearInvokedSkillsForAgent()` 清理状态
> [!tip] Fork 模式适用场景
> Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务),避免污染主对话的上下文。
### 权限模型:Safe Properties 白名单
`checkPermissions()`(第 433 行)实现了一个五层权限检查:
```mermaid
flowchart TD
A["1. Deny 规则匹配\n支持精确匹配和 prefix:* 通配符"] -->|"未命中"| B["2. 远程 canonical Skill 自动放行"]
B -->|"未命中"| C["3. Allow 规则匹配"]
C -->|"未命中"| D["4. Safe Properties 白名单检查\nskillHasOnlySafeProperties"]
D -->|"有非安全属性"| E["5. Ask 用户确认\n附带精确匹配和前缀匹配建议"]
```
**Safe Properties**(`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 30 个属性名的白名单。任何不在白名单中的**有意义的属性值**(排除 `undefined`、`null`、空数组、空对象)都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
### Prompt 预算:1% 上下文窗口的截断策略
Skill 列表注入 System Prompt 时有严格的字符预算(`prompt.ts`):
- **预算计算**:`contextWindowTokens * 4 chars/token * 1%`(约 8000 字符)
- **单条上限**:`MAX_LISTING_DESC_CHARS = 250` 字符(超出截断为 `...`)
- **Bundled Skills 不可截断**:它们始终保留完整描述,预算不足时只截断非 bundled 的
**降级策略**:
1. 尝试完整描述 -> 超预算?
2. Bundled 保留完整,非 bundled 均分剩余预算 -> 每条描述低于 20 字符?
3. 非 bundled 仅保留名称
`formatCommandsWithinBudget()`(`prompt.ts:70`)实现了这个三级降级。
### 动态发现与条件激活
#### 基于文件路径的动态发现
`discoverSkillDirsForPaths()`(`loadSkillsDir.ts:861`)在文件操作时触发:
1. 从被操作的文件路径开始,**向上遍历**至 CWD(不包含 CWD 本身)
2. 在每层查找 `.claude/skills/` 目录
3. 使用 `realpath` 去重,`git check-ignore` 过滤 gitignored 目录
4. 按路径深度排序(**深层优先**),更接近文件的 Skill 优先级更高
#### 条件激活(paths frontmatter)
带有 `paths` 模式的 Skill 在加载时不会立即可用,而是存入 `conditionalSkills` Map。当被操作的文件路径匹配某个 Skill 的 paths 模式时(使用 `ignore` 库做 gitignore 风格匹配),该 Skill 才被**激活**——从 `conditionalSkills` 移入 `dynamicSkills`。
> [!tip] 条件激活的意义
> 一个只在 `*.test.ts` 上激活的测试 Skill,平时完全不可见,只有当 AI 读取或编辑测试文件时才会出现。
### 使用频率排名
`recordSkillUsage()`(`skillUsageTracking.ts`)使用指数衰减算法计算 Skill 排名分数:
```text
score = usageCount * max(0.5^(daysSinceUse / 7), 0.1)
```
- **7 天半衰期**:一周前的使用权重减半
- **最低 0.1 保底**:避免老但高频使用的 Skill 完全沉底
- **60 秒去抖**:同一 Skill 在 1 分钟内的多次调用只计一次,减少文件 I/O
排名数据持久化在全局配置的 `skillUsage` 字段中。
### 远程技能加载(Experimental)
通过 `EXPERIMENTAL_SKILL_SEARCH` feature flag 控制,支持从远程(AKI/GCS/S3)加载 `_canonical_<slug>` 格式的 Skill:
1. `validateInput()` 中 `stripCanonicalPrefix()` 拦截 canonical 名称
2. `executeRemoteSkill()`(第 970 行)从远程 URL 加载 SKILL.md
3. 支持 `gs://`、`https://`、`s3://` 等 URL 协议
4. 内容经过 frontmatter 剥离、`${CLAUDE_SKILL_DIR}` 替换后直接注入
5. 远程 Skill 不经过 `processPromptSlashCommand`——无 `!command` 替换、无 `$ARGUMENTS` 展开
### 完整生命周期总结
```mermaid
flowchart TD
A["磁盘 SKILL.md"] --> B["parseFrontmatter()\nparseSkillFrontmatterFields()"]
B --> C["createSkillCommand() -> Command 对象"]
C --> D["去重\nrealpath + seenFileIds"]
D --> E{"是条件 Skill?"}
E -->|"是"| F["conditionalSkills Map\n等待路径匹配激活"]
E -->|"否"| G["getSkillDirCommands() memoize 缓存"]
F --> G
G --> H["getAllCommands()\n合并 local + MCP"]
H --> I["formatCommandsWithinBudget()\n截断后注入 System Prompt"]
I --> J["AI 选择匹配的 Skill"]
J --> K["SkillTool.validateInput()\n名称校验 + 存在性检查"]
K --> L["SkillTool.checkPermissions()\n五层权限检查"]
L --> M["SkillTool.call()\ninline 或 fork 执行"]
M --> N["contextModifier()\n注入 allowedTools + model + effort"]
N --> O["recordSkillUsage()\n更新使用频率排名"]
```
## 关联笔记
- [[custom-agents|自定义 Agent]]
- [[hooks|Hooks 生命周期钩子]]
- [[mcp-configuration|MCP 配置]]
- [[mcp-protocol|MCP 协议]]