Writing Your First Custom Slash Command with Markdown Frontmatter
A custom slash command is a saved prompt you invoke by name instead of retyping.
It is stored as a plain markdown file with a frontmatter header and a prompt body underneath.
Summary
A command file has exactly two parts: YAML frontmatter between --- markers, and a prompt body below it.
The frontmatter carries metadata: a short description, and optionally which tools the command is allowed to use.
The body is the literal text sent into the conversation the moment someone types /command-name.
Commands are scoped either to a project, at .claude/commands/, or to a user, so they are available across every project that user opens.
Because a command is just a file, creating one and sharing one both reduce to normal git operations, no separate registration step exists.
Recipe
---
description: One-line summary shown in command autocomplete
---
The prompt text that gets sent when this command is invoked. Write it exactly
as you would type the request yourself.Save this at .claude/commands/<command-name>.md and it becomes available as /<command-name> in the very next prompt, no restart needed.
When to reach for this:
- A request you find yourself typing more than two or three times.
- A multi-step instruction that is easy to forget a step of if typed from memory.
- A team convention you want every contributor to phrase the same way (a PR review checklist, a commit message format).
- A prompt that should accept a target (a file, a PR number, a feature name) via
$ARGUMENTS.
Working Example
---
description: Review a pull request for common issues before requesting a human reviewer
allowed-tools: Bash(git diff:*), Bash(git log:*), Read, Grep
---
Review the current branch's changes against main before I request a human reviewer.
1. Run `git diff main...HEAD` to see everything that changed.
2. Check for: missing error handling, leftover debug logging, and any TODO comments
that should be resolved before merge.
3. Confirm the changes match what the branch name or last commit message implies.
4. Report findings as a short bullet list, ranked by severity. If nothing stands out,
say so plainly rather than inventing minor nitpicks.Save this as .claude/commands/pre-review.md. Typing /pre-review runs the checklist against whatever branch is currently checked out.
What this demonstrates:
descriptionfrontmatter surfaces in autocomplete so teammates can discover the command without opening the file.allowed-toolsscopes down what the command can do, here restricting git access to read-only diff and log operations plus file reading.- The body reads like a numbered checklist because that is exactly what gets executed, in order, when the command runs.
- No
$ARGUMENTSis used here; the command always operates on "whatever branch is currently checked out," which is a valid, argument-free command shape.
Deep Dive
How It Works
- Claude Code scans the commands directory (project or user scoped) and lists every markdown file it finds as an available
/command-name. - Typing the command inserts the file's body into the conversation exactly as written, at that point in the turn.
- The frontmatter never gets sent as part of the prompt; it configures how the command behaves and appears, not what gets said.
- If both a project-scoped and a user-scoped command share the same filename, the project-scoped one takes precedence for that project.
- Editing the file changes the command's behavior the next time it runs; there is no cache to clear or process to restart.
Frontmatter Fields at a Glance
| Field | Required | Purpose |
|---|---|---|
description | Recommended | One-line summary shown in command autocomplete and listings. |
allowed-tools | Optional | Restricts which tools this command's prompt is permitted to use, narrower than the session default. |
argument-hint | Optional | A short hint shown in autocomplete describing what $ARGUMENTS expects, e.g. <file-path>. |
model | Optional | Pins this command to a specific model instead of inheriting the session's current model. |
Passing Arguments
---
description: Explain what a specific file does and who depends on it
argument-hint: <file-path>
---
Read $ARGUMENTS and explain its purpose, its public exports, and any files
in this repo that import from it. Keep the answer under 150 words.$ARGUMENTSis replaced with everything typed after the command name, so/explain-file lib/auth.tssendslib/auth.tsin place of$ARGUMENTS.- If the command is invoked with no trailing text,
$ARGUMENTSresolves to an empty string, so word the prompt so it still makes sense (or explicitly states a target is required). argument-hintis purely cosmetic. It shows the reader what to type but does not validate or enforce anything at invocation time.
Markdown Notes
---
description: Keep frontmatter minimal - only fields the command actually uses
---
Prefer a short, direct body over a long one. The prompt body is read every
single time the command runs, so verbosity here is a recurring cost, not a
one-time cost.- Frontmatter is standard YAML between
---fences; unknown fields are typically ignored rather than causing an error, but stick to documented fields to avoid surprises. - The body can be as long as any other prompt, including markdown formatting, code fences, and numbered lists; it is sent as plain text, not rendered.
Gotchas
- Forgetting the
descriptionfield - the command still works, but it becomes much harder for teammates to discover what it does from autocomplete alone. Fix: always include a one-linedescription, even for commands you wrote just for yourself. - Treating
$ARGUMENTSas required - if the prompt body assumes a target was always passed and one wasn't, the command runs against an empty string and produces a confusing result. Fix: explicitly instruct the prompt to ask for a target, or state a default behavior, when$ARGUMENTSis empty. - Naming collisions between project and user commands - a project-scoped command silently shadows a same-named user-scoped one, which can surprise someone who has both. Fix: use distinct, descriptive filenames, and check
.claude/commands/for existing names before adding a personal one. - Overly broad
allowed-tools- leaving this unset means the command inherits the full session's tool access, even for a command that only ever needs to read files. Fix: scopeallowed-toolsdown to exactly what the command's steps require. - Committing a command with secrets baked into the prompt - a command file checked into git is visible to everyone with repo access, so embedding an API key or internal URL as a "convenience" leaks it. Fix: reference environment variables or external config from the prompt instead of hardcoding sensitive values.
- Expecting the command to validate its own arguments - a command has no schema or type system for
$ARGUMENTS; it is a raw string substitution. Fix: write the prompt defensively, telling Claude what to do if the argument looks malformed or missing.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Typing the request fresh each time | The request is truly one-off and unlikely to repeat. | You catch yourself typing a near-identical prompt more than twice. |
| A PostToolUse or PreToolUse hook | The action must happen automatically and deterministically, with no judgment involved. | The task requires reasoning, context-gathering, or a decision, since hooks cannot reason. |
| A subagent spawned ad hoc | The task is a one-time, isolated research or exploration job. | The same delegation pattern will be needed repeatedly, which is better captured as a command that spawns a subagent. |
FAQs
Do I need to register a command anywhere besides creating the file?
No. Claude Code discovers commands by scanning the commands directory; creating the markdown file is the entire setup step.
What is the difference between project-scoped and user-scoped commands?
- Project-scoped commands live in
.claude/commands/inside a repo and are shared with anyone who checks that repo out. - User-scoped commands are available across every project a given user opens, regardless of repo.
- A project-scoped command with the same filename takes precedence over a same-named user-scoped one.
Can a command's prompt include code blocks or numbered steps?
Yes. The body is plain text sent into the conversation, so any markdown formatting, including code fences and numbered lists, is preserved and interpreted normally.
What happens if I invoke a command with no arguments but the prompt uses $ARGUMENTS?
$ARGUMENTS resolves to an empty string. The prompt still runs, so it should be worded to handle that case gracefully rather than assuming a value is always present.
Can a command restrict which tools Claude is allowed to use while running it?
Yes, via the allowed-tools frontmatter field, which scopes tool access down for that command's execution, independent of the broader session's permissions.
Is there a limit to how long a command's prompt body can be?
There is no hard documented limit, but a long body is sent in full every time the command runs, so concise prompts are cheaper and easier to maintain than sprawling ones.
Can a command call another command?
A command's prompt body can instruct Claude to perform the same steps another command would, but commands do not directly invoke one another the way a function calls a function; the body is just text.
Does editing a command's markdown file require restarting Claude Code?
No. The file is read at invocation time, so edits take effect on the very next time the command is called.
How do I make a command's purpose discoverable to my team?
Set the description frontmatter field to a clear one-line summary; it is what shows up in autocomplete when a teammate is browsing available commands.
Can I pin a command to a specific model?
Yes, using the model frontmatter field, which overrides the session's current model just for that command's invocation.
Is $ARGUMENTS validated or type-checked in any way?
No. It is a raw string substitution with no schema, so any validation of the argument's shape or content has to be written into the prompt itself.
Should every repeated request become a custom command?
Not necessarily. A command is worth the overhead once a request repeats often enough that saving and naming it pays for itself; a truly one-off request is simpler typed fresh.
Related
- Custom Commands, Hooks, and Subagents: What Each One Is For - how commands fit alongside hooks and subagents.
- Custom Commands & Subagents Basics - a first command and a first subagent, side by side.
- Firing a Shell Command on PostToolUse with Hooks - the automatic counterpart to a command a user has to type.
- Custom Commands, Hooks & Subagents Best Practices - naming and scoping conventions for commands.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5. Model names, pricing, and product features move quickly - verify current specifics at platform.claude.com/docs before relying on them.