Claude Code Wiki
首页 深入解析 命令系统

命令架构:Command 接口与注册机制

中级 命令系统

Claude Code 的命令系统是一个类型安全、懒加载、多源合并的架构设计,通过统一的 Command 接口将内置命令、用户技能、插件扩展和 MCP 服务整合为一个协调的执行体系。该系统采用** discriminated union** 模式区分三种命令类型(local、local-jsx、prompt),每种类型对应不同的执行语义和返回值处理策略,同时通过 load() 函数实现按需加载,显著降低启动时的依赖图复杂度。

Command 类型系统:三种执行模式

命令系统的核心是 Command 类型定义,它通过 TypeScript 的 discriminated union 将三种不同的执行模式统一在一个类型系统下:

export type Command = CommandBase & 
  (PromptCommand | LocalCommand | LocalJSXCommand)

types/command.ts#L205-L206

每种命令类型都有明确的职责边界和执行特征,下表对比了三种类型的核心差异:

特性维度locallocal-jsxprompt
返回类型LocalCommandResult (text/compact/skip)React.ReactNodeContentBlockParam[] (发送给模型)
执行语义同步计算,立即返回结果渲染交互式 UI 组件扩展为提示词,触发模型推理
适用场景简单查询、状态操作、会话管理配置界面、选择器、向导流程技能、工作流、代码生成任务
模型交互❌ 不触发 API 调用❌ 不触发 API 调用✅ 将提示词发送给模型
示例命令/version, /clear, /cost/help, /model, /config/commit, /review, /init

CommandBase:通用命令元数据

所有命令类型共享的基础元数据定义了命令的标识、可见性和可用性条件:

export type CommandBase = {
  name: string                    // 命令标识符(唯一)
  description: string             // 用户可见描述
  aliases?: string[]              // 命令别名
  isEnabled?: () => boolean       // 动态启用条件(特性开关、环境检查)
  isHidden?: boolean              // 从自动补全和帮助中隐藏
  availability?: CommandAvailability[] // 认证/提供商限制
  argumentHint?: string           // 参数提示文本
  whenToUse?: string              // 详细使用场景说明
  userInvocable?: boolean         // 用户是否可直接调用(默认 true)
  disableModelInvocation?: boolean // 禁止模型通过 SkillTool 调用
  loadedFrom?: 'skills' | 'plugin' | 'bundled' | 'mcp' // 命令来源
  immediate?: boolean             // 立即执行,绕过命令队列
  isSensitive?: boolean           // 参数敏感,在历史记录中脱敏
}

types/command.ts#L175-L200

可用性控制 通过 availability 字段实现基于认证提供商的命令过滤,例如 ['claude-ai', 'console'] 表示仅对 claude.ai 订阅用户和直接 API 用户可见,而对 Bedrock/Vertex 用户隐藏。这与 isEnabled() 形成双层过滤:availability 是静态的身份检查,isEnabled() 是动态的状态检查(如特性开关)。

commands.ts#L417-L443

local 命令:轻量级同步操作

local 命令是最简单的命令类型,用于执行不涉及模型交互的同步操作,如查询状态、清理会话、显示信息等。其执行函数签名如下:

export type LocalCommandCall = (
  args: string,
  context: LocalJSXCommandContext
) => Promise<LocalCommandResult>

export type LocalCommandResult =
  | { type: 'text'; value: string }
  | { type: 'compact'; compactionResult: CompactionResult; displayText?: string }
  | { type: 'skip' }

types/command.ts#L62-L23

返回值的 discriminated union 设计允许命令灵活控制输出行为:

  • text 类型返回纯文本,包装在 <local-command-stdout> 标签中显示
  • compact 类型触发特殊的上下文压缩流程,返回压缩结果
  • skip 类型跳过所有消息生成,用于静默操作

典型实现示例/version 命令展示了 local 命令的最小实现:

const call: LocalCommandCall = async () => {
  return {
    type: 'text',
    value: MACRO.BUILD_TIME
      ? `${MACRO.VERSION} (built ${MACRO.BUILD_TIME})`
      : MACRO.VERSION
  }
}

const version = {
  type: 'local',
  name: 'version',
  description: 'Print the version this session is running',
  isEnabled: () => process.env.USER_TYPE === 'ant',
  supportsNonInteractive: true,
  load: () => Promise.resolve({ call })
} satisfies Command

commands/version.ts#L3-L20

懒加载策略 通过 load() 函数实现,返回包含 call 函数的模块对象。对于简单命令(如 /version),可直接返回 Promise.resolve();对于依赖重模块的命令(如 /clear),则通过 import() 动态加载以减少启动时间:

const clear = {
  type: 'local',
  name: 'clear',
  description: 'Clear conversation history and free up context',
  aliases: ['reset', 'new'],
  load: () => import('./clear.js') // 动态导入,延迟加载依赖
} satisfies Command

commands/clear/index.ts#L10-L17

local-jsx 命令:交互式 UI 组件

local-jsx 命令返回 React 组件,用于渲染交互式界面元素,如配置面板、模型选择器、帮助屏幕等。其执行函数接收 onDone 回调,允许命令在用户交互完成后异步返回结果:

export type LocalJSXCommandCall = (
  onDone: LocalJSXCommandOnDone,
  context: ToolUseContext & LocalJSXCommandContext,
  args: string
) => Promise<React.ReactNode>

export type LocalJSXCommandOnDone = (
  result?: string,
  options?: {
    display?: 'skip' | 'system' | 'user'  // 控制结果显示方式
    shouldQuery?: boolean                 // 是否触发模型查询
    metaMessages?: string[]               // 模型可见但用户不可见的消息
    nextInput?: string                    // 自动填充的下一个输入
    submitNextInput?: boolean             // 是否自动提交 nextInput
  }
) => void

types/command.ts#L131-L126

onDone 回调是 local-jsx 命令的核心控制机制,它允许命令:

  1. 控制显示方式display: 'skip' 跳过所有消息,display: 'system' 使用系统消息样式,display: 'user' 使用用户消息样式
  2. 触发后续操作shouldQuery: true 在命令完成后自动触发模型查询
  3. 注入隐藏上下文:通过 metaMessages 向模型提供额外信息而不在对话历史中显示
  4. 预设下一个输入nextInputsubmitNextInput 实现命令链式调用

执行流程 在命令处理器中通过 Promise 包装 onDone 回调:

case 'local-jsx': {
  return new Promise<SlashCommandResult>(resolve => {
    const onDone = (result?: string, options?: {...}) => {
      // 根据选项构建消息数组
      void resolve({
        messages: [...],  // 根据显示选项构建
        shouldQuery: options?.shouldQuery ?? false,
        command,
        nextInput: options?.nextInput,
        submitNextInput: options?.submitNextInput
      })
    }
    
    void command.load().then(mod => mod.call(onDone, context, args))
      .then(jsx => {
        if (jsx == null) return
        // 将 JSX 组件设置到工具上下文,触发 Ink 渲染
        setToolJSX({
          jsx,
          shouldHidePromptInput: true,
          isLocalJSXCommand: true
        })
      })
  })
}

utils/processUserInput/processSlashCommand.tsx#L551-L656

典型实现示例/help 命令展示了一个简单的 local-jsx 命令:

export const call: LocalJSXCommandCall = async (onDone, { options: { commands } }) => {
  return <HelpV2 commands={commands} onClose={onDone} />
}

commands/help/help.tsx#L4-L10

组件接收 onDone 作为 onClose 回调,当用户关闭帮助界面时触发,完成命令生命周期。

prompt 命令:模型驱动的技能扩展

prompt 命令是最强大的命令类型,它不直接返回结果,而是生成发送给 AI 模型的提示词,由模型执行复杂任务(如代码生成、代码审查、项目初始化等)。这种设计将命令系统从简单的 CLI 工具提升为技能扩展平台

export type PromptCommand = {
  type: 'prompt'
  progressMessage: string           // 加载时显示的进度消息
  contentLength: number             // 提示词长度(用于 token 预算估算)
  source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
  getPromptForCommand(
    args: string,
    context: ToolUseContext
  ): Promise<ContentBlockParam[]>  // 生成提示词的核心方法
  allowedTools?: string[]           // 技能授予的额外工具权限
  model?: string                    // 指定使用的模型
  effort?: EffortValue              // 任务复杂度指示
  context?: 'inline' | 'fork'       // 执行上下文:inline=当前会话,fork=子代理
  agent?: string                    // fork 模式下使用的代理类型
  hooks?: HooksSettings             // 技能注册的钩子
  paths?: string[]                  // 文件路径 glob 模式,限制技能可见性
}

types/command.ts#L25-L57

getPromptForCommand 方法是 prompt 命令的核心,它接收用户参数和执行上下文,返回 Anthropic API 的 ContentBlockParam[] 数组(通常是文本块和工具使用块的组合)。这个设计允许技能:

  1. 动态生成提示词:根据参数和上下文构造不同的指令
  2. 嵌入上下文信息:读取文件、执行 shell 命令获取实时状态
  3. 控制工具权限:通过 allowedTools 授予模型额外的工具访问权限

典型实现示例/commit 命令展示了 prompt 命令的完整实现:

const command = {
  type: 'prompt',
  name: 'commit',
  description: 'Create a git commit',
  allowedTools: [
    'Bash(git add:*)',
    'Bash(git status:*)',
    'Bash(git commit:*)'
  ],
  contentLength: 0,
  progressMessage: 'creating commit',
  source: 'builtin',
  async getPromptForCommand(_args, context) {
    const promptContent = getPromptContent() // 包含 git 状态和指令
    const finalContent = await executeShellCommandsInPrompt(
      promptContent,
      {
        ...context,
        getAppState() {
          const appState = context.getAppState()
          return {
            ...appState,
            toolPermissionContext: {
              ...appState.toolPermissionContext,
              alwaysAllowRules: {
                ...appState.toolPermissionContext.alwaysAllowRules,
                command: ALLOWED_TOOLS // 注入工具权限
              }
            }
          }
        }
      }
    )
    return [{ type: 'text', text: finalContent }]
  }
}

commands/commit.ts#L57-L80

提示词模板 使用特殊的 !\command“ 语法嵌入 shell 命令执行结果:

const promptContent = `
## Context
- Current git status: !\`git status\`
- Current git diff: !\`git diff HEAD\`
- Current branch: !\`git branch --show-current\`
- Recent commits: !\`git log --oneline -10\`

## Your task
Based on the above changes, create a single git commit...
`

commands/commit.ts#L20-L54

executeShellCommandsInPrompt 函数会在运行时解析这些命令并替换为实际输出,实现上下文感知的动态提示词生成

Fork 模式 是 prompt 命令的高级特性,通过 context: 'fork'agent 字段配置,允许技能在独立的子代理中执行,拥有独立的上下文和 token 预算。这种模式适用于:

  • 长时间运行的后台任务(如定时任务、代码审查)
  • 需要隔离上下文的操作(避免污染主会话)
  • 并行执行多个独立任务

utils/processUserInput/processSlashCommand.tsx#L62-L295

命令注册机制:多源合并与懒加载

Claude Code 的命令注册系统采用多源合并策略,将内置命令、用户技能、插件扩展、工作流脚本和 MCP 服务统一整合,通过懒加载 + 缓存优化性能。

命令源优先级与合并顺序

命令从以下源加载,按优先级从高到低排列(后加载的命令可覆盖同名命令):

const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
  const [
    { skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
    pluginCommands,
    workflowCommands
  ] = await Promise.all([
    getSkills(cwd),              // 1. 用户技能目录
    getPluginCommands(),         // 2. 插件命令
    getWorkflowCommands(cwd)     // 3. 工作流脚本
  ])

  return [
    ...bundledSkills,            // 内置技能(基础)
    ...builtinPluginSkills,      // 内置插件技能
    ...skillDirCommands,         // 用户自定义技能
    ...workflowCommands,         // 工作流脚本
    ...pluginCommands,           // 插件命令
    ...pluginSkills,             // 插件技能
    ...COMMANDS()                // 内置命令(最高优先级,可覆盖前面所有)
  ]
})

commands.ts#L449-L469

优先级设计遵循”用户覆盖系统”原则:内置命令(COMMANDS())具有最高优先级,允许系统定义的命令覆盖用户或插件的同名命令;用户技能和插件命令处于中间层,可扩展或覆盖基础功能。

内置命令数组 COMMANDS() 使用 lodash 的 memoize 包装,确保只在首次访问时初始化:

const COMMANDS = memoize((): Command[] => [
  addDir, advisor, agents, branch, btw, chrome, clear, color, 
  compact, config, copy, desktop, context, cost, diff, doctor,
  effort, exit, fast, files, heapDump, help, ide, init, 
  keybindings, mcp, memory, mobile, model, outputStyle, plugin,
  // ... 更多内置命令
])

export const builtInCommandNames = memoize(
  (): Set<string> => new Set(COMMANDS().flatMap(_ => [_.name, ...(_.aliases ?? [])]))
)

commands.ts#L258-L351

getCommands:统一的命令访问接口

getCommands 函数是获取可用命令的唯一入口,它执行以下步骤:

graph TD
    A[调用 getCommands cwd] --> B[loadAllCommands: 合并所有命令源]
    B --> C[过滤 meetsAvailabilityRequirement]
    C --> D[过滤 isCommandEnabled]
    D --> E{存在动态技能?}
    E -->|是| F[去重并插入到插件技能后]
    E -->|否| G[返回 baseCommands]
    F --> G

commands.ts#L476-L517

  1. 加载所有命令源:调用 loadAllCommands(cwd) 获取完整命令列表(memoized)
  2. 可用性过滤:调用 meetsAvailabilityRequirement(cmd) 检查认证/提供商限制
  3. 启用状态过滤:调用 isCommandEnabled(cmd) 检查特性开关和环境条件
  4. 动态技能注入:将运行时发现的技能插入到命令列表的适当位置

动态技能是在文件操作过程中发现的技能(如用户在对话中提及某个文件后,系统发现相关技能),它们通过 getDynamicSkills() 获取,并去重后插入到内置命令之前。

缓存失效策略

命令系统实现了多级缓存以优化性能,但也需要精确的缓存失效机制:

export function clearCommandMemoizationCaches(): void {
  loadAllCommands.cache?.clear?.()      // 清除命令加载缓存
  getSkillToolCommands.cache?.clear?.() // 清除技能工具命令缓存
  getSlashCommandToolSkills.cache?.clear?.() // 清除斜杠命令技能缓存
  clearSkillIndexCache?.()              // 清除技能索引缓存(外部模块)
}

export function clearCommandsCache(): void {
  clearCommandMemoizationCaches()
  clearPluginCommandCache()             // 清除插件命令缓存
  clearPluginSkillsCache()              // 清除插件技能缓存
  clearSkillCaches()                    // 清除技能目录缓存
}

commands.ts#L523-L539

缓存层级

  1. loadAllCommands:缓存完整的命令列表,仅在命令源变更时失效
  2. getSkillToolCommands:缓存模型可调用的技能子集
  3. SkillIndex:技能搜索索引,在动态技能添加时需要显式清除

命令执行流程:从解析到结果

命令执行流程由 processSlashCommand 函数驱动,它处理用户输入、查找命令、分发执行并返回结果。

执行流程总览

sequenceDiagram
    participant User
    participant PromptInput
    participant processSlashCommand
    participant CommandLoader
    participant Executor
    participant Model

    User->>PromptInput: 输入 /command args
    PromptInput->>processSlashCommand: 解析输入
    processSlashCommand->>processSlashCommand: parseSlashCommand
    alt 命令存在
        processSlashCommand->>CommandLoader: getCommand commandName
        CommandLoader-->>processSlashCommand: Command 对象
        alt local 命令
            processSlashCommand->>Executor: command.load().call args
            Executor-->>processSlashCommand: LocalCommandResult
        else local-jsx 命令
            processSlashCommand->>Executor: command.load().call onDone args
            Executor-->>processSlashCommand: React 组件
            processSlashCommand->>PromptInput: setToolJSX jsx
            User->>Executor: 关闭 UI
            Executor->>processSlashCommand: onDone result
        else prompt 命令
            processSlashCommand->>Executor: getPromptForCommand args
            Executor-->>processSlashCommand: ContentBlockParam[]
            processSlashCommand->>Model: 发送提示词
            Model-->>processSlashCommand: 模型响应
        end
        processSlashCommand-->>PromptInput: SlashCommandResult
    else 命令不存在
        processSlashCommand-->>PromptInput: 错误消息
    end

命令解析与查找

解析阶段使用 parseSlashCommand 函数从用户输入中提取命令名和参数:

const parsed = parseSlashCommand(inputString)
// 输入: "/commit add feature"
// 输出: { commandName: 'commit', args: 'add feature', isMcp: false }

utils/processUserInput/processSlashCommand.tsx#L310-L329

查找阶段使用 getCommand 函数在命令列表中查找匹配项,支持名称和别名匹配:

export function findCommand(commandName: string, commands: Command[]): Command | undefined {
  return commands.find(_ =>
    _.name === commandName ||
    getCommandName(_) === commandName ||
    _.aliases?.includes(commandName)
  )
}

commands.ts#L688-L698

分发执行:类型驱动的多态

getMessagesForSlashCommand 函数是命令执行的核心分发器,根据命令类型调用不同的执行路径:

async function getMessagesForSlashCommand(
  commandName: string, 
  args: string, 
  setToolJSX: SetToolJSXFn,
  context: ProcessUserInputContext,
  // ...
): Promise<SlashCommandResult> {
  const command = getCommand(commandName, context.options.commands)
  
  switch (command.type) {
    case 'local-jsx':
      // 执行 React 组件命令
    case 'local':
      // 执行同步命令
    case 'prompt':
      // 执行提示词命令
  }
}

utils/processUserInput/processSlashCommand.tsx#L525-L777

三种执行路径的详细实现

1. local-jsx 执行路径

case 'local-jsx': {
  return new Promise<SlashCommandResult>(resolve => {
    const onDone = (result?: string, options?: {...}) => {
      // 构建消息数组,根据 display 选项决定格式
      void resolve({
        messages: [...],
        shouldQuery: options?.shouldQuery ?? false,
        command
      })
    }
    
    void command.load().then(mod => mod.call(onDone, context, args))
      .then(jsx => {
        if (jsx == null) return
        setToolJSX({
          jsx,
          shouldHidePromptInput: true,
          isLocalJSXCommand: true
        })
      })
  })
}

utils/processUserInput/processSlashCommand.tsx#L551-L656

2. local 执行路径

case 'local': {
  const userMessage = createUserMessage({
    content: prepareUserContent({
      inputString: formatCommandInput(command, args),
      precedingInputBlocks
    })
  })
  
  const mod = await command.load()
  const result = await mod.call(args, context)
  
  if (result.type === 'skip') {
    return { messages: [], shouldQuery: false, command }
  }
  
  if (result.type === 'compact') {
    // 特殊处理压缩结果
    return {
      messages: buildPostCompactMessages(...),
      shouldQuery: false,
      command
    }
  }
  
  // 文本结果
  return {
    messages: [
      userMessage, 
      createCommandInputMessage(`<local-command-stdout>${result.value}</local-command-stdout>`)
    ],
    shouldQuery: false,
    command
  }
}

utils/processUserInput/processSlashCommand.tsx#L657-L722

3. prompt 执行路径

case 'prompt': {
  // 检查是否为 fork 模式
  if (command.context === 'fork') {
    return await executeForkedSlashCommand(
      command, args, context, precedingInputBlocks, setToolJSX, canUseTool
    )
  }
  
  // inline 模式:获取提示词并发送给模型
  const contentBlocks = await command.getPromptForCommand(args, context)
  
  return {
    messages: [
      createUserMessage({
        content: formatCommandLoadingMetadata(command, args)
      }),
      createUserMessage({
        content: contentBlocks,
        allowedTools: command.allowedTools,
        model: command.model,
        effort: command.effort
      })
    ],
    shouldQuery: true,  // 触发模型查询
    allowedTools: command.allowedTools,
    model: command.model,
    command
  }
}

utils/processUserInput/processSlashCommand.tsx#L723-L760

结果处理与消息构建

SlashCommandResult 是命令执行的统一返回类型:

type SlashCommandResult = {
  messages: Message[]          // 要添加到对话的消息数组
  shouldQuery: boolean         // 是否触发模型查询
  command: Command             // 执行的命令对象
  allowedTools?: string[]      // 授予的工具权限(仅 prompt 命令)
  model?: string               // 指定的模型(仅 prompt 命令)
  resultText?: string          // 命令结果文本(用于日志)
  nextInput?: string           // 自动填充的下一个输入
  submitNextInput?: boolean    // 是否自动提交
}

utils/processUserInput/processSlashCommand.tsx#L49-L51

消息格式化使用 XML 标签标记命令输出,便于解析和显示:

  • <local-command-stdout> 包装正常输出
  • <local-command-stderr> 包装错误输出
  • <command-input> 包装命令调用记录

实战:创建自定义命令

示例 1:创建简单的 local 命令

创建 src/commands/hello/index.ts

import type { Command, LocalCommandCall } from '../../types/command.js'

const call: LocalCommandCall = async (args, context) => {
  const name = args.trim() || 'World'
  return {
    type: 'text',
    value: `Hello, ${name}! Current directory: ${context.cwd}`
  }
}

const hello = {
  type: 'local',
  name: 'hello',
  description: 'Say hello to someone',
  argumentHint: '[name]',
  supportsNonInteractive: true,
  load: () => Promise.resolve({ call })
} satisfies Command

export default hello

然后在 src/commands.ts 中注册:

import hello from './commands/hello/index.js'

const COMMANDS = memoize((): Command[] => [
  // ... 其他命令
  hello,
])

示例 2:创建交互式 local-jsx 命令

创建 src/commands/greet/index.ts

import type { Command } from '../../commands.js'

const greet = {
  type: 'local-jsx',
  name: 'greet',
  description: 'Show a greeting dialog',
  load: () => import('./greet.js')
} satisfies Command

export default greet

创建 src/commands/greet/greet.tsx

import * as React from 'react'
import { Box, Text } from 'ink'
import type { LocalJSXCommandCall } from '../../types/command.js'

const GreetingDialog: React.FC<{
  onDone: (result?: string) => void
  initialName?: string
}> = ({ onDone, initialName }) => {
  const [name, setName] = React.useState(initialName || '')
  
  return (
    <Box flexDirection="column">
      <Text>Enter your name:</Text>
      <TextInput
        value={name}
        onChange={setName}
        onSubmit={() => onDone(`Hello, ${name}!`)}
      />
    </Box>
  )
}

export const call: LocalJSXCommandCall = async (onDone, context, args) => {
  return <GreetingDialog onDone={onDone} initialName={args.trim()} />
}

示例 3:创建 prompt 技能

创建 .claude/skills/test-coverage/SKILL.md

---
name: test-coverage
description: Generate comprehensive test coverage for the specified file
allowedTools:
  - Bash(npm test:*)
  - Bash(jest:*)
  - FileRead
  - FileWrite
---

## Task

Generate comprehensive test coverage for the file specified in $ARGUMENTS.

## Steps

1. Read the target file to understand its structure and functions
2. Identify all exported functions, classes, and edge cases
3. Generate a test file that covers:
   - Happy path scenarios
   - Edge cases and error conditions
   - Boundary conditions
   - Integration points
4. Run the tests to verify they pass
5. Report the coverage percentage

## Context

- Current file: $ARGUMENTS
- Test framework: !`cat package.json | grep -o '"jest"\|"mocha"\|"vitest"' | head -1`

用户调用 /test-coverage src/utils/format.ts 时,技能会读取文件、生成测试、运行并报告覆盖率。

架构设计原则总结

Claude Code 的命令架构体现了以下设计原则:

  1. 类型安全的多态:通过 discriminated union 实现编译时类型检查,避免运行时错误
  2. 懒加载优先:所有命令通过 load() 延迟加载,减少启动时间
  3. 多源合并:统一的注册机制支持内置、用户、插件、MCP 等多种命令源
  4. 关注点分离:命令定义、加载、执行、结果显示各司其职
  5. 扩展性:通过 prompt 命令将命令系统扩展为技能平台,支持无限可能

这种架构使 Claude Code 既能作为传统 CLI 工具使用,又能作为 AI 驱动的智能助手运行,实现了命令行界面与 AI 能力的无缝融合


下一步阅读建议