- Home
- Documentation
- runtime-tools
- Slash Command Internals
Slash Command Internals
Slash command internals
Section titled “Slash command internals”This document describes how slash commands are discovered, deduplicated, surfaced in interactive mode, and expanded at prompt time in coding-agent.
Implementation files
Section titled “Implementation files”src/extensibility/slash-commands.tssrc/capability/slash-command.tssrc/discovery/builtin.tssrc/discovery/claude.tssrc/discovery/codex.tssrc/discovery/claude-plugins.tssrc/capability/index.tssrc/discovery/helpers.tssrc/session/agent-session.tssrc/modes/interactive-mode.tssrc/modes/controllers/input-controller.tssrc/modes/utils/ui-helpers.tssrc/modes/controllers/command-controller.ts
1) Discovery model
Section titled “1) Discovery model”Slash commands are a capability (id: "slash-commands") keyed by command name (key: cmd => cmd.name).
The capability registry loads all registered providers, sorted by provider priority descending, and deduplicates by key with first wins semantics.
Provider precedence
Section titled “Provider precedence”Current slash-command providers and priorities:
native(OMP) — priority100claude— priority80claude-plugins— priority70codex— priority70
Tie behavior: equal-priority providers keep registration order. Current import order registers claude-plugins before codex, so plugin commands win over codex commands on name collisions.
Name-collision behavior
Section titled “Name-collision behavior”For slash-commands, collisions are resolved strictly by capability dedup:
- highest-precedence item is kept in
result.items - lower-precedence duplicates remain only in
result.alland are marked_shadowed = true
This applies across providers and also within a provider if it returns duplicate names.
File scanning behavior
Section titled “File scanning behavior”Providers mostly use loadFilesFromDir(...), which currently:
- defaults to non-recursive matching (
*.md) - uses native glob with
gitignore: true,hidden: false - reads each matched file and transforms it into a
SlashCommand
So hidden files/directories are not loaded, and ignored paths are skipped.
2) Provider-specific source paths and local precedence
Section titled “2) Provider-specific source paths and local precedence”native provider (builtin.ts)
Section titled “native provider (builtin.ts)”Search roots come from .xcsh directories:
- project:
<cwd>/.xcsh/commands/*.md - user:
~/.xcsh/agent/commands/*.md
getConfigDirs() returns project first, then user, so project native commands beat user native commands when names collide.
claude provider (claude.ts)
Section titled “claude provider (claude.ts)”Loads:
- user:
~/.claude/commands/*.md - project:
<cwd>/.claude/commands/*.md
The provider pushes user items before project items, so user Claude commands beat project Claude commands on same-name collisions inside this provider.
codex provider (codex.ts)
Section titled “codex provider (codex.ts)”Loads:
- user:
~/.codex/commands/*.md - project:
<cwd>/.codex/commands/*.md
Both sides are loaded then flattened in user-first order, so user Codex commands beat project Codex commands on collisions.
Codex command content is parsed with frontmatter stripping (parseFrontmatter), and command name can be overridden by frontmatter name; otherwise filename is used.
claude-plugins provider (claude-plugins.ts)
Section titled “claude-plugins provider (claude-plugins.ts)”Loads plugin command roots from ~/.claude/plugins/installed_plugins.json, then scans <pluginRoot>/commands/*.md.
Ordering follows registry iteration order and per-plugin entry order from that JSON data. There is no additional sort step.
3) Materialization to runtime FileSlashCommand
Section titled “3) Materialization to runtime FileSlashCommand”loadSlashCommands() in src/extensibility/slash-commands.ts converts capability items into FileSlashCommand objects used at prompt time.
For each command:
- parse frontmatter/body (
parseFrontmatter) - description source:
frontmatter.descriptionif present- else first non-empty body line (trimmed, max 60 chars with
...)
- keep parsed body as executable template content
- compute a display source string like
via Claude Code Project
Frontmatter parse severity is source-dependent:
nativelevel -> parse errors arefataluser/projectlevels -> parse errors arewarnwith fallback parsing
Bundled fallback commands
Section titled “Bundled fallback commands”After filesystem/provider commands, embedded command templates are appended (EMBEDDED_COMMAND_TEMPLATES) if their names are not already present.
Current embedded set comes from src/task/commands.ts and is used as a fallback (source: "bundled").
4) Interactive mode: where command lists come from
Section titled “4) Interactive mode: where command lists come from”Interactive mode combines multiple command sources for autocomplete and command routing.
At construction time it builds a pending command list from:
- built-ins (
BUILTIN_SLASH_COMMANDS, includes argument completion and inline hints for selected commands) - extension-registered slash commands (
extensionRunner.getRegisteredCommands(...)) - TypeScript custom commands (
session.customCommands), mapped to slash command labels - optional skill commands (
/skill:<name>) whenskills.enableSkillCommandsis enabled
Then init() calls refreshSlashCommandState(...) to load file-based commands and install one CombinedAutocompleteProvider containing:
- pending commands above
- discovered file-based commands
refreshSlashCommandState(...) also updates session.setSlashCommands(...) so prompt expansion uses the same discovered file command set.
Refresh lifecycle
Section titled “Refresh lifecycle”Slash command state is refreshed:
- during interactive init
- after
/movechanges working directory (handleMoveCommandcallsresetCapabilities()thenrefreshSlashCommandState(newCwd))
There is no continuous file watcher for command directories.
Other surfacing
Section titled “Other surfacing”The Extensions dashboard also loads slash-commands capability and displays active/shadowed command entries, including _shadowed duplicates.
5) Prompt pipeline placement
Section titled “5) Prompt pipeline placement”AgentSession.prompt(...) slash handling order (when expandPromptTemplates !== false):
- Extension commands (
#tryExecuteExtensionCommand)
If/namematches extension-registered command, handler executes immediately and prompt returns. - TypeScript custom commands (
#tryExecuteCustomCommand)
Boundary only: if matched, it executes and may return:string-> replace prompt text with that stringvoid/undefined-> treated as handled; no LLM prompt
- File-based slash commands (
expandSlashCommand)
If text still starts with/, attempt markdown command expansion. - Prompt templates (
expandPromptTemplate)
Applied after slash/custom processing. - Delivery
- idle: prompt is sent immediately to agent
- streaming: prompt is queued as steer/follow-up depending on
streamingBehavior
This is why slash command expansion sits before prompt-template expansion, and why custom commands can transform away the leading slash before file-command matching.
6) Expansion semantics for file-based slash commands
Section titled “6) Expansion semantics for file-based slash commands”expandSlashCommand(text, fileCommands) behavior:
- only runs when text begins with
/ - parses command name from first token after
/ - parses args from remaining text via
parseCommandArgs - finds exact name match in loaded
fileCommands - if matched, applies:
- positional replacement:
$1,$2, … - aggregate replacement:
$ARGUMENTSand$@ - then template rendering via
prompt.renderwith{ args, ARGUMENTS, arguments }
- positional replacement:
- if no match, returns original text unchanged
parseCommandArgs caveats
Section titled “parseCommandArgs caveats”The parser is simple quote-aware splitting:
- supports
'single'and"double"quoting to keep spaces - strips quote delimiters
- does not implement backslash escaping rules
- unmatched quote is not an error; parser consumes until end
7) Unknown /... behavior
Section titled “7) Unknown /... behavior”Unknown slash input is not rejected by core slash logic.
If command is not handled by extension/custom/file layers, expandSlashCommand returns original text, and the literal /... prompt proceeds through normal prompt-template expansion and LLM delivery.
Interactive mode separately hard-handles many built-ins in InputController (for example /settings, /model, /mcp, /move, /exit). Those are consumed before session.prompt(...) and therefore never reach file-command expansion in that path.
8) Streaming-time differences vs idle
Section titled “8) Streaming-time differences vs idle”Idle path
Section titled “Idle path”session.prompt("/x ...")runs command pipeline and either executes command immediately or sends expanded text directly.
Streaming path (session.isStreaming === true)
Section titled “Streaming path (session.isStreaming === true)”prompt(...)still runs extension/custom/file/template transforms first- then requires
streamingBehavior:"steer"-> queue interrupt message (agent.steer)"followUp"-> queue post-turn message (agent.followUp)
- if
streamingBehavioris omitted, prompt throws an error
Important command-specific streaming behavior
Section titled “Important command-specific streaming behavior”- Extension commands are executed immediately even during streaming (not queued as text).
steer(...)/followUp(...)helper methods reject extension commands (#throwIfExtensionCommand) to avoid queuing command text for handlers that must run synchronously.- Compaction queue replay uses
isKnownSlashCommand(...)to decide whether queued entries should be replayed viasession.prompt(...)(for known slash commands) vs raw steer/follow-up methods.
9) Error handling and failure surfaces
Section titled “9) Error handling and failure surfaces”- Provider load failures are isolated; registry collects warnings and continues with other providers.
- Invalid slash command items (missing name/path/content or invalid level) are dropped by capability validation.
- Frontmatter parse failures:
- native commands: fatal parse error bubbles
- non-native commands: warning + fallback key/value parse
- Extension/custom command handler exceptions are caught and reported via extension error channel (or logger fallback for custom commands without extension runner), and treated as handled (no unintended fallback execution).