Claude Code 技能系统深度解析:核心架构

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 的工作流程:

  1. 工作者完成实现

  2. 看到 prompt 指令:"调用 Simplify 审查代码"

  3. 工作者决定调用 SkillTool(skill: "simplify")

  4. simplify 技能启动 3 个并行审查代理

这是模型自己决定调用,不是平台自动触发。

6.4 技能协作的两种模式

| 模式 | 实现 | 例子 |
| **Prompt 约定** | prompt 里写"应该调用 xxx" | batch → simplify |
| **声明式约定** | 技能文档里声明 `REQUIRED SUB-SKILL`,AI 模型自读(无平台解析) | Superpowers |

**独立命令** 每个步骤是独立命令 OpenSpec

Claude Code 内置技能采用的是 Prompt 约定模式,简单直接,但依赖模型的遵循度。

相关推荐
电磁脑机2 小时前
基于分布式电磁场的双体闭环脑机接口体系与场域认知底层理论
分布式·目标跟踪·重构·架构·交互
电磁脑机2 小时前
人类分布式大脑架构与文明、技术、安全的底层逻辑——原创大脑架构理论研究
网络·分布式·神经网络·安全·架构
蒸汽求职2 小时前
低延迟系统优化:针对金融 IT 与高频交易,如何从 CPU 缓存行(Cache Line)对齐展现硬核工程底蕴?
sql·算法·缓存·面试·职场和发展·金融·架构
fe7tQnVan2 小时前
.NET 11 预览版 1 中的新兴架构演进:RISC-V 与 LoongArch 支持的深度技术解析与生态展望
架构·.net·risc-v
G皮T2 小时前
【OpenClaw】思路转变:从 “传统UI测试” 到 “AI驱动的UI测试”
自动化测试·人工智能·ai·agent·测试·ui测试·openclaw
自然语11 小时前
人工智能之数字生命 认知架构白皮书 第7章
人工智能·架构
eastyuxiao11 小时前
如何在不同的机器上运行多个OpenClaw实例?
人工智能·git·架构·github·php
Database_Cool_12 小时前
OpenClaw-Observability:基于 DuckDB 构建 OpenClaw 的全链路可观测体系
数据库·阿里云·ai
集丰照明12 小时前
使用宝塔安装OpenClaw 龙虾教程
ai·宝塔·龙虾