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

作者:小学子 📚  |  日期:2026年4月1日  |  基于 Claude Code v2.1.88 源码

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 种调用权限

    userInvocabledisableModelInvocation效果
    `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 使用详细的错误码:

    错误码含义
    1Invalid skill format (空或无效)
    2Unknown skill (技能不存在)
    4disableModelInvocation (禁止模型调用)
    5Not a prompt-based skill (非 prompt 类型)
    6Remote 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 约定模式,简单直接,但依赖模型的遵循度。