Claude Code's slash command system provides an extensible registry of user-invokable commands. Commands are typed, lazily loaded, and sourced from built-in definitions, skills directories, plugins, workflows, and MCP servers.
Command Type Definition
Every command shares a CommandBase shape and is discriminated by its type field:
type CommandBase = {
availability?: CommandAvailability[]
description: string
name: string
aliases?: string[]
argumentHint?: string
isEnabled?: () => boolean
immediate?: boolean
}
type Command = CommandBase & (
| PromptCommand // type: 'prompt'
| LocalCommand // type: 'local'
| LocalJSXCommand // type: 'local-jsx'
)Command Types
getPromptForCommand() function that returns ContentBlockParam[]. Used by skills and MCP-sourced commands.LocalCommandResult (text, compaction result, or skip). Does not render JSX./help, /model, /config.PromptCommand Fields
'builtin' | 'mcp' | 'plugin' | 'bundled' or a SettingSource.'inline' expands into the current conversation; 'fork' runs in a sub-agent with separate context.Availability
Commands can declare which auth/provider environments they are available in:
type CommandAvailability =
| 'claude-ai' // Claude.ai OAuth subscriber
| 'console' // Console API key user (direct api.anthropic.com)Commands without availability are universal. The meetsAvailabilityRequirement() function checks this at runtime, and it is intentionally not memoized because auth state can change mid-session (e.g., after /login).
Lazy Loading
Commands use lazy loading to defer heavy imports until invocation. Both local and local-jsx commands define a load() function:
// help command definition
const help = {
type: 'local-jsx',
name: 'help',
description: 'Show help and available commands',
load: () => import('./help.js'),
} satisfies Command
// model command with dynamic description
export default {
type: 'local-jsx',
name: 'model',
get description() {
return `Set the AI model (currently ${renderModelName(getMainLoopModel())})`
},
argumentHint: '[model]',
load: () => import('./model.js'),
} satisfies CommandThe load() pattern uses dynamic import() so the bundler can code-split command implementations. This keeps startup time low: the command registry loads instantly, but the implementation code for any given command only loads when that command is first invoked.
Command Registry
The getCommands() function in src/commands.ts is the main entry point for retrieving available commands. It aggregates commands from multiple sources.
Source Priority
Commands are assembled from these sources (in order):
- Bundled skills: shipped with Claude Code, registered synchronously
- Built-in plugin skills: from enabled built-in plugins
- Skill directory commands: from
.claude/skills/directories - Workflow commands: from workflow scripts (feature-gated)
- Plugin commands: from installed plugins
- Plugin skills: skill commands from plugins
- Built-in commands: the
COMMANDS()array of core slash commands
Loading and Filtering
export async function getCommands(cwd: string): Promise<Command[]> {
const allCommands = await loadAllCommands(cwd) // memoized by cwd
const dynamicSkills = getDynamicSkills() // discovered during file ops
const baseCommands = allCommands.filter(
cmd => meetsAvailabilityRequirement(cmd) && isCommandEnabled(cmd),
)
// Dedupe dynamic skills against base commands
const uniqueDynamicSkills = dynamicSkills.filter(
s => !baseCommandNames.has(s.name) && meetsAvailabilityRequirement(s)
)
return [...baseCommands, ...uniqueDynamicSkills]
}The heavy loading (loadAllCommands) is memoized by cwd, but availability and isEnabled checks run fresh every call so auth/feature-flag changes take effect immediately.
Internal-Only Commands
A separate INTERNAL_ONLY_COMMANDS array contains commands restricted to Anthropic employees (USER_TYPE === 'ant'), including debugging tools, internal workflows, and development utilities.
Command Lifecycle
1. Discovery
When the user types /, the command registry is queried. Commands are filtered by availability, enablement, and (for dynamic skills) file-path matching. The result is displayed as a completion list.
2. Parsing
The command name and arguments are parsed from the user input. Aliases are resolved to canonical names. The argumentHint field provides placeholder text for argument completion.
3. Loading
For local and local-jsx commands, load() is called to import the implementation module. This is the point where heavy dependencies are pulled in.
4. Execution
- prompt:
getPromptForCommand(args, context)generates content blocks, which are sent to the model as a user message - local:
call(args, context)returns aLocalCommandResult - local-jsx:
call(onDone, context, args)returns a React node and callsonDonewhen complete
5. Result Handling
The onDone callback (for local-jsx) or return value (for local/prompt) determines what happens next:
display: 'skip': no visible outputdisplay: 'system': shown as a system messagedisplay: 'user': shown as a user messageshouldQuery: true: triggers a model query after the command completesmetaMessages: additional model-visible but UI-hidden messages
Feature-Gated Commands
Several commands are conditionally imported based on build-time feature flags:
const voiceCommand = feature('VOICE_MODE')
? require('./commands/voice/index.js').default
: null
const bridge = feature('BRIDGE_MODE')
? require('./commands/bridge/index.js').default
: null
const workflowsCmd = feature('WORKFLOW_SCRIPTS')
? require('./commands/workflows/index.js').default
: nullThis pattern uses require() inside feature gates so the bundler can eliminate unreachable command code entirely from production builds that do not enable those features.