1.1 重新理解「技能」的本质
1.1.1 技能是什么
在 Claude Code 源码中,技能的本质定义在 src/commands.ts 中:
// Skills are commands that provide specialized capabilities for the model to use.
// They are identified by loadedFrom being 'skills', 'plugin', or 'bundled',
// or having disableModelInvocation set.
export const getSlashCommandToolSkills = memoize(
async (cwd: string): Promise => {
const allCommands = await getCommands(cwd)
return allCommands.filter(
cmd =>
cmd.type === 'prompt' &&
(cmd.loadedFrom === 'skills' ||
cmd.loadedFrom === 'plugin' ||
cmd.loadedFrom === 'bundled' ||
cmd.disableModelInvocation),
)
},
)
关键洞察:技能本质上是一个 Command 对象,必须满足以下条件之一:
- `loadedFrom` 为 `skills`(文件技能)、`plugin`(插件技能)或 `bundled`(内置技能)
- 或者设置了 `disableModelInvocation`(禁用模型调用)
1.1.2 技能不是提示词,是能力封装
传统观念:技能 = 一段系统提示词。
Claude Code 的设计:技能 = 结构化的能力封装,包含:
| 字段 | 含义 | 示例 |
| `name` | 技能名称 | `verify` |
| `description` | 人类可读的描述 | 验证代码变更是否达到预期 |
| `whenToUse` | 何时自动调用 | 用户要求验证时 |
| `allowedTools` | 允许使用的工具 | `["Bash", "Read", "Edit"]` |
| `model` | 模型覆盖 | 使用特定模型执行 |
| `context` | 执行上下文 | `inline` 或 `fork` |
| `agent` | 代理类型 | `task` 子代理 |
| `effort` | 投入精力级别 | `medium` |
| `hooks` | 生命周期钩子 | 执行前/后的回调 |
| `isEnabled` | 动态可见性控制 | `() => isFeatureEnabled()` |
| `aliases` | 技能别名 | `['keys', 'shortcuts']` |
| `kind` | 类型标记 | `'workflow'` |
| `immediate` | 立即执行 | 跳过队列 |
| `isSensitive` | 敏感信息隐藏 | 参数在历史中隐藏 |
| `version` | 版本号 | 未来版本控制用 |
|---|
1.1.3 技能的前端格式:SKILL.md
用户定义的技能使用 Markdown 文件格式(.claude/skills//SKILL.md):
---
name: verify
description: 验证代码变更是否达到预期
allowed-tools:
- Bash(npm test:*)
- Read
- Grep
when_to_use: |
当用户要求验证功能、运行测试时使用
argument-hint: "${验证目标}"
arguments:
- 验证目标
context: fork
---
# Verify Skill
## 目标
验证代码变更产生预期的结果。
## Steps
### 1. 理解验证目标
与用户确认需要验证的具体内容。
**Success criteria**: 明确知道要验证什么
### 2. 执行验证
运行相关测试或检查。
**Success criteria**: 测试通过或检查完成
💡 **关键设计**:Claude Code 使用 YAML frontmatter 定义技能元数据,Markdown body 定义技能的具体步骤。这种设计让技能既能被机器解析(有结构化元数据),又对人类友好(自然语言描述)。
1.2 技能类型体系
Claude Code 的技能有两种文件形式:
| 类型 | 文件形式 | 位置 | 示例 |
| **内置技能** | TypeScript (.ts) | `src/skills/bundled/` | batch.ts, simplify.ts |
| **用户技能** | Markdown (.md) | `.claude/skills//SKILL.md` | 用户创建的技能 |
|---|
内置技能 用 TypeScript 编写,编译进 CLI 二进制;用户技能用 Markdown 格式定义。
Claude Code 支持四种技能来源,形成一个分层的能力体系:
1.2.1 Bundled Skills(内置技能)
内置技能编译进 CLI 二进制文件,所有用户开箱即用。位于 src/skills/bundled/ 目录:
src/skills/bundled/
├── index.ts # 注册入口
├── batch.ts # 批量处理
├── claudeApi.ts # Claude API 操作
├── debug.ts # 调试技能
├── keybindings.ts # 快捷键管理
├── loop.ts # 循环执行
├── loremIpsum.ts # 占位内容生成
├── remember.ts # 记忆技能
├── scheduleRemoteAgents.ts # 远程代理调度
├── simplify.ts # 简化技能
├── skillify.ts # ★ 技能化(将过程捕获为技能)
├── stuck.ts # 卡住时建议
├── updateConfig.ts # 配置更新
└── verify.ts # ★ 验证技能
注册模式 (src/skills/bundled/verify.ts):
export function registerVerifySkill(): void {
if (process.env.USER_TYPE !== 'ant') {
return // 仅 Anthropic 内部用户可见
}
registerBundledSkill({
name: 'verify',
description: DESCRIPTION,
userInvocable: true,
files: SKILL_FILES, // 提取到磁盘的参考文件
async getPromptForCommand(args) {
const parts: string[] = [SKILL_BODY.trimStart()]
if (args) {
parts.push(`## User Request\n\n${args}`)
}
return [{ type: 'text', text: parts.join('\n\n') }]
},
})
}
1.2.2 File-based Skills(文件技能)
用户通过文件系统定义的技能,位于项目级(.claude/skills/)或用户级(~/.claude/skills/)。
加载流程 (src/skills/loadSkillsDir.ts):
// 只支持目录格式:skill-name/SKILL.md
async function loadSkillsFromSkillsDir(basePath, source) {
const entries = await fs.readdir(basePath)
for (const entry of entries) {
// 必须有 SKILL.md 文件
const skillFilePath = join(basePath, entry.name, 'SKILL.md')
const content = await fs.readFile(skillFilePath, { encoding: 'utf-8' })
// 解析 frontmatter
const { frontmatter, content: markdownContent } = parseFrontmatter(content)
// 创建技能命令
const skill = createSkillCommand({
skillName: entry.name,
markdownContent,
source,
...parseSkillFrontmatterFields(frontmatter, markdownContent)
})
}
}
1.2.3 MCP Skills(MCP 技能)
来自 Model Context Protocol 服务器的技能,通过 /mcp 命令管理。
💡 **安全设计**:MCP 技能是远程来源,源码中明确标记为"不可信",禁止执行内联 shell 命令(`` !`...` `` 语法)。
1.2.4 Plugin Skills(插件技能)
来自第三方插件的技能,遵循插件市场协议。
1.2.5 技能来源优先级
在 src/components/skills/SkillsMenu.tsx 中,技能按来源分组显示:
const groups = {
policySettings: [], // 策略设置(托管)
userSettings: [], // 用户设置 (~/.claude)
projectSettings: [], // 项目设置 (.claude)
localSettings: [], // 本地设置
flagSettings: [], // Feature Flag 控制
plugin: [], // 插件技能
mcp: [] // MCP 技能
}
1.3 技能的加载与发现机制
1.3.1 技能加载入口
技能在 Claude Code 启动时通过 getSlashCommandToolSkills() 一次性加载,并被 memoize 缓存:
// src/commands.ts
export const getSlashCommandToolSkills = memoize(
async (cwd: string): Promise => {
try {
const allCommands = await getCommands(cwd)
return allCommands.filter(
cmd =>
cmd.type === 'prompt' && // 必须是 prompt 类型
cmd.source !== 'builtin' && // 不是内置命令
(cmd.hasUserSpecifiedDescription || cmd.whenToUse) && // 有描述或触发条件
(cmd.loadedFrom === 'skills' ||
cmd.loadedFrom === 'plugin' ||
cmd.loadedFrom === 'bundled' ||
cmd.disableModelInvocation), // 来自可信来源
)
} catch (error) {
logError(toError(error))
return [] // 技能加载失败不影响系统
}
},
)
1.3.2 技能加载时机
在 src/QueryEngine.ts 中,技能在系统初始化消息中传递给模型:
// QueryEngine.ts
headlessProfilerCheckpoint('before_skills_plugins')
const [skills, { enabled: enabledPlugins }] = await Promise.all([
getSlashCommandToolSkills(getCwd()),
loadAllPluginsCacheOnly(),
])
headlessProfilerCheckpoint('after_skills_plugins')
yield buildSystemInitMessage({
tools,
mcpClients,
model: mainLoopModel,
commands,
agents,
skills, // ★ 技能列表传递
plugins: enabledPlugins,
fastMode: initialAppState.fastMode,
})
1.3.3 技能的延迟加载
注意:getPromptForCommand 是异步函数 ,技能的实际内容是按需加载的。这意味着:
-
启动时只加载技能元数据(名称、描述、触发条件)
-
技能内容在首次调用时才从磁盘读取
-
支持 `files` 字段将参考文件提取到磁盘
// src/skills/bundledSkills.ts
async getPromptForCommand(args, ctx) {
// 延迟提取参考文件(如果需要)
if (files) {
extractionPromise ??= extractBundledSkillFiles(name, files)
const extractedDir = await extractionPromise
// ...
}// 实际读取技能内容 const blocks = await definition.getPromptForCommand(args, ctx) return prependBaseDir(blocks, extractedDir)}
1.3.4 Token 估算策略
技能使用 frontmatter 估算 Token,而不是完整内容:
// src/skills/loadSkillsDir.ts
export function estimateSkillFrontmatterTokens(skill: Command): number {
const frontmatterText = [skill.name, skill.description, skill.whenToUse]
.filter(Boolean)
.join(' ')
return roughTokenCountEstimation(frontmatterText)
}
💡 **设计价值**:启动时不需要加载完整技能内容,只根据 frontmatter 估算 Token,用于预算计算。
1.4 技能的高级属性与调用控制
1.4.1 完整属性清单
除了核心字段外,技能还有以下重要属性(src/types/command.ts):
| 属性 | 类型 | 含义 |
| `isEnabled` | `() => boolean` | 动态控制技能是否显示 |
| `aliases` | `string[]` | 技能的别名列表 |
| `kind` | `'workflow'` | 标记为工作流类型 |
| `immediate` | `boolean` | 不等待队列,立即执行 |
| `isSensitive` | `boolean` | 参数在历史中隐藏 |
| `version` | `string` | 技能版本号 |
|---|
1.4.2 isEnabled:技能的动态可见性
技能可以动态决定是否显示,根据系统状态动态控制:
// src/skills/bundled/remember.ts
registerBundledSkill({
name: 'remember',
isEnabled: () => isAutoMemoryEnabled(), // 依赖配置动态决定
})
// src/skills/bundled/keybindings.ts
registerBundledSkill({
name: 'keybindings',
isEnabled: isKeybindingCustomizationEnabled, // 依赖 Feature Flag
})
// src/skills/bundled/loop.ts
registerBundledSkill({
name: 'loop',
isEnabled: isKairosCronEnabled, // 依赖 KAIROS 功能开启状态
})
💡 **设计价值**:这使得技能的可见性可以与用户配置、功能开关、订阅状态等动态绑定,而不是简单的静态开关。
1.4.3 userInvocable vs disableModelInvocation
这两个标志组合出4 种调用权限:
| userInvocable | disableModelInvocation | 效果 |
| `true` | `false` | 用户和模型都能调用(默认行为) |
| `true` | `true` | 用户能调用,模型不能(batch、skillify) |
| `false` | `false` | 只有模型能调用(内部技能) |
| `false` | `true` | 禁止调用 |
|---|
// src/skills/bundled/batch.ts - 用户可调用,模型禁止
registerBundledSkill({
name: 'batch',
userInvocable: true,
disableModelInvocation: true, // 模型不能自动调用此技能
})
// src/skills/bundled/keybindings.ts - 只有模型能调用
registerBundledSkill({
name: 'keybindings',
userInvocable: false, // 用户在 /help 中看不到这个技能
})
1.4.4 aliases:技能的别名机制
技能可以定义多个名称触发同一个技能:
// 内置技能中使用
// src/skills/bundled/keybindings.ts
registerBundledSkill({
name: 'keybindings',
aliases: ['keys', 'shortcuts'], // 用户可以用 /keys 或 /shortcuts 触发
})
1.4.5 kind: 'workflow' 工作流标记
// src/types/command.ts
kind?: 'workflow' // Distinguishes workflow-backed commands (badaged in autocomplete)
// src/commands.ts - 显示时添加 (workflow) 标记
if (cmd.kind === 'workflow') {
return `${cmd.description} (workflow)`
}
1.4.6 immediate:立即执行模式
// src/types/command.ts
immediate?: boolean // If true, command executes immediately
// without waiting for a stop point (bypasses queue)
设置 immediate: true 的技能会跳过命令队列,立即执行。
1.4.7 isSensitive:敏感信息保护
// src/types/command.ts
isSensitive?: boolean // If true, args are redacted from the conversation history
当技能处理敏感信息(如密码、密钥)时,可以设置此标记来隐藏参数。
第二章:技能的执行架构
2.1 SkillTool:技能的统一入口
所有技能通过 SkillTool(一个 AI Tool)执行。这确保了:
- 统一的接口(`{ skill: string, args?: string }`)
- 集中的权限检查
- 完整的遥测追踪
工具定义 (src/tools/SkillTool/SkillTool.ts):
export const inputSchema = lazySchema(() =>
z.object({
skill: z.string().describe('The skill name. E.g., "verify", "review"'),
args: z.string().optional().describe('Optional arguments for the skill'),
}),
)
export const outputSchema = lazySchema(() =>
z.union([
// Inline 输出的 schema
inlineOutputSchema,
// Forked 输出的 schema
forkedOutputSchema,
])
)
2.1.1 Inline 输出 Schema
const inlineOutputSchema = z.object({
success: z.boolean().describe('Whether the skill is valid'),
commandName: z.string().describe('The name of the skill'),
allowedTools: z
.array(z.string())
.optional()
.describe('Tools allowed by this skill'),
model: z.string().optional().describe('Model override if specified'),
status: z.literal('inline').optional().describe('Execution status'),
})
2.1.2 Forked 输出 Schema
const forkedOutputSchema = z.object({
success: z.boolean().describe('Whether the skill completed successfully'),
commandName: z.string().describe('The name of the skill'),
status: z.literal('forked').describe('Execution status'),
agentId: z.string().describe('The ID of the sub-agent that executed the skill'),
result: z.string().describe('The result from the forked skill execution'),
})
2.2 执行流程图
模型调用 SkillTool({ skill: "verify", args: "检查登录功能" })
│
▼
validateInput() ──── 验证技能是否存在
│ │
│ ├─ 技能名称规范化(去除前导 /)
│ ├─ 远程技能处理(ant-only 实验性功能)
│ └─ 命令查找
│
▼
checkPermissions() ── 权限检查
│
│ ├─ deny 规则检查
│ ├─ allow 规则检查
│ ├─ 安全属性自动放行
│ └─ 需要用户授权
│
▼
call() ────────────── 执行技能
│
├── context === 'fork'
│ │
│ ▼
│ executeForkedSkill()
│ │
│ ├─ 创建子代理 (createAgentId)
│ ├─ 准备 fork 上下文 (prepareForkedCommandContext)
│ ├─ 运行子代理 (runAgent)
│ └─ 返回 { success, agentId, result }
│
└── context === 'inline'
│
▼
processPromptSlashCommand()
│
├─ 解析技能内容
├─ 替换参数 (${ARGUMENTS})
├─ 执行 shell 命令 (!`...`)
├─ 注册技能钩子
└─ 返回处理后的消息
2.3 Inline 执行模式
Inline 执行在当前对话中运行技能,技能内容被展开为用户消息:
// processPromptSlashCommand 处理
const processedCommand = await processPromptSlashCommand(
commandName,
args || '',
commands,
context,
)
// 返回的消息被注入到对话中
return {
data: { success: true, commandName, allowedTools, model },
newMessages: processedCommand.messages, // ★ 展开为消息
contextModifier(ctx) {
// 修改工具权限
},
}
2.4 Fork 执行模式
Fork 执行在独立子代理中运行,有自己的上下文和 token 预算:
// executeForkedSkill
async function executeForkedSkill(
command: Command & { type: 'prompt' },
commandName: string,
args: string | undefined,
context: ToolUseContext,
) {
const agentId = createAgentId() // 创建独立代理
// 准备 fork 上下文
const { modifiedGetAppState, baseAgent, promptMessages, skillContent } =
await prepareForkedCommandContext(command, args || '', context)
// 运行子代理
for await (const message of runAgent({
agentDefinition: baseAgent,
promptMessages,
toolUseContext: { ...context, getAppState: modifiedGetAppState },
isAsync: false,
model: command.model,
override: { agentId },
})) {
agentMessages.push(message)
// 报告进度...
}
return {
data: {
success: true,
commandName,
status: 'forked',
agentId,
result: extractResultText(agentMessages),
},
}
}
💡 **Fork 的价值**:子代理有独立的 token 计数和上下文管理,不会耗尽主对话的上下文空间。适合耗时的验证任务、复杂的重构工作流。
2.5 防止重复调用:COMMAND_NAME_TAG 机制
2.5.1 标签的作用
当技能被调用时,一个特殊标签会被插入到对话中:
// SkillTool prompt.ts
export const getPrompt = () => `
...
- If you see a <${COMMAND_NAME_TAG}> tag in the current conversation turn,
the skill has ALREADY been loaded - follow the instructions directly
instead of calling this tool again
...
`
2.5.2 防止重复调用的逻辑
// 在 processSlashCommand 中
if (isAlreadyProcessing) {
// 如果当前已经在处理,不要重复处理
return { messages: [], shouldQuery: false }
}
💡 **设计价值**:避免模型在同一个对话轮次中重复调用同一个技能,导致死循环或资源浪费。
2.6 技能调用时的进度报告
2.6.1 Fork 模式下的进度追踪
// src/tools/SkillTool/SkillTool.ts
// 当子代理产生工具调用时,报告进度
if (
(message.type === 'assistant' || message.type === 'user') &&
onProgress
) {
const hasToolContent = m.message.content.some(
c => c.type === 'tool_use' || c.type === 'tool_result',
)
if (hasToolContent) {
onProgress({
toolUseID: `skill_${parentMessage.message.id}`,
data: {
message: m,
type: 'skill_progress',
prompt: skillContent,
agentId,
},
})
}
}
2.6.2 进度报告的类型
type SkillProgress = {
toolUseID: string // 工具使用 ID
data: {
message: Message // 包含工具调用的消息
type: 'skill_progress' // 进度类型标识
prompt: string // 技能内容
agentId: string // 子代理 ID
}
}
2.7 错误码系统
SkillTool 使用详细的错误码:
| 错误码 | 含义 |
| 1 | Invalid skill format (空或无效) |
| 2 | Unknown skill (技能不存在) |
| 4 | disableModelInvocation (禁止模型调用) |
| 5 | Not a prompt-based skill (非 prompt 类型) |
| 6 | Remote skill not discovered (远程技能未发现) |
|---|
async validateInput({ skill }, context): Promise {
const trimmed = skill.trim()
if (!trimmed) {
return { result: false, message: `Invalid skill format: ${skill}`, errorCode: 1 }
}
// Get available commands (including MCP skills)
const commands = await getAllCommands(context)
// Check if command exists
const foundCommand = findCommand(normalizedCommandName, commands)
if (!foundCommand) {
return { result: false, message: `Unknown skill: ${normalizedCommandName}`, errorCode: 2 }
}
// Check if command has model invocation disabled
if (foundCommand.disableModelInvocation) {
return {
result: false,
message: `Skill ${normalizedCommandName} cannot be used with ${SKILL_TOOL_NAME} tool`,
errorCode: 4,
}
}
// Check if command is a prompt-based command
if (foundCommand.type !== 'prompt') {
return {
result: false,
message: `Skill ${normalizedCommandName} is not a prompt-based skill`,
errorCode: 5,
}
}
return { result: true }
}
2.8 技能的参数与模板系统
2.8.1 参数定义
技能支持在 frontmatter 中定义参数:
---
name: cherry-pick
description: 将 PR cherry-pick 到目标分支
argument-hint: "${PR号码} 到 ${目标分支}"
arguments:
- PR号码
- 目标分支
---
# Cherry-pick Skill
## Steps
### 1. 获取 PR 信息
使用 `$PR号码` 获取 PR 详情。
2.8.2 参数替换
在 src/utils/argumentSubstitution.ts 中实现:
// substituteArguments
finalContent = substituteArguments(
finalContent,
args,
true, // allowMissing
argumentNames, // ["PR号码", "目标分支"]
)
// 参数格式:$参数名 或 ${参数名}
2.8.3 内置变量
技能内容中可使用内置变量:
| 变量 | 含义 |
| `{CLAUDE_SKILL_DIR}\` | 技能目录路径 |
| \`{CLAUDE_SESSION_ID}` | 当前会话 ID |
| `$ARGUMENTS` | 所有参数的拼接 |
| `1, 2, ...` | 按位置引用参数 |
|---|
第三章:权限与安全模型
3.1 技能权限检查流程
权限检查在 checkPermissions() 中分多层进行:
// 1. deny 规则检查(优先级最高)
const denyRules = getRuleByContentsForTool(permissionContext, SkillTool, 'deny')
for (const [ruleContent, rule] of denyRules) {
if (ruleMatches(ruleContent)) {
return { behavior: 'deny', ... }
}
}
// 2. 特定用户组的自动放行
if (isRemoteCanonicalSkill) {
return { behavior: 'allow', ... }
}
// 3. allow 规则检查
const allowRules = getRuleByContentsForTool(permissionContext, SkillTool, 'allow')
for (const [ruleContent, rule] of allowRules) {
if (ruleMatches(ruleContent)) {
return { behavior: 'allow', ... }
}
}
// 4. 安全属性检查(自动放行)
if (skillHasOnlySafeProperties(command)) {
return { behavior: 'allow', ... }
}
// 5. 默认:询问用户
return { behavior: 'ask', suggestions: [...] }
3.1.1 规则匹配
规则支持精确匹配和前缀匹配:
const ruleMatches = (ruleContent: string): boolean => {
// Normalize rule content by stripping leading slash
const normalizedRule = ruleContent.startsWith('/')
? ruleContent.substring(1)
: ruleContent
// Check exact match (using normalized commandName)
if (normalizedRule === commandName) {
return true
}
// Check prefix match (e.g., "review:*" matches "review-pr 123")
if (normalizedRule.endsWith(':*')) {
const prefix = normalizedRule.slice(0, -2)
return commandName.startsWith(prefix)
}
return false
}
3.2 SAFE_SKILL_PROPERTIES 白名单
3.2.1 安全属性白名单
// src/tools/SkillTool/SkillTool.ts
const SAFE_SKILL_PROPERTIES = new Set([
// PromptCommand properties
'type',
'progressMessage',
'contentLength',
'argNames',
'model',
'effort',
'source',
'pluginInfo',
'disableNonInteractive',
'skillRoot',
'context',
'agent',
'getPromptForCommand',
'frontmatterKeys',
// CommandBase properties
'name',
'description',
'hasUserSpecifiedDescription',
'isEnabled',
'isHidden',
'aliases',
'isMcp',
'argumentHint',
'whenToUse',
'paths',
'version',
'disableModelInvocation',
'userInvocable',
'loadedFrom',
'immediate',
'userFacingName',
])
3.2.2 自动授权的条件
function skillHasOnlySafeProperties(command: Command): boolean {
for (const key of Object.keys(command)) {
if (SAFE_SKILL_PROPERTIES.has(key)) {
continue
}
// 不在白名单中的属性,检查是否有意义
const value = command[key]
if (value === undefined || value === null) {
continue // undefined/null 视为安全
}
if (Array.isArray(value) && value.length === 0) {
continue // 空数组视为安全
}
// 有意义的非安全属性 → 需要用户授权
return false
}
return true // 全部安全,自动放行
}
💡 **设计价值**:当新的属性被添加到 PromptCommand 或 CommandBase 时,默认需要授权,直到明确被加入白名单。这是一种防御性设计。
3.3 MCP 技能的安全隔离
MCP 技能是远程来源,源码中明确禁止执行内联 shell 命令 (!`...`) 和 ${CLAUDE_SKILL_DIR} 变量替换。
// src/skills/loadSkillsDir.ts
if (loadedFrom !== 'mcp') {
finalContent = await executeShellCommandsInPrompt(finalContent, ...)
}
3.3.1 MCP 命令的特殊解析
MCP 技能有特殊的命令格式(src/utils/slashCommandParsing.ts):
// 格式:/mcp:tool (MCP) arg1 arg2
export function parseSlashCommand(input: string): ParsedSlashCommand | null {
const trimmedInput = input.trim()
if (!trimmedInput.startsWith('/')) {
return null
}
const withoutSlash = trimmedInput.slice(1)
const words = withoutSlash.split(' ')
let commandName = words[0]
let isMcp = false
let argsStartIndex = 1
// 检查第二个词是否是 (MCP)
if (words.length > 1 && words[1] === '(MCP)') {
commandName = commandName + ' (MCP)'
isMcp = true
argsStartIndex = 2
}
const args = words.slice(argsStartIndex).join(' ')
return { commandName, args, isMcp }
}
3.4 formatDescriptionWithSource:技能的来源标注
技能在显示时会根据来源自动添加标注(src/commands.ts):
export function formatDescriptionWithSource(cmd: Command): string {
if (cmd.type !== 'prompt') {
return cmd.description
}
// 工作流类型
if (cmd.kind === 'workflow') {
return `${cmd.description} (workflow)`
}
// 插件技能
if (cmd.source === 'plugin') {
const pluginName = cmd.pluginInfo?.pluginManifest.name
if (pluginName) {
return `(${pluginName}) ${cmd.description}`
}
return `${cmd.description} (plugin)`
}
// 内置技能
if (cmd.source === 'bundled') {
return `${cmd.description} (bundled)`
}
// 其他来源
return `${cmd.description} (${getSettingSourceName(cmd.source)})`
}
3.4.1 显示效果示例
| 技能来源 | 显示效果 |
| Bundled | `Verify code changes (bundled)` |
| Plugin | `(Anthropic Plugin) Description` |
| Workflow | `Complex workflow (workflow)` |
| MCP | `MCP skill description (mcp)` |
| File | `Custom skill (project)` |
|---|
第四章:Hooks 系统
4.1 钩子事件体系
系统定义了 28 种钩子事件(src/entrypoints/sdk/coreSchemas.ts):
| 类别 | 事件 |
| **工具执行** | `PreToolUse` - 工具执行前 |
| | `PostToolUse` - 工具执行后 |
| | `PostToolUseFailure` - 工具执行失败 |
| **会话生命周期** | `SessionStart` - 会话开始 |
| | `SessionEnd` - 会话结束 |
| | `Setup` - 初始化完成 |
| **代理事件** | `SubagentStart` / `SubagentStop` |
| | `TeammateIdle` - 队友空闲 |
| **任务事件** | `TaskCreated` - 任务创建 |
| | `TaskCompleted` - 任务完成 |
| **用户交互** | `UserPromptSubmit` - 用户提交输入 |
| | `Elicitation` / `ElicitationResult` - 信息请求 |
| | `Notification` - 通知 |
| **Compact 与权限** | `PreCompact` / `PostCompact` |
| | `PermissionRequest` / `PermissionDenied` |
| | `Stop` / `StopFailure` |
| | `ConfigChange` - 配置变更 |
| **文件系统** | `WorktreeCreate` / `WorktreeRemove` |
| | `CwdChanged` - 工作目录变更 |
| | `FileChanged` - 文件变更 |
| **指令** | `InstructionsLoaded` - 指令加载完成 |
|---|
4.2 四种钩子类型
每种钩子支持四种执行方式(src/schemas/hooks.ts):
// 1. Bash 命令钩子
{
type: 'command',
command: 'echo "Tool: $TOOL_NAME"',
if: 'Bash(git *)', // 条件过滤
shell: 'bash', // shell 类型
timeout: 30, // 超时(秒)
statusMessage: 'Running hook...',
once: false, // 是否只执行一次
async: false, // 是否异步执行
}
// 2. LLM 提示词钩子
{
type: 'prompt',
prompt: 'Should we allow $ARGUMENTS? Reply yes or no.',
if: 'Write(*.env)',
model: 'claude-haiku-4',
timeout: 60,
}
// 3. HTTP 钩子
{
type: 'http',
url: 'https://webhook.example.com/notify',
if: 'Bash(rm *)',
headers: { 'Authorization': 'Bearer $WEBHOOK_TOKEN' },
allowedEnvVars: ['WEBHOOK_TOKEN'],
timeout: 10,
}
// 4. Agent 验证钩子
{
type: 'agent',
prompt: 'Verify that unit tests ran and passed.',
if: 'Bash(npm test:*)',
model: 'claude-sonnet-4-6',
timeout: 120,
}
4.3 技能钩子的注册流程
// src/utils/hooks/registerSkillHooks.ts
export function registerSkillHooks(
setAppState,
sessionId: string,
hooks: HooksSettings,
skillName: string,
skillRoot?: string,
): void {
for (const eventName of HOOK_EVENTS) {
const matchers = hooks[eventName]
if (!matchers) continue
for (const matcher of matchers) {
for (const hook of matcher.hooks) {
// once: true 的钩子执行后自动移除
const onHookSuccess = hook.once
? () => removeSessionHook(setAppState, sessionId, eventName, hook)
: undefined
addSessionHook(
setAppState,
sessionId,
eventName,
matcher.matcher || '',
hook,
onHookSuccess,
skillRoot,
)
}
}
}
}
4.4 条件匹配语法
钩子的 if 字段使用权限规则语法:
// 工具名称匹配
if: 'Bash(git *)' // 所有 git 命令
if: 'Write(*.env)' // 写入 .env 文件
if: 'Read(*.ts)' // 读取 TypeScript 文件
// 支持 * 和 ? 通配符
if: 'Bash(rm -rf *)' // 所有 rm -rf 命令(危险操作)
4.5 技能的 Hooks 前置声明
技能可以在 frontmatter 中直接声明 Hooks:
// src/skills/loadSkillsDir.ts
if (!frontmatter.hooks) {
return undefined
}
// 使用 HooksSchema 验证
const result = HooksSchema().safeParse(frontmatter.hooks)
if (!result.success) {
logForDebugging(`Invalid hooks in skill '${skillName}': ${result.error.message}`)
return undefined
}
return result.data
示例:
---
name: my-workflow
hooks:
PostToolUse:
- if: "Bash(npm test)"
type: prompt
prompt: "Should we add coverage reporting?"
---
5.1 条件技能机制:基于文件路径的动态激活
5.1.1 条件技能的核心概念
条件技能是 Claude Code 最强大的特性之一:技能可以根据用户当前工作的文件类型自动激活,而不需要用户显式调用。
---
name: react-component-review
description: React 组件审查技能
paths:
- "**/*.tsx"
- "**/*.jsx"
---
# React Component Review Skill
当你编辑 React 组件时自动激活此技能。
执行代码审查,确保遵循 React 最佳实践。
5.1.2 paths frontmatter 的解析
条件技能使用与 CLAUDE.md 相同的 paths 格式(src/skills/loadSkillsDir.ts):
// 解析 paths frontmatter
function parseSkillPaths(frontmatter: FrontmatterData): string[] | undefined {
if (!frontmatter.paths) {
return undefined
}
const patterns = splitPathInFrontmatter(frontmatter.paths)
.map(pattern => {
// 移除 /** 后缀 - ignore 库会把 'path' 当作匹配 path 和 path/**
return pattern.endsWith('/**') ? pattern.slice(0, -3) : pattern
})
.filter((p: string) => p.length > 0)
// 如果全是 ** (match-all),视为无限制
if (patterns.length === 0 || patterns.every((p: string) => p === '**')) {
return undefined
}
return patterns
}
5.1.3 条件技能的激活流程
// 当文件被访问时,触发条件技能的激活检查
export function activateConditionalSkillsForPaths(
filePaths: string[],
cwd: string,
): string[] {
for (const [name, skill] of conditionalSkills) {
if (skill.type !== 'prompt' || !skill.paths || skill.paths.length === 0) {
continue
}
const skillIgnore = ignore().add(skill.paths)
for (const filePath of filePaths) {
// 计算相对路径
const relativePath = isAbsolute(filePath)
? relative(cwd, filePath)
: filePath
// 检查路径是否匹配
if (skillIgnore.ignores(relativePath)) {
// 激活技能:移动到动态技能列表
dynamicSkills.set(name, skill)
conditionalSkills.delete(name)
activatedConditionalSkillNames.add(name)
break
}
}
}
}
5.1.4 gitignore 安全边界
动态发现的技能目录会被 gitignore 检查保护:
// 检查目录是否被 gitignored
if (await isPathGitignored(currentDir, resolvedCwd)) {
logForDebugging(`[skills] Skipped gitignored skills dir: ${skillDir}`)
continue
}
💡 **设计价值**:防止 node_modules 或其他被版本控制忽略的目录中的技能被意外加载。
5.2 动态技能发现:按需加载架构
5.2.1 动态发现的触发时机
技能可以在用户工作时动态发现和加载,不需要重启 Claude Code:
用户编辑 /project/src/components/Button.tsx
│
▼
discoverSkillDirsForPaths([filePath], cwd)
│
▼
遍历文件的父目录,查找 .claude/skills/
│
▼
/project/.claude/skills/ ← CWD 级别(启动时加载)
/project/src/.claude/skills/ ← 新发现!
/project/src/components/.claude/skills/ ← 新发现!
5.2.2 目录遍历算法
export async function discoverSkillDirsForPaths(
filePaths: string[],
cwd: string,
): Promise {
const resolvedCwd = cwd.endsWith(pathSep) ? cwd.slice(0, -1) : cwd
const newDirs: string[] = []
for (const filePath of filePaths) {
// 从文件的父目录开始
let currentDir = dirname(filePath)
// 向上遍历到 cwd(不包括 cwd 本身)
while (currentDir.startsWith(resolvedCwd + pathSep)) {
const skillDir = join(currentDir, '.claude', 'skills')
// 避免重复检查(缓存未命中的目录)
if (!dynamicSkillDirs.has(skillDir)) {
dynamicSkillDirs.add(skillDir)
try {
await fs.stat(skillDir)
// 目录存在,检查是否被 gitignored
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.3 动态技能的加载与合并
export async function loadDynamicSkillsForDirs(
dirs: string[],
): Promise {
for (const dir of dirs) {
const skills = await loadSkillsFromSkillsDir(dir, 'project')
for (const skill of skills) {
dynamicSkills.set(skill.name, skill) // 合并到动态技能
}
}
}
5.2.4 技能优先级的层级结构
技能来源优先级(从高到低):
│
1. Managed Skills(托管技能)
│
2. Project Skills(项目技能)
│ └─ 动态发现的嵌套项目技能
│
3. User Skills(用户技能)
│
4. Additional Skills(额外技能)
│
5. Legacy Commands(遗留命令)
5.3 技能预算系统:上下文窗口的精细管控
5.3.1 预算分配策略
Claude Code 模型看到的技能列表有严格的上下文窗口限制:
// 技能列表占用上下文窗口的 1%
export const SKILL_BUDGET_CONTEXT_PERCENT = 0.01
export const CHARS_PER_TOKEN = 4
export const DEFAULT_CHAR_BUDGET = 8_000 // 200k × 4 × 1%
// 每个技能描述的最大长度
export const MAX_LISTING_DESC_CHARS = 250
5.3.2 截断策略
function formatCommandsWithinBudget(commands: Command[]): string {
const budget = getCharBudget(contextWindowTokens)
// 1. 先尝试完整描述
const fullTotal = fullEntries.reduce((sum, e) => sum + stringWidth(e.full), 0)
if (fullTotal <= budget) {
return fullEntries.map(e => e.full).join('\n')
}
// 2. 内置技能永远保留完整描述
const bundledIndices = new Set()
for (let i = 0; i < commands.length; i++) {
if (cmd.type === 'prompt' && cmd.source === 'bundled') {
bundledIndices.add(i)
}
}
// 3. 非内置技能的描述被截断以适应预算
const remainingBudget = budget - bundledChars
const maxDescLen = Math.floor(remainingBudget / restCommands.length)
if (maxDescLen < MIN_DESC_LENGTH) {
// 极端情况:非内置技能只保留名称
return commands.map((cmd, i) =>
bundledIndices.has(i) ? fullEntries[i]!.full : `- ${cmd.name}`,
).join('\n')
}
}
5.3.3 截断模式
| 模式 | 条件 | 效果 |
| `names_only` | 预算极度紧张 | 只有内置技能保留描述,其他只显示名称 |
| `description_trimmed` | 正常截断 | 所有技能描述都被截断到 maxDescLen |
|---|
5.3.4 技能描述的截断遥测
// src/tools/SkillTool/prompt.ts
if (process.env.USER_TYPE === 'ant') {
logEvent('tengu_skill_descriptions_truncated', {
skill_count: commands.length,
budget,
full_total: fullTotal,
truncation_mode:
'names_only' | 'description_trimmed',
max_desc_length: maxDescLen,
bundled_count: bundledIndices.size,
bundled_chars: bundledChars,
})
}
5.4 Compact 时的技能持久化预算
5.4.1 Token 预算定义
// src/services/compact/compact.ts
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件
export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 // 每技能
export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 // 技能总预算
5.4.2 技能的保留策略
// 按最近调用时间排序,优先保留最近的技能
const skills = Array.from(invokedSkills.values())
.sort((a, b) => b.invokedAt - a.invokedAt) // 最近优先
.map(skill => ({
name: skill.skillName,
path: skill.skillPath,
content: truncateToTokens(
skill.content,
POST_COMPACT_MAX_TOKENS_PER_SKILL, // 每个技能最多 5k tokens
),
}))
.filter(skill => {
const tokens = roughTokenCountEstimation(skill.content)
if (usedTokens + tokens > POST_COMPACT_SKILLS_TOKEN_BUDGET) {
return false // 超出 25k 预算则丢弃
}
usedTokens += tokens
return true
})
💡 **设计价值**:上下文压缩时,优先保留最近使用的技能,确保重要技能在压缩后仍可用。
5.5 技能调用追踪与状态管理
5.5.1 InvokedSkillInfo 数据结构
Claude Code 使用 InvokedSkillInfo 追踪每个技能调用:
// src/bootstrap/state.ts
export type InvokedSkillInfo = {
skillName: string
skillPath: string
content: string // 技能完整内容
invokedAt: number // 调用时间戳
agentId: string | null // 所属代理(null = 主会话)
}
5.5.2 技能调用的注册
// src/tools/SkillTool/SkillTool.ts
addInvokedSkill(
commandName, // 技能名称
skillPath, // 技能路径
skillContent, // 技能内容
getAgentContext()?.agentId ?? null // 代理 ID
)
5.5.3 按代理隔离的技能历史
// src/bootstrap/state.ts
export function getInvokedSkillsForAgent(
agentId: string | undefined | null,
): Map {
const normalizedId = agentId ?? null
const filtered = new Map()
for (const [key, skill] of STATE.invokedSkills) {
if (skill.agentId === normalizedId) {
filtered.set(key, skill)
}
}
return filtered
}
💡 **设计价值**:确保技能不会跨代理泄露。主会话调用的技能不会出现在子代理的技能历史中,反之亦然。
5.6 远程规范技能(实验性)
5.6.1 远程技能的执行流程
// src/tools/SkillTool/SkillTool.ts
// 远程规范技能是 ant-only 实验性功能
if (
feature('EXPERIMENTAL_SKILL_SEARCH') &&
process.env.USER_TYPE === 'ant'
) {
const slug = remoteSkillModules!.stripCanonicalPrefix(commandName)
if (slug !== null) {
return executeRemoteSkill(slug, commandName, parentMessage, context)
}
}
5.6.2 远程技能的遥测
logEvent('tengu_skill_tool_invocation', {
command_name: 'remote_skill',
_PROTO_skill_name: commandName,
execution_context: 'remote', // 远程执行
was_discovered: true, // 始终为 true
is_remote: true,
remote_cache_hit: cacheHit,
remote_load_latency_ms: latencyMs,
})
6. batch 与 simplify:技能协作范例
6.1 batch 技能:并行工作流协调器
源码 :src/skills/bundled/batch.ts
功能:批量并行工作流协调器
执行流程:
用户调用 /batch
│
▼
Phase 1: 研究和规划(Plan Mode)
│
├── 理解范围,启动子代理研究
├── 分解为 5-30 个独立单元
├── 确定 e2e 测试方案
└── 等待用户批准
│
▼
Phase 2: 启动并行工作者
│
└── 每个工作者在独立 git worktree 中工作
│
├── 实现代码
├── 调用 simplify 审查
├── 运行测试
├── 提交并创建 PR
└── 报告 PR URL
│
▼
Phase 3: 追踪进度
│
└── 协调者汇总所有 PR
关键设计:
- 使用 `isolation: "worktree"` 确保每个工作者在独立 git worktree 中工作
- `run_in_background: true` 实现真正的并行执行
- 协调者启动所有工作者后,等待完成通知
6.2 simplify 技能:代码审查
源码 :src/skills/bundled/simplify.ts
功能:代码复用、质量、效率的3并行代理审查
执行流程:
用户调用 /simplify(或被 batch 触发)
│
▼
启动 3 个并行子代理
│
├── 代码复用审查
├── 代码质量审查
└── 代码效率审查
│
▼
等待 3 个代理完成
│
▼
聚合结果,直接修复问题
6.3 batch 与 simplify 的协作关系
batch.ts 第 13 行的指令:
const WORKER_INSTRUCTIONS = `After you finish implementing the change:
1. **Simplify** --- Invoke the \`${SKILL_TOOL_NAME}\` tool with \`skill: "simplify"\` to review and clean up your changes.
2. **Run unit tests** ...
3. **Test end-to-end** ...
4. **Commit and push** ...
5. **Report** --- End with a single line: \`PR: \`
`
关键点 :这不是平台级的"技能链"机制,而是 prompt 约定:
| 机制 | 实现方式 |
| **Prompt 约定** | 在 prompt 里写"完成后调用 simplify" |
| **Superpowers 方式** | 技能文档里声明 `REQUIRED SUB-SKILL`,AI 模型自读并决定调用(无平台解析) |
|---|
batch 的工作流程:
-
工作者完成实现
-
看到 prompt 指令:"调用 Simplify 审查代码"
-
工作者决定调用
SkillTool(skill: "simplify") -
simplify 技能启动 3 个并行审查代理
这是模型自己决定调用,不是平台自动触发。
6.4 技能协作的两种模式
| 模式 | 实现 | 例子 |
| **Prompt 约定** | prompt 里写"应该调用 xxx" | batch → simplify |
| **声明式约定** | 技能文档里声明 `REQUIRED SUB-SKILL`,AI 模型自读(无平台解析) | Superpowers |
| **独立命令** | 每个步骤是独立命令 | OpenSpec |
|---|
Claude Code 内置技能采用的是 Prompt 约定模式,简单直接,但依赖模型的遵循度。