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:
- Enterprise (under the managed settings directory) overrides personal.
- Personal (
~/.claude/skills/) overrides project. - Any of those overrides a bundled skill of the same name.
- A skill beats a
.claude/commands/file with the same name. - Plugin skills live in a
plugin-name:skill-namenamespace and cannot conflict. At runtime the namespace uses the plugin half of the<plugin>@<marketplace>id and drops the marketplace: my enabled pack surfaces assuede-skills:suede-code, notsuede-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.