报告日期:2026-05-12 分析范围:Claude Code CLI 项目完整 skill 系统
目录
- 概述
- 核心类型系统
- [Skill 来源分类](#Skill 来源分类)
- 生命周期
- 动态发现与条件激活
- 权限模型
- [Skill 与工具系统的集成](#Skill 与工具系统的集成)
- [Prompt 预算与列表管理](#Prompt 预算与列表管理)
- 使用追踪
- 关键文件索引
1. 概述
1.1 Skill 是什么
Skill(技能)是 Claude Code 中的一种可扩展的 prompt 注入机制。每个 skill 本质上是一个 Markdown 文件(或程序化定义的 prompt 生成器),当被触发时,会将预定义的指令内容注入到当前对话上下文中,指导 Claude 模型执行特定任务。
1.2 Skill 解决什么问题
没有 skill 系统时,用户每次都需要用自然语言详细描述任务。有了 skill 系统后:
- 标准化复杂任务:将多步骤工作流封装为可复用的 skill
- 预授权工具权限:skill 可以预先声明需要的工具,避免反复弹出权限确认
- 动态上下文注入:skill 可以根据运行时状态动态生成 prompt 内容
- 条件激活:skill 可以根据文件路径自动启用,无需手动触发
1.3 实际案例:/simplify 技能
simplify 技能展示了 skill 系统的核心能力。当用户调用 /simplify 时:
用户输入: /simplify
触发流程:
1. 解析斜杠命令 → 识别为 simplify 技能
2. 注入 SIMPLIFY_PROMPT 到对话上下文
3. 模型根据 prompt 内容执行:
- Phase 1: git diff 获取变更
- Phase 2: 并行启动 3 个 Agent(复用审查、质量审查、效率审查)
- Phase 3: 汇总发现并修复问题
对应的代码实现(src/skills/bundled/simplify.ts):
const SIMPLIFY_PROMPT = `# Simplify: Code Review and Cleanup
Review all changed files for reuse, quality, and efficiency. Fix any issues found.
## Phase 1: Identify Changes
Run \`git diff\` ...
## Phase 2: Launch Three Review Agents in Parallel
Use the Agent tool to launch all three agents concurrently...
### Agent 1: Code Reuse Review
### Agent 2: Code Quality Review
### Agent 3: Efficiency Review
## Phase 3: Fix Issues
Wait for all three agents to complete...`
export function registerSimplifySkill(): void {
registerBundledSkill({
name: 'simplify',
description: 'Review changed code for reuse, quality, and efficiency...',
userInvocable: true,
async getPromptForCommand(args) {
let prompt = SIMPLIFY_PROMPT
if (args) {
prompt += `\n\n## Additional Focus\n\n${args}`
}
return [{ type: 'text', text: prompt }]
},
})
}
关键点 :skill 本身不执行代码,而是通过注入 prompt 指令让模型自主完成任务。getPromptForCommand 是 skill 的核心函数,负责生成最终注入到对话中的内容。
2. 核心类型系统
2.1 Command 联合类型
Skill 系统的核心类型定义在 src/types/command.ts:
// 三种 Command 变体
export type Command = CommandBase & (PromptCommand | LocalCommand | LocalJSXCommand)
| 变体 | type 值 |
用途 | 示例 |
|---|---|---|---|
PromptCommand |
'prompt' |
Skill 的主要类型,生成 prompt 内容注入对话 | /simplify、/review |
LocalCommand |
'local' |
懒加载的本地原生命令,执行特定逻辑 | 某些内部命令 |
LocalJSXCommand |
'local-jsx' |
渲染 React/Ink UI 组件的命令 | /skills、/help |
2.2 PromptCommand 关键字段
PromptCommand 是 skill 的核心类型,定义在 src/types/command.ts:25-57:
export type PromptCommand = {
type: 'prompt' // 固定值,标识这是一个 prompt 类型的命令
progressMessage: string // 执行时显示的进度消息
contentLength: number // 内容字符数(用于 token 估算)
argNames?: string[] // 参数名列表(用于参数替换)
allowedTools?: string[] // 预授权的工具列表
model?: string // 模型覆盖(如 'opus'、'haiku')
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled' // 来源
pluginInfo?: { // 插件元数据
pluginManifest: PluginManifest
repository: string
}
hooks?: HooksSettings // 生命周期钩子
skillRoot?: string // 技能资源根目录
context?: 'inline' | 'fork' // 执行上下文模式
agent?: string // fork 模式下的 Agent 类型
effort?: EffortValue // 思考努力级别
paths?: string[] // 条件激活的 glob 模式
getPromptForCommand( // 核心方法:生成 prompt 内容
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}
字段详解:
| 字段 | 作用 | 示例值 |
|---|---|---|
context |
'inline':内容直接注入当前对话;'fork':在独立子 Agent 中执行 |
'fork' |
allowedTools |
预授权的工具,执行时不会弹出权限确认 | ['Read', 'Bash(git:*)'] |
model |
覆盖当前会话的模型选择 | 'opus' |
effort |
控制模型思考深度 | 'high' |
paths |
条件激活:只有当模型操作匹配的文件时才启用 | ['src/**/*.ts'] |
hooks |
注册生命周期钩子(PreToolUse、PostToolUse 等) | { PreToolUse: [...] } |
2.3 BundledSkillDefinition
内置技能使用 BundledSkillDefinition 类型定义(src/skills/bundledSkills.ts:15-41):
export type BundledSkillDefinition = {
name: string // 技能名称
description: string // 描述
aliases?: string[] // 别名
whenToUse?: string // 何时使用的详细说明(给模型看的)
argumentHint?: string // 参数提示
allowedTools?: string[] // 预授权工具
model?: string // 模型覆盖
disableModelInvocation?: boolean // 禁止模型调用
userInvocable?: boolean // 是否允许用户通过 /name 调用
isEnabled?: () => boolean // 条件启用回调
hooks?: HooksSettings // 生命周期钩子
context?: 'inline' | 'fork' // 执行上下文
agent?: string // Agent 类型
files?: Record<string, string> // 参考文件(首次调用时提取到磁盘)
getPromptForCommand: ( // 核心方法
args: string,
context: ToolUseContext,
) => Promise<ContentBlockParam[]>
}
与 PromptCommand 的映射 :registerBundledSkill() 将 BundledSkillDefinition 转换为 Command 对象,自动设置 source: 'bundled'、loadedFrom: 'bundled' 等字段。
2.4 SKILL.md Frontmatter 完整字段表
自定义技能通过 SKILL.md 文件的 YAML frontmatter 配置(src/utils/frontmatterParser.ts:10-59):
| 字段 | 类型 | 默认值 | 说明 | 使用示例 |
|---|---|---|---|---|
description |
string | - | 技能描述 | description: "代码审查工具" |
allowed-tools |
string \ | string[] | [] |
预授权工具 |
argument-hint |
string | - | 参数提示文本 | argument-hint: "<file> [options]" |
when_to_use |
string | - | 模型自动调用的条件说明 | when_to_use: "Use when reviewing code" |
version |
string | - | 版本号 | version: "1.0" |
model |
string | 'inherit' |
模型覆盖 | model: opus |
user-invocable |
string (boolean) | true |
是否允许用户 /name 调用 | user-invocable: false |
disable-model-invocation |
string (boolean) | false |
禁止模型调用 | disable-model-invocation: true |
hooks |
HooksSettings | - | 生命周期钩子 | 见下方 hooks 示例 |
effort |
string | - | 思考努力级别 | effort: high |
context |
'inline' \ |
'fork' |
'inline' |
执行上下文 |
agent |
string | - | Agent 类型(仅 fork) | agent: general-purpose |
paths |
string \ | string[] | - | 条件激活 glob 模式 |
shell |
string | 'bash' |
内联命令的 shell | shell: powershell |
name |
string | 目录名 | 显示名覆盖 | name: "My Skill" |
arguments |
string \ | string[] | - | 参数名列表 |
完整 SKILL.md 示例:
---
name: my-code-reviewer
description: 智能代码审查工具,支持多种语言
allowed-tools:
- Read
- Grep
- Glob
- Bash(git:*)
when_to_use: "Use when the user wants to review code changes or PRs"
argument-hint: "[file-pattern]"
arguments:
- file-pattern
context: inline
model: opus
effort: high
paths:
- "src/**/*.ts"
- "src/**/*.tsx"
hooks:
PostToolUse:
- matcher: Write|Edit
hooks:
- type: command
command: prettier --write
user-invocable: true
---
# Code Reviewer
## Inputs
- `$file-pattern`: Glob pattern for files to review (default: all changed files)
## Goal
Perform a thorough code review and provide actionable feedback.
## Steps
### 1. Identify Changes
Run `git diff` to see what changed.
**Success criteria**: Complete diff is available.
### 2. Analyze Code Quality
Review each change for:
- Bug potential
- Performance issues
- Security concerns
- Code style
**Success criteria**: All changes reviewed.
### 3. Report Findings
Summarize findings with file/line references.
3. Skill 来源分类
3.1 四种来源概览
| 来源 | 加载方式 | 存储位置 | 安全限制 | 配置能力 |
|---|---|---|---|---|
| 内置 (bundled) | 代码注册 | 编译进 CLI 二进制 | 完全信任 | 完整(程序化定义) |
| 自定义 (file-based) | 磁盘加载 | .claude/skills/ 目录 |
中等 | 完整(SKILL.md frontmatter) |
| MCP | MCP 服务器加载 | 外部 MCP 服务器 | 严格(禁用内联 shell) | 受限 |
| 插件 (plugin) | 插件系统加载 | 插件目录 | 中等 | 完整(pluginInfo 元数据) |
3.2 内置 (Bundled) 技能
注册方式 :在 src/skills/bundled/index.ts 的 initBundledSkills() 中程序化注册:
export function initBundledSkills(): void {
registerUpdateConfigSkill() // 配置 settings.json
registerKeybindingsSkill() // 键盘快捷键自定义
registerVerifySkill() // 代码验证(ANT-ONLY)
registerDebugSkill() // 调试会话问题
registerLoremIpsumSkill() // 生成占位文本(ANT-ONLY)
registerSkillifySkill() // 捕获会话流程为可复用 skill(ANT-ONLY)
registerRememberSkill() // 自动记忆管理(ANT-ONLY)
registerSimplifySkill() // 代码审查(复用/质量/效率)
registerBatchSkill() // 并行工作编排
registerStuckSkill() // 诊断冻结会话(ANT-ONLY)
// ... 特性开关控制的技能
}
内置技能完整列表:
| 技能名 | 文件 | 功能 | 限制 |
|---|---|---|---|
update-config |
updateConfig.ts |
配置 settings.json、hooks、权限 | - |
keybindings |
keybindings.ts |
键盘快捷键自定义 | 需要 feature flag |
verify |
verify.ts |
通过测试/lint 验证代码变更 | ANT-ONLY |
debug |
debug.ts |
诊断会话问题 | disableModelInvocation: true |
lorem-ipsum |
loremIpsum.ts |
生成占位文本 | ANT-ONLY |
skillify |
skillify.ts |
将会话捕获为可复用 skill | ANT-ONLY |
remember |
remember.ts |
审查/管理自动记忆 | ANT-ONLY |
simplify |
simplify.ts |
3 并行 Agent 代码审查 | - |
batch |
batch.ts |
5-30 worktree Agent 并行编排 | disableModelInvocation: true |
stuck |
stuck.ts |
诊断冻结会话 | ANT-ONLY |
loop |
loop.ts |
定时循环执行 | 需要 KAIROS flag |
claude-api |
claudeApi.ts |
Claude API 开发 | 需要 BUILDING_CLAUDE_APPS flag |
条件注册示例:
// src/skills/bundled/index.ts
if (feature('KAIROS') || feature('KAIROS_DREAM')) {
const { registerDreamSkill } = require('./dream.js')
registerDreamSkill()
}
3.3 自定义 (File-based) 技能
目录结构:
# 项目级(推荐)
.claude/skills/
my-reviewer/
SKILL.md # 必需:技能内容 + YAML frontmatter
style-guide.md # 可选:参考文件
# 用户级
~/.claude/skills/
my-reviewer/
SKILL.md
# 企业/策略级
~/.managed/.claude/skills/
my-reviewer/
SKILL.md
# 遗留格式(兼容)
.claude/commands/
my-command.md # 单文件格式(旧)
my-skill/
SKILL.md # 目录格式(旧)
加载层级(按优先级从高到低):
- Managed (策略设置):
~/.managed/.claude/skills/ - User (个人):
~/.claude/skills/ - Project (项目级):
.claude/skills/(向上遍历到 home) - Additional (
--add-dir):<dir>/.claude/skills/ - Legacy commands :
.claude/commands/
加载代码 (src/skills/loadSkillsDir.ts:638-804):
export const getSkillDirCommands = memoize(async (cwd: string): Promise<Command[]> => {
const userSkillsDir = join(getClaudeConfigHomeDir(), 'skills')
const managedSkillsDir = join(getManagedFilePath(), '.claude', 'skills')
const projectSkillsDirs = getProjectDirsUpToHome('skills', cwd)
// 并行加载所有来源
const [managedSkills, userSkills, projectSkillsNested, additionalSkillsNested, legacyCommands] =
await Promise.all([
loadSkillsFromSkillsDir(managedSkillsDir, 'policySettings'),
loadSkillsFromSkillsDir(userSkillsDir, 'userSettings'),
Promise.all(projectSkillsDirs.map(dir => loadSkillsFromSkillsDir(dir, 'projectSettings'))),
Promise.all(additionalDirs.map(dir => loadSkillsFromSkillsDir(join(dir, '.claude', 'skills'), 'projectSettings'))),
loadSkillsFromCommandsDir(cwd),
])
// 按 realpath 去重(处理符号链接)
const fileIds = await Promise.all(allSkillsWithPaths.map(({ skill, filePath }) =>
skill.type === 'prompt' ? getFileIdentity(filePath) : Promise.resolve(null),
))
// ...
})
3.4 MCP 技能
从 MCP(Model Context Protocol)服务器加载的技能:
// src/skills/mcpSkillBuilders.ts
// MCP 技能通过 write-once 注册表加载,避免循环依赖
// 安全限制:MCP 技能永远不执行内联 shell 命令
if (loadedFrom !== 'mcp') {
finalContent = await executeShellCommandsInPrompt(finalContent, ...)
}
安全边界:MCP 技能来自外部服务器,被视为不可信来源,因此:
- 禁用内联 shell 命令执行(
!代码块) ${CLAUDE_SKILL_DIR}变量无意义(不适用)- 需要明确的权限检查
3.5 插件 (Plugin) 技能
插件可以通过 skillsPath/skillsPaths 提供技能:
// 插件定义中的技能声明
type BuiltinPluginDefinition = {
skills?: BundledSkillDefinition[]
// ...
}
// 插件技能加载后设置元数据
const command: Command = {
// ...
source: 'plugin',
loadedFrom: 'plugin',
pluginInfo: {
pluginManifest: manifest,
repository: 'github.com/org/plugin',
},
}
4. 生命周期
4.1 完整生命周期流程图
┌─────────────────────────────────────────────────────────────────────┐
│ Skill 生命周期 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 注册阶段 │───▶│ 加载阶段 │───▶│ 聚合阶段 │ │
│ │ (Startup) │ │ (Load) │ │ (Aggregate) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 触发阶段 │ │ 执行阶段 │ │ 完成阶段 │ │
│ │ (Trigger) │───▶│ (Execute) │───▶│ (Complete) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
4.2 注册阶段
内置技能注册(启动时):
// src/skills/bundled/index.ts
export function initBundledSkills(): void {
registerUpdateConfigSkill() // 调用 registerBundledSkill(definition)
registerKeybindingsSkill()
registerVerifySkill()
// ...
}
// src/skills/bundledSkills.ts
export function registerBundledSkill(definition: BundledSkillDefinition): void {
const { files } = definition
let skillRoot: string | undefined
let getPromptForCommand = definition.getPromptForCommand
// 如果有参考文件,包装 getPromptForCommand 以提取文件到磁盘
if (files && Object.keys(files).length > 0) {
skillRoot = getBundledSkillExtractDir(definition.name)
let extractionPromise: Promise<string | null> | undefined
const inner = definition.getPromptForCommand
getPromptForCommand = async (args, ctx) => {
extractionPromise ??= extractBundledSkillFiles(definition.name, files)
const extractedDir = await extractionPromise
const blocks = await inner(args, ctx)
if (extractedDir === null) return blocks
return prependBaseDir(blocks, extractedDir) // 添加 "Base directory for this skill: ..."
}
}
// 创建 Command 对象并注册
const command: Command = {
type: 'prompt',
name: definition.name,
source: 'bundled',
loadedFrom: 'bundled',
// ... 其他字段映射
getPromptForCommand,
}
bundledSkills.push(command)
}
文件技能加载(启动时 + 动态发现):
加载流程:
1. 扫描所有来源目录(managed/user/project/additional/legacy)
2. 并行读取每个目录下的 skill-name/SKILL.md
3. 解析 YAML frontmatter → 提取元数据
4. 调用 createSkillCommand() 创建 Command 对象
5. 按 realpath 去重(处理符号链接)
6. 分离条件技能(有 paths 字段)和无条件技能
7. 返回无条件技能列表
4.3 加载与聚合
命令聚合 (src/commands.ts:460-517):
// loadAllCommands 按优先级合并所有命令源
const loadAllCommands = memoize(async (cwd: string) => {
const bundledSkills = getBundledSkills() // 1. 内置技能
const builtinPluginSkills = getBuiltinPluginSkills() // 2. 内置插件技能
const skillDirCommands = await getSkillDirCommands(cwd) // 3. 文件技能
const workflowCommands = await getWorkflowCommands() // 4. 工作流命令(feature flag)
const pluginCommands = await getPluginCommands() // 5. 插件命令
const pluginSkills = await getPluginSkills() // 6. 插件技能
return [
...bundledSkills, // 最高优先级
...builtinPluginSkills,
...skillDirCommands,
...workflowCommands,
...pluginCommands,
...pluginSkills,
...COMMANDS(), // 最低优先级(硬编码命令)
]
})
// getCommands 过滤并合并动态技能
export async function getCommands(cwd: string): Promise<Command[]> {
const allCommands = await loadAllCommands(cwd)
const dynamicSkills = getDynamicSkills() // 运行时发现的技能
// 过滤:满足可用性要求 + 已启用
const baseCommands = allCommands.filter(
_ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
)
// 去重后插入动态技能
const baseCommandNames = new Set(baseCommands.map(c => c.name))
const uniqueDynamicSkills = dynamicSkills.filter(
s => !baseCommandNames.has(s.name) && meetsAvailabilityRequirement(s) && isCommandEnabled(s),
)
// 插入到内置命令之前
const builtInNames = new Set(COMMANDS().map(c => c.name))
const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))
// ...
}
4.4 触发机制
Skill 有两条触发路径:
路径 A:用户输入 /skill-name
用户输入 "/simplify"
│
▼
processUserInput() // src/utils/processUserInput/processUserInput.ts
│
▼
parseSlashCommand() // src/utils/slashCommandParsing.ts
│ 解析: { commandName: "simplify", args: "", isMcp: false }
│
▼
processPromptSlashCommand() // src/utils/processUserInput/processSlashCommand.tsx
│
├── 查找命令: findCommand("simplify", commands)
│
├── 获取 prompt: command.getPromptForCommand(args, context)
│ │
│ ├── 参数替换: $argName → 实际值
│ ├── 变量替换: ${CLAUDE_SKILL_DIR} → 技能目录路径
│ ├── 变量替换: ${CLAUDE_SESSION_ID} → 当前会话 ID
│ └── 执行内联 shell: !`cmd` → 实际输出
│
├── 注册 hooks: registerSkillHooks()
│
├── 记录使用: recordSkillUsage()
│
└── 返回消息注入对话
路径 B:模型调用 SkillTool
模型判断用户请求匹配某个 skill
│
▼
调用 Skill 工具: { skill: "simplify", args: "" }
│
▼
SkillTool.validateInput() // src/tools/SkillTool/SkillTool.ts:354
│
├── 标准化名称: 去除前导 "/"
├── 查找命令: getAllCommands() → findCommand()
├── 检查: 是否存在?是否 prompt 类型?是否禁用模型调用?
│
▼
SkillTool.checkPermissions() // SkillTool.ts:432
│
├── 检查 deny 规则
├── 检查 allow 规则
├── 检查 safe auto-allow(仅安全属性的技能)
│
▼
SkillTool.call() // SkillTool.ts:580
│
├── 记录使用: recordSkillUsage(commandName)
│
├── 检查执行模式:
│ ├── context === 'fork' → executeForkedSkill()
│ └── 默认 → processPromptSlashCommand()(inline 模式)
│
└── 返回: { data, newMessages, contextModifier }
4.5 执行流程:inline vs forked
Inline 模式(默认)
技能内容直接注入当前对话上下文:
// SkillTool.ts:635-643
const { processPromptSlashCommand } = await import(
'src/utils/processUserInput/processSlashCommand.js'
)
const processedCommand = await processPromptSlashCommand(
commandName,
args || '',
commands,
context,
)
// 返回 contextModifier 修改后续工具权限
return {
data: { success: true, commandName, allowedTools, model },
newMessages, // 注入到对话中的消息
contextModifier(ctx) {
// 修改 allowedTools、model、effort
// ...
},
}
contextModifier 的作用:
contextModifier(ctx) {
let modifiedContext = ctx
// 1. 修改工具权限
if (allowedTools.length > 0) {
modifiedContext = {
...modifiedContext,
getAppState() {
const appState = modifiedContext.getAppState()
return {
...appState,
toolPermissionContext: {
...appState.toolPermissionContext,
alwaysAllowRules: {
...appState.toolPermissionContext.alwaysAllowRules,
command: [
...new Set([
...(appState.toolPermissionContext.alwaysAllowRules.command || []),
...allowedTools,
]),
],
},
},
}
},
}
}
// 2. 修改模型选择
if (model) {
modifiedContext = { ...modifiedContext, model }
}
return modifiedContext
}
Forked 模式
在独立子 Agent 中执行,有自己的 token 预算:
// SkillTool.ts:122-150
async function executeForkedSkill(
command: Command & { type: 'prompt' },
commandName: string,
args: string | undefined,
context: ToolUseContext,
canUseTool: CanUseToolFn,
parentMessage: AssistantMessage,
onProgress?: ToolCallProgress<Progress>,
): Promise<ToolResult<Output>> {
const agentId = createAgentId()
// ...
const result = await runAgent({
// 独立的上下文和 token 预算
agentId,
prompt: command.getPromptForCommand(args, context),
// ...
})
// ...
}
5. 动态发现与条件激活
5.1 动态技能发现
当模型读写文件时,系统会自动发现相关的 .claude/skills/ 目录:
// src/skills/loadSkillsDir.ts:861-915
export async function discoverSkillDirsForPaths(
filePaths: string[],
cwd: string,
): Promise<string[]> {
const newDirs: string[] = []
for (const filePath of filePaths) {
let currentDir = dirname(filePath)
// 从文件目录向上遍历到 cwd
while (currentDir.startsWith(resolvedCwd + pathSep)) {
const skillDir = join(currentDir, '.claude', 'skills')
// 跳过已检查的路径(缓存)
if (!dynamicSkillDirs.has(skillDir)) {
dynamicSkillDirs.add(skillDir)
try {
await fs.stat(skillDir)
// 检查是否被 gitignore 忽略
if (await isPathGitignored(currentDir, resolvedCwd)) {
continue
}
newDirs.push(skillDir)
} catch {
// 目录不存在,跳过
}
}
currentDir = dirname(currentDir)
}
}
// 按深度排序(最深的优先)
return newDirs.sort(
(a, b) => b.split(pathSep).length - a.split(pathSep).length,
)
}
5.2 条件激活(Path-Filtered Skills)
带有 paths frontmatter 的技能只在匹配的文件被操作时激活:
// src/skills/loadSkillsDir.ts:771-796
// 分离条件技能和无条件技能
const unconditionalSkills: Command[] = []
const newConditionalSkills: Command[] = []
for (const skill of deduplicatedSkills) {
if (
skill.type === 'prompt' &&
skill.paths &&
skill.paths.length > 0 &&
!activatedConditionalSkillNames.has(skill.name)
) {
newConditionalSkills.push(skill) // 条件技能,暂存
} else {
unconditionalSkills.push(skill) // 无条件技能,立即可用
}
}
// 存储条件技能,等待匹配文件时激活
for (const skill of newConditionalSkills) {
conditionalSkills.set(skill.name, skill)
}
激活条件 :当模型操作的文件匹配 paths glob 模式时:
// src/skills/loadSkillsDir.ts (activateConditionalSkillsForPaths)
// 使用 ignore 库进行 gitignore 风格的路径匹配
示例:
# 某个技能只在 TypeScript 文件被操作时启用
paths:
- "src/**/*.ts"
- "src/**/*.tsx"
5.3 热重载机制
使用 chokidar 监听技能文件变化:
// src/utils/skills/skillChangeDetector.ts
// 时间常量
const FILE_STABILITY_THRESHOLD_MS = 1000 // 文件稳定性阈值
const FILE_STABILITY_POLL_INTERVAL_MS = 500 // 轮询间隔
const RELOAD_DEBOUNCE_MS = 300 // 重载防抖时间
// 初始化监听
export async function initialize(): Promise<void> {
const paths = await getWatchablePaths()
watcher = chokidar.watch(paths, {
persistent: true,
ignoreInitial: true,
depth: 2, // skill-name/SKILL.md 格式
awaitWriteFinish: {
stabilityThreshold: FILE_STABILITY_THRESHOLD_MS,
pollInterval: FILE_STABILITY_POLL_INTERVAL_MS,
},
usePolling: USE_POLLING, // Bun 环境使用轮询(避免死锁)
})
watcher.on('add', handleChange)
watcher.on('change', handleChange)
watcher.on('unlink', handleChange)
}
// 防抖处理
function scheduleReload(changedPath: string): void {
pendingChangedPaths.add(changedPath)
if (reloadTimer) clearTimeout(reloadTimer)
reloadTimer = setTimeout(async () => {
const paths = [...pendingChangedPaths]
pendingChangedPaths.clear()
// 触发 ConfigChange hooks
const results = await executeConfigChangeHooks('skills', paths[0]!)
if (hasBlockingResult(results)) return // hook 阻止重载
// 清除缓存并通知
clearSkillCaches()
clearCommandsCache()
resetSentSkillNames()
skillsChanged.emit()
}, RELOAD_DEBOUNCE_MS)
}
重载流程:
- chokidar 检测到文件变化(add/change/unlink)
- 300ms 防抖合并多次变化
- 触发
ConfigChangehooks(可阻止重载) - 清除所有技能缓存
- 重置已发送的技能名称
- 发出
skillsChanged信号 - UI 更新技能列表
6. 权限模型
6.1 分层权限检查
SkillTool 实现了 4 层权限检查(src/tools/SkillTool/SkillTool.ts:432-578):
┌─────────────────────────────────────────────────────────────────┐
│ 权限检查流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 第 1 层:Deny 规则 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 检查是否有 deny 规则匹配技能名称 │ │
│ │ 匹配方式:精确匹配 或 前缀匹配("review:*" 匹配 "review-pr")│ │
│ │ 结果:deny → 阻止执行,返回错误 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第 2 层:远程 Canonical 技能(实验性) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 如果是 _canonical_ 前缀的远程技能,自动授权 │ │
│ │ (远程技能经过审核,视为可信) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第 3 层:Allow 规则 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 检查是否有 allow 规则匹配技能名称 │ │
│ │ 匹配方式:精确匹配 或 前缀匹配 │ │
│ │ 结果:allow → 授权执行 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第 4 层:Safe Auto-Allow │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 检查技能是否只有 "安全" 属性 │ │
│ │ 安全属性列表:SAFE_SKILL_PROPERTIES(约 30 个属性) │ │
│ │ 如果有 hooks 等非安全属性 → 需要用户确认 │ │
│ │ 结果:safe → 自动授权;unsafe → 询问用户 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 第 5 层:用户交互确认 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 显示 SkillPermissionRequest UI │ │
│ │ 选项:Yes / Yes + don't ask again (exact) / │ │
│ │ Yes + don't ask again (prefix) / No │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 规则匹配逻辑
// SkillTool.ts:451-467
const ruleMatches = (ruleContent: string): boolean => {
// 标准化:去除前导斜杠
const normalizedRule = ruleContent.startsWith('/')
? ruleContent.substring(1)
: ruleContent
// 精确匹配
if (normalizedRule === commandName) {
return true
}
// 前缀匹配(如 "review:*" 匹配 "review-pr 123")
if (normalizedRule.endsWith(':*')) {
const prefix = normalizedRule.slice(0, -2)
return commandName.startsWith(prefix)
}
return false
}
6.3 Safe Auto-Allow 判定
// SkillTool.ts:875-933
const SAFE_SKILL_PROPERTIES = new Set([
// PromptCommand 属性
'type', 'progressMessage', 'contentLength', 'argNames',
'model', 'effort', 'source', 'pluginInfo', 'disableNonInteractive',
'skillRoot', 'context', 'agent', 'getPromptForCommand', 'frontmatterKeys',
// CommandBase 属性
'name', 'description', 'hasUserSpecifiedDescription', 'isEnabled',
'isHidden', 'aliases', 'isMcp', 'argumentHint', 'whenToUse', 'paths',
'version', 'disableModelInvocation', 'userInvocable', 'loadedFrom',
'immediate', 'userFacingName',
])
function skillHasOnlySafeProperties(command: Command): boolean {
for (const key of Object.keys(command)) {
if (SAFE_SKILL_PROPERTIES.has(key)) continue
// 检查是否有有意义的值
const value = (command as Record<string, unknown>)[key]
if (value === undefined || value === null) continue
if (Array.isArray(value) && value.length === 0) continue
if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) continue
return false // 有非安全属性且有值,需要权限
}
return true
}
示例:
// 纯文本技能(只有安全属性)→ 自动授权
const safeSkill = {
type: 'prompt',
name: 'my-skill',
description: 'A simple skill',
// ... 只有 SAFE_SKILL_PROPERTIES 中的属性
}
// skillHasOnlySafeProperties(safeSkill) → true → 自动授权
// 带 hooks 的技能(非安全属性)→ 需要用户确认
const unsafeSkill = {
type: 'prompt',
name: 'my-skill',
hooks: { // hooks 不在 SAFE_SKILL_PROPERTIES 中
PostToolUse: [{ matcher: 'Write', hooks: [{ type: 'command', command: 'prettier' }] }]
},
}
// skillHasOnlySafeProperties(unsafeSkill) → false → 需要用户确认
6.4 用户交互确认 UI
当需要用户确认时,显示 SkillPermissionRequest 组件:
// src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx
// 提供四个选项:
// 1. Yes - 允许本次执行
// 2. Yes + don't ask again (exact) - 添加精确匹配的 allow 规则
// 3. Yes + don't ask again (prefix) - 添加前缀匹配的 allow 规则(如 "skill:*")
// 4. No - 拒绝执行
7. Skill 与工具系统的集成
7.1 allowedTools 预授权
技能可以通过 allowedTools 预先声明需要的工具权限:
# SKILL.md frontmatter
allowed-tools:
- Read
- Grep
- Glob
- Bash(git:*)
- Bash(npm:*)
// 执行时,allowedTools 被合并到工具权限上下文
contextModifier(ctx) {
const appState = ctx.getAppState()
return {
...ctx,
getAppState() {
return {
...appState,
toolPermissionContext: {
...appState.toolPermissionContext,
alwaysAllowRules: {
...appState.toolPermissionContext.alwaysAllowRules,
command: [
...new Set([
...(appState.toolPermissionContext.alwaysAllowRules.command || []),
...allowedTools,
]),
],
},
},
}
},
}
}
7.2 ToolUseContext 上下文
当 getPromptForCommand 被调用时,它接收 ToolUseContext 参数:
// src/Tool.ts
type ToolUseContext = {
messages: Message[] // 当前对话消息历史
getAppState: () => AppState // 获取应用状态
options: {
tools: Tool[] // 可用工具列表
// ...
}
// ...
}
技能可以通过这个上下文:
- 读取会话记忆和消息历史
- 访问应用状态(MCP 连接、设置等)
- 获取可用工具列表
7.3 Hooks 系统
技能可以在 frontmatter 中声明生命周期钩子:
hooks:
PreToolUse:
- matcher: Write|Edit
hooks:
- type: command
command: "echo 'About to write file'"
PostToolUse:
- matcher: Write|Edit
hooks:
- type: command
command: prettier --write $CLAUDE_FILE_PATH
Stop:
- hooks:
- type: command
command: "echo 'Skill execution stopped'"
钩子通过 registerSkillHooks() 注册为会话级钩子,支持 once: true 一次性执行。
7.4 实际案例:simplify 的多 Agent 协作
// src/skills/bundled/simplify.ts
const SIMPLIFY_PROMPT = `# Simplify: Code Review and Cleanup
## Phase 2: Launch Three Review Agents in Parallel
Use the Agent tool to launch all three agents concurrently in a single message.
### Agent 1: Code Reuse Review
Search for existing utilities that could replace newly written code...
### Agent 2: Code Quality Review
Review for hacky patterns: redundant state, parameter sprawl...
### Agent 3: Efficiency Review
Review for efficiency: unnecessary work, missed concurrency...
## Phase 3: Fix Issues
Wait for all three agents to complete. Aggregate their findings...`
// getPromptForCommand 返回这个 prompt
// 模型读取后会自主调用 Agent 工具启动 3 个并行子 Agent
// 每个 Agent 独立审查代码的不同方面
// 最后汇总结果并修复问题
执行流程:
/simplify 触发
│
▼
注入 SIMPLIFY_PROMPT 到对话
│
▼
模型读取 prompt 内容
│
▼
模型调用 Agent 工具 3 次(并行)
│
├── Agent 1: 代码复用审查
├── Agent 2: 代码质量审查
└── Agent 3: 效率审查
│
▼
等待所有 Agent 完成
│
▼
模型汇总发现并修复问题
8. Prompt 预算与列表管理
8.1 预算计算
技能列表占用上下文窗口的 1%(src/tools/SkillTool/prompt.ts:21-41):
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000 // 回退值:200k × 1% × 4
export function getCharBudget(contextWindowTokens?: number): number {
// 允许环境变量覆盖
if (Number(process.env.SLASH_COMMAND_TOOL_CHAR_BUDGET)) {
return Number(process.env.SLASH_COMMAND_TOOL_CHAR_BUDGET)
}
if (contextWindowTokens) {
return Math.floor(
contextWindowTokens * CHARS_PER_TOKEN * SKILL_BUDGET_CONTEXT_PERCENT,
)
}
return DEFAULT_CHAR_BUDGET
}
计算示例:
| 上下文窗口 | 字符预算 | 可容纳技能数(平均 200 字符/技能) |
|---|---|---|
| 200k tokens | 8,000 chars | ~40 个技能 |
| 100k tokens | 4,000 chars | ~20 个技能 |
8.2 描述截断策略
export const MAX_LISTING_DESC_CHARS = 250
function formatCommandsWithinBudget(
commands: Command[],
contextWindowTokens?: number,
): string {
const budget = getCharBudget(contextWindowTokens)
// 尝试完整描述
const fullEntries = commands.map(cmd => ({
cmd,
full: formatCommandDescription(cmd),
}))
const fullTotal = fullEntries.reduce((sum, e) => sum + stringWidth(e.full), 0)
+ (fullEntries.length - 1)
if (fullTotal <= budget) {
return fullEntries.map(e => e.full).join('\n')
}
// 预算不足时,分层截断:
// 1. Bundled 技能永远保留完整描述
// 2. 非 bundled 技能按预算截断
// 3. 极端情况:非 bundled 只显示名称
// ...
}
截断逻辑:
- 完整描述:如果总字符数在预算内,显示所有完整描述
- Bundled 优先:内置技能保留完整描述,其他技能截断
- 名称-only:极端情况下,非 bundled 技能只显示名称
8.3 技能列表生成
// prompt.ts:173-196
export const getPrompt = memoize(async (_cwd: string): Promise<string> => {
return `Execute a skill within the main conversation
When users ask you to perform tasks, check if any of the available skills match.
How to invoke:
- Use this tool with the skill name and optional arguments
- Examples:
- \`skill: "pdf"\` - invoke the pdf skill
- \`skill: "commit", args: "-m 'Fix bug'"\` - invoke with arguments
Important:
- Available skills are listed in system-reminder messages
- When a skill matches, this is a BLOCKING REQUIREMENT: invoke BEFORE generating any other response
- NEVER mention a skill without actually calling this tool
- Do not invoke a skill that is already running
`
})
9. 使用追踪
9.1 指数衰减评分算法
// src/utils/suggestions/skillUsageTracking.ts
export function getSkillUsageScore(skillName: string): number {
const config = getGlobalConfig()
const usage = config.skillUsage?.[skillName]
if (!usage) return 0
// 时间衰减:每 7 天分数减半
const daysSinceUse = (Date.now() - usage.lastUsedAt) / (1000 * 60 * 60 * 24)
const recencyFactor = Math.pow(0.5, daysSinceUse / 7)
// 最小衰减因子 0.1,避免旧但高频使用的技能完全消失
return usage.usageCount * Math.max(recencyFactor, 0.1)
}
评分示例:
| 使用次数 | 最后使用时间 | 衰减因子 | 最终得分 |
|---|---|---|---|
| 10 | 今天 | 1.0 | 10.0 |
| 10 | 7 天前 | 0.5 | 5.0 |
| 10 | 14 天前 | 0.25 | 2.5 |
| 10 | 30 天前 | 0.1(最小值) | 1.0 |
| 1 | 今天 | 1.0 | 1.0 |
9.2 数据存储结构
{
"skillUsage": {
"simplify": {
"usageCount": 15,
"lastUsedAt": 1715500000000
},
"review": {
"usageCount": 8,
"lastUsedAt": 1715400000000
}
}
}
9.3 防抖写入
const SKILL_USAGE_DEBOUNCE_MS = 60_000 // 1 分钟防抖
export function recordSkillUsage(skillName: string): void {
const now = Date.now()
const lastWrite = lastWriteBySkill.get(skillName)
// 1 分钟内不重复写入(分数算法使用 7 天半衰期,分钟级精度无关紧要)
if (lastWrite !== undefined && now - lastWrite < SKILL_USAGE_DEBOUNCE_MS) {
return
}
lastWriteBySkill.set(skillName, now)
saveGlobalConfig(current => {
const existing = current.skillUsage?.[skillName]
return {
...current,
skillUsage: {
...current.skillUsage,
[skillName]: {
usageCount: (existing?.usageCount ?? 0) + 1,
lastUsedAt: now,
},
},
}
})
}
10. 关键文件索引
10.1 类型定义
| 文件 | 职责 | 关键行数 |
|---|---|---|
src/types/command.ts |
Command 联合类型、PromptCommand、CommandBase 定义 | 1-217 |
src/skills/bundledSkills.ts |
BundledSkillDefinition 类型、内置技能注册表 | 1-221 |
src/utils/frontmatterParser.ts |
FrontmatterData 类型、YAML frontmatter 解析 | 1-371 |
10.2 注册与加载
| 文件 | 职责 | 关键行数 |
|---|---|---|
src/skills/bundled/index.ts |
内置技能初始化入口 | 1-80 |
src/skills/loadSkillsDir.ts |
文件技能加载、frontmatter 解析、动态发现 | 1-930+ |
src/skills/mcpSkillBuilders.ts |
MCP 技能加载桥接 | - |
src/commands.ts |
命令聚合、优先级排序、过滤 | 1-580+ |
10.3 触发与执行
| 文件 | 职责 | 关键行数 |
|---|---|---|
src/tools/SkillTool/SkillTool.ts |
Skill 工具定义、验证、权限、执行 | 1-950+ |
src/tools/SkillTool/prompt.ts |
技能列表生成、Prompt 预算管理 | 1-242 |
src/utils/processUserInput/processSlashCommand.tsx |
斜杠命令处理、inline/fork 执行 | - |
src/utils/processUserInput/processUserInput.ts |
用户输入入口、斜杠命令检测 | - |
src/utils/slashCommandParsing.ts |
斜杠命令解析 | - |
10.4 权限管理
| 文件 | 职责 | 关键行数 |
|---|---|---|
src/tools/SkillTool/SkillTool.ts |
checkPermissions、SAFE_SKILL_PROPERTIES | 432-933 |
src/components/permissions/SkillPermissionRequest/SkillPermissionRequest.tsx |
权限确认 UI | - |
10.5 动态发现与热重载
| 文件 | 职责 | 关键行数 |
|---|---|---|
src/skills/loadSkillsDir.ts |
discoverSkillDirsForPaths、条件激活 | 830-930+ |
src/utils/skills/skillChangeDetector.ts |
chokidar 文件监听、防抖重载 | 1-312 |
10.6 使用追踪
| 文件 | 职责 | 关键行数 |
|---|---|---|
src/utils/suggestions/skillUsageTracking.ts |
指数衰减评分、防抖写入 | 1-56 |
10.7 UI 组件
| 文件 | 职责 | 关键行数 |
|---|---|---|
src/components/skills/SkillsMenu.tsx |
/skills 对话框、技能列表展示 | 1-50+ |
src/commands/skills/index.ts |
/skills 命令定义(type: local-jsx) | - |
10.8 内置技能实现
| 文件 | 技能 | 关键行数 |
|---|---|---|
src/skills/bundled/simplify.ts |
simplify(3 并行 Agent 审查) | 1-70 |
src/skills/bundled/updateConfig.ts |
update-config(配置 settings.json) | - |
src/skills/bundled/keybindings.ts |
keybindings(键盘快捷键) | - |
src/skills/bundled/verify.ts |
verify(代码验证) | - |
src/skills/bundled/debug.ts |
debug(调试诊断) | - |
src/skills/bundled/batch.ts |
batch(并行工作编排) | - |
src/skills/bundled/claudeApi.ts |
claude-api(API 开发) | - |
附录 A:技能执行完整时序图
用户输入 /skill-name
│
▼
processUserInput()
│
▼
parseSlashCommand() → { commandName, args }
│
▼
processPromptSlashCommand()
│
├── findCommand() → Command 对象
│
├── command.getPromptForCommand(args, context)
│ │
│ ├── substituteArguments() → 参数替换
│ ├── ${CLAUDE_SKILL_DIR} → 技能目录
│ ├── ${CLAUDE_SESSION_ID} → 会话 ID
│ └── executeShellCommandsInPrompt() → 内联命令
│
├── registerSkillHooks() → 注册钩子
│
├── recordSkillUsage() → 记录使用
│
└── 返回 { messages, shouldQuery, allowedTools, model }
│
▼
注入消息到对话上下文
│
▼
模型读取 skill 内容并执行
│
├── 调用工具(Read、Bash、Agent 等)
│
└── 完成任务
附录 B:SKILL.md 编写最佳实践
- 明确目标 :在
## Goal部分清晰描述成功标准 - 分步骤:将复杂任务分解为可执行的步骤
- 成功标准 :每个步骤后添加
**Success criteria** - 参数化 :使用
arguments和$argName支持动态输入 - 工具预授权 :通过
allowed-tools避免反复权限确认 - 条件激活 :使用
paths只在相关文件被操作时启用 - 安全优先 :避免使用
hooks(会导致需要权限确认),除非必要