Writing Custom Slash Commands
Authoring a slash command from a markdown file. Argument handling, model selection, and team-shared command libraries.
What it is#
A custom slash command is a reusable prompt — sometimes parameterised, sometimes wrapped around a specific tool allowlist — packaged as a markdown file in ~/.claude/commands/ or .claude/commands/ inside a project. When you type /<name> in the session, Claude Code loads the markdown body as the next user turn (with arguments interpolated) and runs the conversation loop from there.
The mechanism is intentionally simple: a slash command is just a named prompt template on disk. There is no plugin runtime, no JavaScript hook, no compile step. The leverage comes from naming, sharing, and standardising the prompts you reach for multiple times a week — /review-pr, /explain-this-bug, /write-changelog — so that everyone on the team triggers the same well-tuned prompt with one keystroke instead of retyping six paragraphs.
Slash commands are the lightest form of customization in Claude Code. They sit one rung below custom sub-agents and several rungs below the Agent SDK in complexity. If a workflow is just “say the same thing in slightly different words every time”, it should be a slash command.
When to use it#
Reach for a custom slash command when a prompt is:
- Repeated. You’ve typed the same instructions three or more times. Anything you say weekly belongs on disk.
- Long. A six-paragraph prompt that loads the right files, sets the right tone, and asks for the right output is too tedious to retype but worth saving.
- Stable. The instruction set has stopped evolving. If you’re still tweaking the wording every time, save it later.
- Team-shareable. A code-review prompt or a release-notes prompt is more valuable when everyone uses the same one.
Don’t reach for slash commands when:
- The prompt is one-shot. Just type it.
- The work needs an isolated context (you don’t want the rest of the session to see what the command did) — use a sub-agent.
- The work needs enforcement (must run after every edit, must block certain actions) — use a hook.
- The work is best modelled as a long-running script with structured I/O — use the Agent SDK.
How it works#
File layout#
A custom slash command is a markdown file. Two locations are searched:
- User-global:
~/.claude/commands/<name>.md— available in every session. - Project-scoped:
<repo>/.claude/commands/<name>.md— available only when Claude Code is run inside that repo. Project-scoped commands are typically committed alongside the codebase.
Project commands shadow user commands with the same name, so a repo can override or specialise a global command without renaming it.
Frontmatter#
The markdown file may begin with a YAML frontmatter block declaring metadata. Common fields:
---description: Review a pull request for correctness, style, and risk.argument-hint: <PR number or URL>model: claude-opus-4-7allowed-tools: ["Bash", "Read", "Grep"]---
Body of the prompt follows here.description shows up in the / autocomplete menu. argument-hint is the placeholder shown next to the command name. model overrides the session model for this single invocation — useful when you want the smartest model for review work even if your default is faster and cheaper. allowed-tools narrows the tool set for the command’s execution, which both reduces accidental scope creep and gives the model a tighter prompt.
Argument interpolation#
Arguments are passed positionally on the command line and interpolated into the markdown body. The convention is $ARGUMENTS for “everything after the command name” and $1, $2, $3 for individual positional args. A simple parameterised command:
---description: Summarise the changes in commit $1.---
Read the diff for commit $1, then write a one-paragraph summarycovering: the user-visible change, the risk profile, and thefiles touched.Invocation: /summarise-commit abc1234.
If the user runs /summarise-commit without arguments, the command still runs — the substitution is literal, so the body would contain the bare token $1. Defensive commands check for empty arguments and ask the user to retry.
The loaded turn#
When you invoke a slash command, the rendered markdown body becomes the next user turn in the conversation. From the model’s point of view there is no difference between you typing the body and the command expanding to it — which means slash commands inherit the full session context, including CLAUDE.md, conversation history, and any active permission mode.
This shared-context behaviour is the main contrast with sub-agents: a sub-agent starts in a fresh context window; a slash command does not.
Configuration#
Authoring a new command#
- Decide on a name. Kebab-case, short, action-shaped:
review-pr,explain-bug,write-changelog. - Decide on a scope. Project-shared?
.claude/commands/<name>.md. Personal?~/.claude/commands/<name>.md. - Write the body as if you were typing the prompt. Use Markdown for structure — headings for sub-steps, bullet lists for criteria.
- Add frontmatter for
descriptionandargument-hint. Addmodelandallowed-toolsif useful. - Test by invoking with a representative input. Iterate until the output is consistently good.
Team-shared command libraries#
A .claude/commands/ directory committed to the repo gives the whole team the same shortcuts. A typical project library:
.claude/commands/ review-pr.md # /review-pr 123 changelog.md # /changelog v1.4.0 triage-issue.md # /triage-issue 456 release-notes.md # /release-notes since=v1.3.0 audit-bash.md # /audit-bash (no args; reviews recent shell activity)The first time a new teammate joins, they run git pull and instantly get the same /review-pr flow the rest of the team uses. There’s no separate install step — Claude Code finds the directory automatically.
A worked example: /review-pr#
---description: Review a pull request — correctness, style, risk, and a recommended action.argument-hint: <PR number or URL>model: claude-opus-4-7allowed-tools: ["Bash", "Read", "Grep"]---
Review pull request $1 with the following structure:
1. Pull the PR via `gh pr view $1 --json title,body,files`.2. For each file in the diff, read the surrounding code (not just the diff) so you understand the context.3. Identify three categories of issues: - **Correctness.** Bugs, missing edge cases, incorrect contracts. - **Style.** Naming, readability, idiomatic use of the codebase. - **Risk.** Reversibility, blast radius, rollout concerns.4. End with one of: **APPROVE**, **APPROVE WITH NITS**, or **REQUEST CHANGES**, with one sentence of justification.
Be specific. Cite line numbers. Don't invent issues to look thorough.The first time you save this and run /review-pr 123, the model has a tight, repeatable review prompt — and so does every teammate who pulls the repo.
Examples#
Project-scoped command with strict tool allowlist#
A command that triages issues but is forbidden from making code changes:
---description: Triage a GitHub issue — labels, priority, and a one-line plan.argument-hint: <issue number>allowed-tools: ["Bash", "Read", "Grep"]---
Fetch issue $1 with `gh issue view $1`. Read any files it references.Output:- Suggested labels (bug / feature / docs / chore).- Priority (P0 / P1 / P2 / P3) with a one-sentence reason.- A one-line plan describing what a fix would look like.
Do not edit any files. Do not run any tests. This is a triage pass only.The allowed-tools array excludes Edit and Write, so even if the model is tempted to start fixing things, the tool calls fail.
Personal command with model override#
A daily-driver /explain command that always uses the most capable model regardless of the session default:
---description: Explain a piece of code in depth — what it does and why it's structured this way.argument-hint: <file path or symbol>model: claude-opus-4-7---
Read $1 and explain:1. What the code does at a high level.2. Why it's structured the way it is — what alternatives were rejected.3. The most interesting line and why it's interesting.
Be opinionated. If the code is bad, say so and explain how you wouldrefactor it.The model field overrides the default for just this invocation — the rest of the session keeps using whatever the default model is.
Command that delegates to a sub-agent#
A slash command can launch a sub-agent for the heavy work and stay out of the main context:
---description: Audit recent Bash history for risky commands and write a markdown report.allowed-tools: ["Task"]---
Spawn the `security-auditor` sub-agent. Give it this brief:"Read ~/.claude/audit/bash.log. Find any commands matching the riskheuristics in ~/.claude/policy/bash-deny.md. Return a markdown reportwith one section per finding."
Wait for the sub-agent to complete. Surface the report inline.This pattern keeps the audit work — which may read a lot of log lines — entirely inside the sub-agent’s context window, so the main session stays clean.
Composing slash commands and hooks#
A /changelog slash command writes the markdown. A PostToolUse hook on Write runs pnpm format to normalise it. The slash command stays simple (just a prompt template); the hook handles the deterministic post-processing.
Gotchas#
- Frontmatter is YAML, not Markdown. Indentation matters. Commas inside
allowed-toolsarrays need correct quoting. A subtle YAML error fails the command silently — the body loads but the metadata is ignored. $ARGUMENTSis literal substitution. The body sees the raw string the user typed. There is no escaping, no parsing, no validation. If the user types'; rm -rf /; 'and your prompt blindly tells the model “run $ARGUMENTS as a Bash command”, that’s a footgun. Treat arguments as untrusted input.- Project commands shadow user commands. A repo’s
.claude/commands/review.mdwill hide your personal~/.claude/commands/review.mdwhile you’re inside that repo. Surprising at first; intentional for team-shared workflows. - Slash commands inherit the session context. If you run
/review-pr 123after a long unrelated debugging conversation, the review will be coloured by everything that came before. For cleanest results,/clearbefore invoking — or use a sub-agent. - No live reload on edit. If you edit a slash-command file mid-session, Claude Code may not pick up the change until you restart the CLI. When iterating on a command, restart between edits.
- The autocomplete list can get crowded. A repo with thirty slash commands becomes hard to navigate. Curate. Delete ones nobody uses.
- Argument hints are unenforced.
argument-hint: <PR number>is just documentation. The command will run with any argument, including none. Build defensive checks into the body if the absence of arguments would produce nonsense. - Model overrides cost. A command with
model: claude-opus-4-7will always run on the most expensive tier, even for trivial questions. Reserve overrides for commands where the extra capability actually matters.
Related features#
- Slash Commands
- Custom Sub-Agents
- CLAUDE.md and Memory Files
- Settings and Configuration
- Hooks Overview
When a slash command should become a sub-agent
A useful heuristic: if you find yourself adding /clear before your slash command “so the context is clean”, that’s the signal the work belongs in a sub-agent. Sub-agents start in a fresh context window by construction; you don’t have to remember to clear. Conversely, if your slash command genuinely benefits from seeing the conversation so far (a review command that uses what the user was just working on), it belongs as a slash command. The dividing line is whether the work needs the session’s context or wants to escape it.