Files
claude-code/docs/extensibility/skills.mdx
CyberScrubber 8b2532a9c1 docs: fix documentation deviations from source code (#220)
* docs: 修正 docs/conversation 文档与源码的偏差(multi-turn/streaming/the-loop)

- multi-turn: TranscriptWriter→Project 私有类, 会话路径改用 sanitized-cwd,
  补充 StoredCostState.lastDuration 字段, 模型切换改为 setModel(),
  QueryEngine 状态补全 loadedNestedMemoryPaths/hasHandledOrphanedPermission,
  行号改为符号引用
- streaming: STALL_THRESHOLD_MS 10s→30s, 新增 90s 主动空闲看门狗描述,
  非流式降级补充 didFallBackToNonStreaming/executeNonStreamingRequest,
  行号改为符号引用
- the-loop: 终止条件 7→11, 继续条件重整为 5 组层级结构,
  max_output_tokens 拆分 escalate/recovery 子阶段,
  prompt-too-long 拆分 collapse_drain/reactive_compact 子策略,
  State 类型修正 autoCompactTracking 为可选, 行号改为符号引用
- 全部: 添加 sourceRef 版本锚定(3ec5675)

* docs: 修正 docs/extensibility 文档与源码的偏差(custom-agents/hooks/skills)

- custom-agents: Verification 模型修正为 inherit, 补充 Plugin Agent 字段限制
  (permissionMode/hooks/mcpServers 被安全忽略, isolation 仅 worktree),
  加载流程修正为 6 层优先级, 补充 memory snapshot 门控条件
- hooks: 事件数 22→27(补充 Notification), Hook 类型定义位置修正为 3 个文件,
  行号改为符号引用, Zod schema 范围修正, 去重键修正为四部分复合键,
  registerFrontmatterHooks/clearSessionHooks 区分定义位置和调用位置
- skills: 字段数 17→16, 权限层级 4→5(补充 remote canonical auto-allow),
  SAFE_SKILL_PROPERTIES 28→30, skillUsageTracking 路径修正,
  行号改为符号引用
- mcp-protocol: 全部验证通过, 无需修改
- 全部: 添加 sourceRef 版本锚定(3ec5675)

* Revert "docs: 修正 docs/extensibility 文档与源码的偏差(custom-agents/hooks/skills)"

* docs: 修正 docs/extensibility 文档与源码的偏差(hooks/skills/mcp-protocol)

hooks:
- 事件数 22→27(补充 Notification 事件)
- Hook 类型定义位置修正为 3 个文件分布
  (schemas/hooks.ts / types/hooks.ts / utils/hooks/sessionHooks.ts)
- Zod schema 引用从硬编码行号改为符号引用
- hookSpecificOutput 表从 6 扩展至 15 个事件
  (补全 permissionDecisionReason / PostToolUseFailure / SubagentStart 等)
- 去重键从 pluginRoot\0command 修正为四部分复合键
  (pluginRoot\0shell\0command\0ifCondition)
- 全部硬编码行号改为符号引用以避免版本漂移

skills:
- parseSkillFrontmatterFields 字段数 17→16
- SAFE_SKILL_PROPERTIES 属性数 28→30
- checkPermissions 层级 4→5
- 第 2 层描述从"官方市场"修正为"远程 canonical"

mcp-protocol:
- 配置层级从"三级"修正为
  "enterprise 独占或合并 user/project/local + plugin + claude.ai"

* docs: 修正 system-prompt.mdx 中 Boundary 章节的层级与可读性

- Boundary 插入条件从 ### 降为 blockquote,不再打断三种分块模式的并列结构
- 表格中 Boundary 缓存策略列补充说明其分割作用
- 新增 Boundary 概念释义(blockquote),解释其分割静态区/动态区以实现全局缓存的设计意图
2026-04-09 17:53:11 +08:00

222 lines
11 KiB
Plaintext
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.
---
title: "Skills 技能系统 - Prompt 即能力的架构哲学"
description: "深入剖析 Claude Code Skills 系统的完整实现从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行inline/fork、权限白名单、条件激活、动态发现到远程技能加载揭示一条完整的 Skill 生命周期链路。"
keywords: ["Skills", "SkillTool", "技能加载", "Frontmatter", "whenToUse", "allowedTools", "fork执行", "动态发现"]
---
{/* 本章目标:揭示 Skill 系统从文件到执行的全链路实现 */}
## 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 |
Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Prompt 质量**。一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
## Skill 的五个来源与加载链路
### 1. 内置命令Built-in Commands
硬编码在 `src/commands.ts:258` 的 `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` 防止符号链接攻击(`safeWriteFile`,第 186 行)
- **闭包级 memoize**:并发调用共享同一个 extraction promise避免竞态写入
- 来源标记为 `source: 'bundled'`,在 Prompt 预算中享有**不可截断**的特权
### 3. 磁盘 Skills`.claude/skills/`
由 `loadSkillsFromSkillsDir()``src/skills/loadSkillsDir.ts:407`)加载,这是最重要的加载路径:
```
管理策略: $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'`。
**安全边界**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`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()` 处理模型覆盖,保留 `[1m]` 后缀以避免 200K 窗口截断
- **努力级别覆盖**:修改 `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()` 清理状态
Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务),避免污染主对话的上下文。
## 权限模型Safe Properties 白名单
`checkPermissions()`(第 433 行)实现了一个五层权限检查:
```
1. Deny 规则匹配(支持精确匹配和 prefix:* 通配符)
↓ 未命中
2. 远程 canonical Skill 自动放行EXPERIMENTAL_SKILL_SEARCH + USER_TYPE === 'ant'
↓ 未命中
3. Allow 规则匹配
↓ 未命中
4. Safe Properties 白名单检查skillHasOnlySafeProperties第 911 行)
↓ 有非安全属性
5. Ask 用户确认(附带精确匹配和前缀匹配两条建议规则)
```
**Safe Properties**`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 30 个属性名的白名单(覆盖 `PromptCommand` 和 `CommandBase` 两个类型的所有安全属性)。任何不在白名单中的**有意义的属性值**(排除 `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`。
这意味着一个只在 `*.test.ts` 上激活的测试 Skill平时完全不可见只有当 AI 读取或编辑测试文件时才会出现。
## 使用频率排名
`recordSkillUsage()``skillUsageTracking.ts`)使用指数衰减算法计算 Skill 排名分数:
```
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. 通过 `addInvokedSkill()` 注册到 compaction 保留状态,确保压缩后仍可恢复
6. 远程 Skill 不经过 `processPromptSlashCommand`——无 `!command` 替换、无 `$ARGUMENTS` 展开
## 完整生命周期总结
```
磁盘 SKILL.md
↓ parseFrontmatter()
↓ parseSkillFrontmatterFields() → 16 个字段
↓ createSkillCommand() → Command 对象
↓ 去重realpath + seenFileIds
↓ 条件 Skill → conditionalSkills Map等待路径匹配激活
↓ getSkillDirCommands() memoize 缓存
↓ getAllCommands() 合并 local + MCP
↓ formatCommandsWithinBudget() → 截断后的 Skill 列表注入 System Prompt
↓ AI 选择匹配的 Skill
↓ SkillTool.validateInput() → 名称校验 + 存在性检查
↓ SkillTool.checkPermissions() → 五层权限检查
↓ SkillTool.call() → inline 或 fork 执行
↓ contextModifier() → 注入 allowedTools + model + effort
↓ recordSkillUsage() → 更新使用频率排名
```