Chapter 7. Claude Code Slash Commands

A custom slash command in Claude Code lives in one markdown file. .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md both create the /deploy command, existing commands files keep working with the same frontmatter, and the name comes from the file basename or the directory name rather than from a registry. A slash command in current Claude Code is a skill invoked by name, plus a legacy single-file format that still resolves.

My ~/.claude/commands directory holds three markdown files: suede-execute.md, suede-plan.md, and suede-slides.md. It also holds one symlink, suedify.md, aimed at a SKILL.md under ~/.codex/skills that no longer exists, so /suedify resolves by filename and loads nothing. The directory is its own git repository with no origin remote. That inventory says where custom commands stand on my machine: the format still works, and I stopped adding to it.

The reason is upstream. Fetch https://code.claude.com/docs/en/slash-commands today and you get an HTTP 200 serving a page titled “Extend Claude with skills”, with a canonical link pointing at /docs/en/skills. The standalone slash-commands page is gone, because custom commands merged into skills.

Where a command name comes from

Command names derive from file layout, not from a registry.

Source Command name comes from
Skill directory under ~/.claude/skills/ or .claude/skills/ The directory name: deploy-staging/ becomes /deploy-staging
File under a commands/ directory The file basename: deploy.md becomes /deploy
Plugin skills/ subdirectory Frontmatter name or directory name, namespaced by plugin: /my-plugin:review
Plugin-root SKILL.md Frontmatter name, falling back to the plugin directory name

In personal and project skills, frontmatter name sets only the display label. The directory name is the identity. On my machine the distinction never bites: across all 31 user-level skills and all 29 skills in my public pack, frontmatter name equals the directory name. Lowercase hyphenated slugs, zero mismatches.

Plugin skills give name more work, and the behavior changed at v2.1.216. From that version, a plugin skill’s frontmatter name replaces only the last command segment, the plugin prefix stays (/my-plugin:fancy), and the bare form /fancy works unless the name is taken. Before v2.1.216 the frontmatter name replaced the whole command name and the prefixed form did not autocomplete.

A <skill-name> entry in the enterprise, personal, or project location can be a symlink to a directory elsewhere. Claude Code follows it, reads SKILL.md from the target, and loads the skill once if the same target is reachable from more than one location. My broken suedify.md shows the failure mode: the link outlives its target, the command keeps resolving, and the body is gone. Symlinked commands need the same rot check as any other link farm.

Frontmatter fields that change how a command runs

The documented SKILL.md frontmatter fields, all optional, with description the only recommended one:

Field What it controls
name Display label; command segment for plugin skills
description Listing text that drives skill selection
when_to_use Additional listing text, combined with description
argument-hint Usage hint for the command’s arguments
arguments Declares named arguments for $name substitution
disable-model-invocation Blocks Claude from loading the skill on its own
user-invocable false hides the command from the / menu
allowed-tools Permission grants for the invoking turn
disallowed-tools Tools removed while the skill is active
model Model override for the rest of the turn
effort Reasoning effort: low, medium, high, xhigh, max
context fork runs the skill in a forked subagent context
agent Subagent type for the fork
background false makes the invoking turn wait for the fork
hooks Hook definitions active while the skill runs
paths Glob patterns gating automatic activation
shell bash or powershell for injected shell commands

Five of those fields carry semantics the one-line summary cannot hold.

The listing cap. The combined description plus when_to_use text truncates at 1,536 characters in the skill listing; the skillListingMaxDescChars setting moves the cap. Past it, the tail of your routing clause is invisible to the model.

The invocation-control pair. disable-model-invocation: true (default false) blocks automatic loading by Claude, blocks preloading into subagents, and from v2.1.196 blocks the skill from running when a scheduled task fires with it as the prompt. user-invocable: false (default true) hides the command from the / menu without blocking Skill-tool access. One switch governs what Claude may do, the other what the menu shows; neither implies the other.

The tool fields grant, they do not restrict. allowed-tools pre-approves permissions for the turn that invokes the skill and clears on the next user message. disallowed-tools removes tools from the pool while the skill is active and clears the same way. Neither can remove EndConversation while any other tool remains. Both accept a space- or comma-separated string or a YAML list, and the entries are permission-rule patterns rather than bare tool names: my impeccable skill declares Bash(npx impeccable *) and Bash(node .claude/skills/impeccable/scripts/*).

Model and effort. model accepts the same values as /model plus inherit, applies for the rest of the current turn only, and Claude Code skips it if the org’s availableModels allowlist excludes it. effort defaults to the session’s level. Boolean fields, for the record, accept yes, no, on, off, 1, and 0 in any letter case from v2.1.218; earlier versions recognize only true and false.

Forking. context: fork runs the skill in a forked subagent context; agent picks the type (Explore, Plan, general-purpose, or any custom subagent in .claude/agents/), defaulting to general-purpose. background defaults to true; false makes the invoking turn wait for the result and requires v2.1.218. Four conditions force the wait regardless of background: non-interactive mode (-p or the Agent SDK), CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1, an earlier invocation of the same skill still running, and a scheduled task firing with the skill as its prompt.

The last two fields are narrower. paths uses the same glob format as path-specific memory rules. shell selects the interpreter for injected commands; PowerShell needs the PowerShell tool, on by default on Windows without Git Bash and enabled elsewhere with CLAUDE_CODE_USE_POWERSHELL_TOOL=1.

Field usage in the wild runs thinner than the reference. Of my 31 user-level skills, five pin model: opus (suede-debug, suede-plan, suede-product, suede-spec, suede-think), one (impeccable) declares argument-hint, user-invocable, and allowed-tools, and the rest carry name and description and nothing more. Impeccable also carries version and license keys the documented field list does not include; whether the loader honors them I could not test read-only.

How $ARGUMENTS and named arguments expand

Placeholder Expands to
$ARGUMENTS The full argument string
$ARGUMENTS[N] The Nth argument, 0-indexed
$N Shorthand for $ARGUMENTS[N]
$name A named argument from the arguments frontmatter list, mapped by position
${CLAUDE_SESSION_ID} The current session ID
${CLAUDE_EFFORT} The current effort level
${CLAUDE_SKILL_DIR} The invoked skill’s directory
${CLAUDE_PROJECT_DIR} The project root

Indexed arguments split with shell-style quoting: /my-skill "hello world" second gives $0 the value hello world. An indexed placeholder with no matching argument stays literal in the text; a named placeholder with no match expands to an empty string. Those two defaults point in opposite directions, so a skill that mixes both styles fails in two shapes.

A literal dollar sign before a digit, ARGUMENTS, or a declared name escapes with a single backslash: \$1.00 survives as $1.00. A double backslash does not double-escape. \\$1 leaves both backslashes in place and $1 still expands.

Arguments passed to a skill whose content contains no $ARGUMENTS are not dropped: Claude Code appends ARGUMENTS: <value> to the end of the skill content.

${CLAUDE_SKILL_DIR} and ${CLAUDE_PROJECT_DIR} substitute in two places: the markdown body and Bash rules inside allowed-tools. The second site is what lets a bundled script run without a permission prompt:

allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.sh *)

That allowed-tools substitution needs v2.1.129 for the skill directory and v2.1.196 for the project directory.

argument-hint plays no substitution role; it documents the expected shape. The one instance on my machine, impeccable’s, packs its sub-commands into a single quoted line using | alternation and · group separators, ending in a target placeholder.

Running a shell command inside a command file

A !`command` placeholder runs before Claude sees the skill content, and its output replaces the placeholder. Claude Code recognizes the syntax only when the ! starts a line or follows whitespace; KEY=!`cmd` stays literal. Multi-line commands use a fenced block opened with ```!. Substitution runs once, and Claude Code does not re-scan command output for further placeholders, so injected output cannot smuggle in a second expansion.

The shell frontmatter field picks the interpreter. An org can also shut the mechanism off: "disableSkillShellExecution": true replaces each injected command with [shell command execution disabled by policy] across user, project, plugin, and additional-directory skills and commands. Bundled and managed skills are exempt.

Which command wins when two sources define the same name

When two sources define the same command name, the documented order is:

  1. Enterprise (under the managed settings directory) overrides personal.
  2. Personal (~/.claude/skills/) overrides project.
  3. Any of those overrides a bundled skill of the same name.
  4. A skill beats a .claude/commands/ file with the same name.
  5. Plugin skills live in a plugin-name:skill-name namespace and cannot conflict. At runtime the namespace uses the plugin half of the <plugin>@<marketplace> id and drops the marketplace: my enabled pack surfaces as suede-skills:suede-code, not suede-skills@suede:suede-code.

Monorepos add a sixth case. A skill in a subdirectory’s .claude/skills/ that clashes with another name surfaces under a directory-qualified name such as /apps/web:deploy; invoking the unqualified /deploy loads the project-root skill and appends a list of the qualified variants. That behavior needs v2.1.203 or later.

The namespace rules do not stop you from colliding with yourself. Four skill names on my machine (suede-codex-fleet, suede-design, suede-deslop, suede-full-send) exist in both ~/.claude/skills/ and the enabled suede-skills plugin, so the session listing shows each twice: once bare, once prefixed. Which body a bare invocation loads is untested here, and it is the exact loader-order dependence my own overlap auditor (audit_skill_overlap.py --exclusive-root) exists to fail builds on. I wrote the rule and I am violating it on my own machine.

The four locations a command loads from

Skills load from four documented locations: personal ~/.claude/skills/<skill-name>/SKILL.md, project .claude/skills/<skill-name>/SKILL.md, plugin <plugin>/skills/<skill-name>/SKILL.md, and an enterprise location under the managed settings directory. SKILL.md is the required entrypoint filename in each. My legacy command files sit at the user level in ~/.claude/commands/; the docs describe the same single-file format at .claude/commands/.

Project discovery walks upward: .claude/skills/ loads from the starting directory and each parent up to the repository root, plus on demand from nested .claude/skills/ directories below the starting directory. The gate is the working directory, and it is a hard gate. My suede-geo repo carries 46 project skills; a session started outside that repo lists none of them.

Added directories are the exception worth memorizing. --add-dir and /add-dir load .claude/skills/ from the added directory, an explicit carve-out from the rule that additional directories grant file access only. The permissions.additionalDirectories setting grants file access and does not load skills. Other .claude/ config in an added directory, commands included, stays unloaded either way.

Changes land live. Adding, editing, or removing a skill under ~/.claude/skills/, project .claude/skills/, or an added directory’s skills folder takes effect in the current session. Two caveats: a top-level skills directory created after session start needs a restart to be seen, and detection covers SKILL.md text only, so hooks/, .mcp.json, agents/, and output-styles/ inside a skills-directory plugin need /reload-plugins.

What else the / menu lists, and the aliases it accepts

Typing / surfaces three populations: built-in commands, bundled skills that ship inside Claude Code, and yours. The skills documentation names /doctor, /code-review, /batch, /debug, /loop, and /claude-api as bundled, plus the run trio /run, /verify, and /run-skill-generator (v2.1.145 or later; the generator records a per-project launch recipe at .claude/skills/run-<name>/). The commands reference labels 21 entries as bundled skills, adding /dataviz, /design-sync, /fewer-permission-prompts, /plan, /pr, /publish, /quality, /review, /security-review, /simplify, /test, /tests, /tips, and /web, with /deep-research marked as a Workflow. My extraction of that table was truncated, so I hold its tail at lower confidence than the rest of this material.

Bundled skills have a kill switch: disableBundledSkills disables all of them except /doctor, which takes a second step (DISABLE_DOCTOR_COMMAND, or a skillOverrides entry of "doctor": "off"). /doctor became a bundled skill in v2.1.205; before that it was a built-in command. From v2.1.215, /verify and /code-review run only when the user invokes them; earlier versions let Claude run them itself.

Aliases from the commands reference:

Alias Resolves to
/reset, /new /clear
/bg /background
/cost /usage
/checkup /doctor
/app /desktop
/ios, /android /mobile
/settings /config
/quit /exit
/proactive /loop

/bug and /share are former aliases of /feedback as of v2.1.212.

Two control planes sit above the menu. Permission rules use Skill(name) for an exact match and Skill(name *) for a prefix with any arguments; denying the bare Skill tool disables all skills. A few built-ins (/init, /review, /security-review) stay reachable through the Skill tool while others such as /compact are not. Visibility uses skillOverrides, with four states per skill: "on" (name and description listed, in the menu), "name-only", "user-invocable-only" (hidden from Claude, still in the menu), and "off" (hidden everywhere; from v2.1.199 that includes Remote Control clients and Agent SDK callers). Absent entries mean "on", plugin skills are unaffected, and the /skills menu cycles states with Space and saves to .claude/settings.local.json with Enter.

What an invocation costs for the rest of the session

An invoked skill renders into the conversation as one message and stays for the session; Claude Code does not re-read the file on later turns, so a mid-session edit reaches the next invocation, not the ones already loaded. Re-invoking with identical rendered content adds a short already-loaded note instead of a second copy, as of v2.1.202; earlier versions appended the full body each time.

Skills stack. From v2.1.199, /write-tests /fix-issue 123 at the start of one message loads both skills and passes 123 as $ARGUMENTS to each. Expansion covers the first skill plus up to five more and stops at the first token that is not an inline user-invocable skill (a forked skill such as /code-review, or /loop); that token onward becomes the argument text.

Two budgets govern long sessions. Auto-compaction re-attaches the most recent invocation of each skill after the summary, keeping the first 5,000 tokens of each within a combined 25,000-token budget filled from the most recent invocation backward. The skill listing itself defaults to 1% of the model’s context window, adjustable with skillListingBudgetFraction or a fixed SLASH_COMMAND_TOOL_CHAR_BUDGET; on overflow, descriptions drop starting with the least-invoked skills while names stay listed. A skill you stopped invoking loses its description first, which is the right order and a standing argument for pruning.

One closing caution. claude --version on this machine reports 2.1.211, below the gates for user-only /verify and /code-review (2.1.215), the plugin name-segment rule (2.1.216), and the extended boolean spellings plus background: false (2.1.218). The documentation describes the newest build; your installed binary is the one parsing your frontmatter. Check the version before you file a behavior difference as a bug.