Custom Slash Commands in Claude Code: A Complete Guide
[01]Why Write Custom Slash Commands
If you keep pasting the same prompt — "review this diff," "write a commit message," "explain this module" — you are already describing a slash command. Write it once in .claude/commands/ and you get three wins: tab-completion, team-shareable via git, and consistent output because the prompt is fixed.
Custom commands also serve as lightweight guardrails. A command with allowed-tools set to Read-only can enforce "review, do not modify" at the harness level, not the model level. That is a stronger guarantee than "please don't change anything" in the prompt.
This guide covers the file anatomy, every frontmatter field that matters, the three argument passing styles, six copy-paste templates, and the scope rules that decide who runs your command.
[02]Anatomy of a Command File
A slash command is a Markdown file under .claude/commands/ (project) or ~/.claude/commands/ (user). Filename becomes the command name — review.md → /review. Subdirectories use : as separator — git/commit.md → /git:commit.
Structure:
---
description: One-line summary shown in /help and autocomplete
allowed-tools: Read, Grep, Glob, Bash(git diff:*)
argument-hint: <file-path or branch>
model: sonnet
---
Your prompt template goes here. It becomes the user message sent to
Claude when the command runs.
Use $ARGUMENTS to interpolate whatever the user typed after the command.
Everything above the second --- is YAML frontmatter. Everything below is the prompt body sent verbatim to Claude, with $ARGUMENTS substituted for whatever the user typed after the command name.
[03]Frontmatter Fields Reference
| Field | Required | What it does |
|---|---|---|
description | Yes | Shows in /help, autocomplete tooltip, and team docs |
allowed-tools | No | Restrict which tools Claude can use. Tighter than session-wide permissions |
argument-hint | No | Autocomplete hint shown after the command name (e.g. <file>) |
model | No | Force a specific model for this command (opus, sonnet, haiku) |
disable-model-invocation | No | Prevent the AI from auto-calling this command — user must type it explicitly |
allowed-tools Syntax
Takes a comma-separated list of tool names, with optional patterns in parentheses for Bash:
allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*)
Anything outside this list triggers a permission prompt even if the session-wide config would auto-approve. Use this to lock down destructive commands inside high-trust command templates.
[04]Passing Arguments
Three styles, from simplest to most powerful:
Style 1: Plain $ARGUMENTS
Everything after the command name goes into $ARGUMENTS verbatim.
// .claude/commands/explain.md
---
description: Explain a file in plain English
argument-hint: <file-path>
---
Read the file at $ARGUMENTS and produce a 5-sentence plain-English
explanation aimed at a new team member.
Usage: /explain src/auth/session.ts
Style 2: Positional $1 $2
Reference individual space-separated tokens. Skipped if your version does not support them — fall back to parsing $ARGUMENTS.
---
description: Compare two git branches on a specific path
argument-hint: <branch-a> <branch-b> [path]
---
Compare git branch $1 against $2, focusing on the path $3 if provided.
Summarize the diff in three bullet points.
Usage: /compare-branches feature/auth main client/src/auth/
Style 3: Embedded Bash with !
Prefix a line with ! to run bash at expansion time and splice the output into the prompt. Useful for grounding the prompt in real repo state.
---
description: Draft commit message from current staged changes
allowed-tools: Bash(git diff:*), Bash(git status)
---
Current git status:
!`git status --short`
Staged diff:
!`git diff --cached`
Draft a conventional commit message under 72 chars.
This runs the bash commands first, then hands Claude a prompt pre-populated with real state. No ambiguity about "what's staged."
[05]Six Copy-Paste Templates
1. /review — PR review against main
---
description: Review current branch vs main
allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*)
argument-hint: <focus-area optional>
---
Compare the current branch to main.
!`git diff main...HEAD --stat`
Focus: $ARGUMENTS (default: security, performance, test coverage)
Report findings as a numbered list with file:line references.
End with a PASS/FAIL verdict.
2. /commit — draft conventional commit
---
description: Draft a conventional commit message from staged changes
allowed-tools: Bash(git diff:*), Bash(git status)
---
Current staged changes:
!`git diff --cached`
Draft a single-line conventional commit (< 72 chars) plus an optional
two-line body. Do not run git commit.
3. /explain — plain-English walkthrough
---
description: Explain a file or symbol in plain English
allowed-tools: Read, Grep
argument-hint: <file-path>
---
Read $ARGUMENTS and produce a 5-sentence explanation for a developer
new to this codebase. Avoid jargon. Name the three most important
functions and what calls them.
4. /doc — generate JSDoc / TSDoc
---
description: Add JSDoc to an undocumented function
allowed-tools: Read, Edit
argument-hint: <file:line>
---
Open the file in $ARGUMENTS and add TSDoc comments to every exported
function. Keep comments under 3 lines. Follow the existing style in the
file.
5. /fix-types — resolve TypeScript errors
---
description: Run typecheck and fix errors
allowed-tools: Read, Edit, Write, Bash(npm run check), Grep
---
!`npm run check 2>&1 | head -60`
Fix every TypeScript error above with the minimal possible change.
Do not add `any`. Do not widen types. If a fix is non-obvious, leave
it and report what is stuck.
6. /deploy — preview deploy (read-only metadata)
---
description: Show deploy metadata for the current branch
allowed-tools: Bash(git branch:*), Bash(git rev-parse:*), Read
argument-hint: <env: preview|production>
---
Branch: !`git branch --show-current`
Commit: !`git rev-parse HEAD`
Environment requested: $ARGUMENTS
Summarize what would deploy and list any pending migrations. Do NOT
run the deploy — only describe what would happen.[06]Project vs User Scope
Slash commands resolve from two locations:
| Scope | Location | Use for |
|---|---|---|
| Project | .claude/commands/ | Team-shared, committed to git |
| User | ~/.claude/commands/ | Your personal shortcuts across all projects |
When names collide, project wins — that way a team can lock down /deploy with the approved template regardless of what an individual has globally. If you want your personal preferences alongside team commands, use different names (/my-review vs /review).
Namespacing with Subdirectories
Place related commands in a subdirectory and invoke them with : notation:
.claude/commands/
git/
commit.md → /git:commit
review.md → /git:review
test/
unit.md → /test:unit
e2e.md → /test:e2e
Keeps /help readable once you pass ten custom commands.
[07]Next Steps
You have commands running. What compounds them:
- .claude/ folder complete guide — settings.json, hooks, agents alongside commands.
- Hooks cookbook — pair
/reviewwith a PreToolUse hook that enforces type-check before it runs. - Claude Code cheat sheet — quick lookup table for every built-in slash command.
- Top 10 MCP servers — commands that call MCP tools (e.g.,
/github-search) need the right servers wired up.
Write a command worth sharing? Start from the checker and drop it in a blog comment.
[08]Frequently Asked Questions
Can I use custom commands to call other custom commands?
Not directly — slash commands are expanded into a single prompt, they do not recursively invoke each other. If you need composition, create a sub-agent (see .claude/agents/) and delegate. The agent can in turn use any command it has access to.
What happens if my command body has $ in it (not as a variable)?
Escape it by writing $$ or, in bash embedded sections, use \$. The expansion engine treats $ARGUMENTS and $1..$9 as special; everything else should pass through, but prefer to escape when mixing shell commands with prompt text.
Do commands respect session-wide permissions or override them?
allowed-tools in a command is a restriction, not an expansion. If your session already denies Bash(rm *), no command can grant it. If your command declares allowed-tools: Read, that is the ceiling — even if the session allows more.
Can I ship commands as a package for others to install?
There is no official package format as of 2026 Q1. Teams share commands by committing them to the project .claude/commands/ or by publishing a GitHub repo others clone into ~/.claude/commands/. Some community tooling proposes npm-style distribution — watch the Claude Code GitHub for a canonical spec.
Why does my !`command` bash execution not run?
Two common causes: (1) the command you invoked is not in allowed-tools — explicit Bash allowlist required. (2) You wrote !`cmd` on a line that has content before the !; the expansion only triggers when ! is the first non-whitespace character. Fix: put the bash line on its own line, no prefix.