Claude Code Custom Slash Commands: Building Your Own Shortcuts
Why Typing The Same Prompt Twice Is A Bug, Not A Habit
If you've used Claude Code for more than a week, you've noticed a pattern in your own behavior. You type some version of the same instruction over and over: "review this diff for security issues and check for hardcoded secrets," or "write unit tests for this file using our existing test patterns," or "summarize what changed in this PR and draft a commit message." Every time, you retype it slightly differently, and every time, Claude has to re-infer what you actually want from scratch.
That's not a workflow. That's a workaround for a feature you haven't set up yet.
Claude Code ships with a mechanism specifically built to kill this repetition: custom slash commands. You write a prompt once, save it as a markdown file in a .claude/commands folder, and from then on you invoke it with something like /review-security or /test-file. The command expands into your full, carefully worded prompt, optionally injects arguments you pass in, and can even run shell commands or reference other files before Claude sees a single token of it.
This article is a practical, hands-on guide to building your own slash commands — where the files live, the exact syntax they use, how arguments and frontmatter work, and a set of real command files you can copy into your own project today. By the end, you'll have a personal or team-wide command library that turns multi-paragraph prompts into two-word shortcuts.
What A Slash Command Actually Is
Strip away the terminology and a Claude Code custom slash command is just a markdown file. There's no special compiler, no build step, no proprietary format to learn. You create a file, you write a prompt inside it in plain English, and Claude Code reads that file and treats its contents as the message you just typed — with a few extra conveniences layered on top.
There are two places these files can live:
- Project commands — stored in
.claude/commands/inside your repository. These are checked into git, so your whole team gets the same shortcuts the moment they pull the branch. They show up in the/menu labeled as "(project)". - Personal commands — stored in
~/.claude/commands/in your home directory. These follow you across every project on your machine but nobody else sees them. They show up labeled as "(user)".
The filename becomes the command name. A file at .claude/commands/review.md becomes /review. If you organize commands into subdirectories, like .claude/commands/git/commit.md, the command becomes /git:commit — the folder path turns into a namespace prefix separated by colons. This is the mechanism you want to use once you have more than eight or nine commands, because a flat list gets unwieldy fast.
Let's set one up from scratch.
Setting Up Your First Command Directory
Start by creating the folder structure inside your project root:
mkdir -p .claude/commandsNow create a file called explain.md inside it:
---
description: Explain the selected code in plain English
---
Explain what the following code does. Assume the reader is a mid-level
engineer who is new to this codebase but comfortable with the language.
Cover, in order:
1. What problem this code solves
2. The overall control flow, step by step
3. Any non-obvious tricks, edge cases, or gotchas
4. One thing you'd flag for a code reviewer
Keep it under 200 words unless the code is unusually complex.Save that file, restart or reload your Claude Code session, and type /explain at the prompt. Claude Code will pull in the entire body of that markdown file as your instruction. If you had a file open or code selected in context, Claude answers using this exact structure every single time — no more hoping you phrased the ad hoc request the same way you did last Tuesday.
That's the entire mental model. Everything else in this article is refinements on top of this one idea: frontmatter metadata, argument placeholders, shell command execution, and file references.
Frontmatter: Description, Tools, And Model
The block between the two --- lines at the top of a command file is YAML frontmatter, and it's optional but valuable. Three fields matter most in practice.
description — a one-line summary shown in the / autocomplete menu. Without it, Claude Code falls back to showing the first line of the file body, which is usually not written to be a menu label. Always set this explicitly.
allowed-tools — restricts which tools Claude Code is permitted to use while running this specific command. This matters more than it sounds like it does. A command meant purely for analysis — like generating a code review — has no business being able to run Write or Edit. Locking it down means you can safely hand teammates a /review command without worrying it might "helpfully" start rewriting files mid-review.
model — pins the command to a specific model, overriding whatever model the session is currently using. Useful when a command is cheap and mechanical (like generating a commit message) and you want it to always run fast regardless of what model you're chatting with for the harder parts of your session.
Here's a command that uses all three:
---
description: Read-only security review of the current diff
allowed-tools: Read, Grep, Bash(git diff:*)
model: claude-sonnet-4-5
---
Review the current git diff for security issues only. Do not comment on
style, naming, or performance.
Look specifically for:
- Hardcoded secrets, API keys, or credentials
- SQL or command injection risk from unsanitized input
- Missing authorization checks on new routes or handlers
- Sensitive data logged in plaintext
- Unsafe deserialization or eval-like patterns
Output a numbered list. For each finding, give the file, the line, the
risk in one sentence, and a suggested fix in one sentence. If nothing is
found, say so plainly — do not invent findings to seem thorough.Notice the Bash(git diff:*) syntax inside allowed-tools. This is Claude Code's way of scoping the Bash tool down to one specific command pattern, rather than granting blanket shell access. The command can run git diff and its variants, but not rm, not curl, not anything else. This is the difference between a command you trust enough to bind to a single keystroke and one you'll always hesitate before running.
Passing Arguments Into A Command
Static prompts get you most of the way, but the real power shows up once a command can take input. Claude Code supports this through the $ARGUMENTS placeholder, plus positional variables $1, $2, and so on if you need to unpack multiple pieces of input separately.
Suppose you want a command that writes tests for a specific file, and you don't want to hardcode the filename into the prompt. Create .claude/commands/test-file.md:
---
description: Write unit tests for a given file path
argument-hint: [file-path]
---
Write unit tests for the file at $ARGUMENTS.
Rules:
- Use the same test framework and file naming convention already present
elsewhere in this repo. Look at an existing test file first if you are
unsure which one is in use.
- Cover the happy path, at least two edge cases, and one failure case.
- Do not mock things that don't need mocking. Prefer real objects over
mocks when the dependency is cheap to construct.
- Place the new test file next to the source file, or in the existing
test directory if this project uses one.
After writing the tests, run them and show me the output.Now /test-file src/utils/parseInvoice.ts expands $ARGUMENTS into that exact path, and Claude runs the whole prompt against it. The argument-hint field in the frontmatter is what makes the autocomplete menu show /test-file [file-path] instead of just the bare command name, so anyone on your team can tell at a glance what input the command expects.
If you need multiple distinct arguments rather than one blob of text, use positional variables instead:
---
description: Open a PR from the current branch against a target branch
argument-hint: [target-branch] [pr-title]
---
Create a pull request from the current branch targeting $1.
Use "$2" as the PR title. In the body, summarize the actual commits on
this branch (not just the latest one), and include a short test plan
as a checklist. Do not add any AI-attribution footer or co-author trailer
to the PR description.Called as /open-pr main "Fix invoice rounding bug", $1 becomes main and $2 becomes the quoted title. This is exactly the kind of command worth having on hand if you frequently ship small fixes and want the PR description drafted consistently every time, without retyping your formatting preferences in every session.
Letting Commands Run Shell Commands First
Some of the most useful slash commands don't just describe what Claude should do — they gather live information before Claude even starts reasoning. Claude Code supports this with an inline bash execution syntax: any line prefixed with ! inside the command body runs as a shell command, and its output gets substituted directly into the prompt before Claude sees it.
Here's a commit-message command that reads your actual staged diff first:
---
description: Draft a commit message from currently staged changes
allowed-tools: Bash(git diff:*), Bash(git status:*)
---
Here is the current git status:
!`git status --short`
Here is the staged diff:
!`git diff --cached`
Based only on the diff above, draft a commit message. Follow these rules:
- First line under 70 characters, imperative mood ("fix", not "fixed")
- Blank line, then a short paragraph explaining why the change was made,
not just what changed
- No bullet-point recap of every line touched
- No AI attribution, no co-author trailer, no generated-by footer
Show me the message. Do not run git commit yourself — I will review it
and commit manually.Notice the allowed-tools frontmatter is doing double duty here: it grants exactly the two read-only git commands the prompt needs, and nothing more. This is worth doing on every command that touches bash. If a command only ever needs git diff and git status, don't leave the door open to arbitrary shell execution just because it's convenient — scope it down explicitly, the same way you'd scope a database credential to read-only.
The ! syntax works with any shell command, not just git. A command that summarizes failing tests might run !npm test 2>&1 | tail -50` to pull in the tail of the test output. A command that checks for outdated dependencies might run !npm outdated`. The pattern is the same every time: gather facts with bash, then hand Claude a prompt that reasons over those facts instead of guessing at them.
Referencing Files With The @ Syntax
The other piece of context injection worth knowing is the @ file reference. Inside a command body, @path/to/file pulls the contents of that file directly into the prompt, the same way you'd drag a file into the chat manually. This is different from telling Claude to "read the file" as an instruction — with @, the content is already there before Claude starts reasoning, which is faster and more reliable for files you always want in context.
This is especially useful for commands that need to check new code against a standing convention document. Say your team keeps a style guide at docs/api-conventions.md. A command to review new API endpoints against it looks like this:
---
description: Review a new API route against our API conventions doc
allowed-tools: Read, Grep
argument-hint: [file-path]
---
Here are our API conventions:
@docs/api-conventions.md
Review the route defined in $ARGUMENTS against every rule in the
conventions doc above. For each rule, say explicitly whether the code
complies, and if it doesn't, quote the specific line and explain the fix.
Do not paraphrase the conventions doc back to me — I've already read it.
Focus entirely on whether this specific file follows it.This pattern turns a slash command into something closer to a lightweight linter that understands intent rather than just syntax. You're not writing an ESLint rule for "all list endpoints must support pagination" — you're pointing Claude at your prose conventions doc and a specific file, and letting it reason about the gap between them.
Organizing Commands With Namespaces
Once your command library grows past a handful of files, a flat .claude/commands/ directory turns into a junk drawer. Subdirectories solve this, and Claude Code turns the directory structure into a colon-separated namespace automatically.
A layout like this:
.claude/commands/
git/
commit.md
open-pr.md
test/
unit.md
e2e.md
review/
security.md
performance.mdgives you /git:commit, /git:open-pr, /test:unit, /test:e2e, /review:security, and /review:performance. The autocomplete menu groups these visually, so typing /review and pausing shows you both review commands before you commit to one. This is worth doing proactively rather than waiting until the flat list becomes painful — retrofitting a namespace later just means moving files and it's a five-minute job either way, but a well-organized set of commands is also easier to describe to a new teammate joining the project ("everything under /test: handles our testing workflows").
Project Commands vs Personal Commands: Choosing Where To Put Things
The project-versus-personal split is not just a technical detail — it's a decision about what belongs to the team and what belongs to you.
Put a command in .claude/commands/ (project-level, committed to git) when:
- It encodes a convention specific to this codebase — your test framework, your PR format, your directory layout.
- You want every contributor to get the same behavior, including new hires who haven't built up their own habits yet.
- The command references project files with
@, like the API conventions example above — those references only make sense inside this specific repo.
Put a command in ~/.claude/commands/ (personal, not committed) when:
- It reflects your own working style rather than a team standard — maybe you always want explanations pitched at a certain level, or you have a personal debugging checklist you run through regardless of which project you're in.
- You're experimenting with a command and don't want to clutter a shared PR with your work-in-progress prompt engineering.
- The command is genuinely cross-project, like a general "explain this like I'm reviewing it cold" command you use on every codebase you touch.
A healthy setup usually has both: a lean, well-curated set of project commands that every teammate relies on for consistency, plus a personal layer of commands that make your individual sessions faster. Nothing stops a command in one from calling out to conventions defined in the other — they simply layer on top of each other in the / menu, project commands and personal commands sitting side by side.
A Few Commands Worth Building On Day One
If you're starting from zero, here are the categories of commands that pay for themselves almost immediately, roughly in the order most teams end up building them:
- A diff-based commit message generator — the
git diff --cachedexample above. Nearly universal, low risk since it's read-only, and it enforces whatever commit format your team actually wants instead of whatever Claude defaults to. - A scoped code reviewer — security-only, performance-only, or convention-only reviews are more useful than a single "review everything" command, because a focused prompt catches more of what it's looking for and ignores what it isn't asked about.
- A test-writer bound to your framework — pointing at your existing test files so Claude infers the pattern rather than inventing a new one each time.
- A "catch me up" command — something like
!git log --oneline -20`` combined with a prompt asking Claude to summarize recent activity on the branch, useful after stepping away from a project for a few days. - A changelog or release-notes drafter — reads the diff between two tags or branches and drafts notes in your project's existing style.
None of these need to be perfect on the first try. The nice part about a command being a markdown file is that iterating on it is just editing text — tweak the wording, add a rule you forgot, narrow the allowed-tools list, and the next invocation picks up the change immediately. Treat your .claude/commands folder the way you'd treat any other piece of shared tooling: something that gets a little better every time someone on the team notices a gap and fixes the prompt instead of just working around it in the moment.
Common Mistakes That Undercut A Command Library
A few patterns show up repeatedly once teams start building slash commands, and they're worth naming so you can skip them.
- Writing commands that are too broad. A command called
/fixthat's supposed to handle "anything broken" ends up vaguer than just typing the actual request, because Claude has no more information than it would from a blank prompt. Commands work best when they're narrow enough that the description alone tells you exactly what will happen. - Forgetting `allowed-tools` on anything that touches bash. A command with unrestricted tool access is a command you'll hesitate to run, which defeats the purpose of making it a fast shortcut in the first place.
- Not using `argument-hint`. Without it, teammates have to open the file to figure out what input a command expects. It's one line of frontmatter and it removes an entire category of confusion.
- Letting commands rot. A command written against last year's test framework, before you migrated frameworks, will confidently produce tests in the wrong style. Revisit commands when the underlying convention changes, the same way you'd update a linter config.
- Never sharing them. The single biggest waste is building a great personal command in
~/.claude/commands/and never promoting the genuinely reusable ones into the project folder where the rest of the team benefits.
Making This Part Of How You Actually Work
The gap between "I know slash commands exist" and "my team has a command library we rely on daily" is really just repetition applied to the right thing. The first time you catch yourself typing a familiar multi-line prompt, stop and spend three minutes turning it into a file instead. Add the frontmatter, scope the tools, name it something your future self will recognize in the autocomplete menu. That's the whole workflow — there's no larger system to learn beyond markdown files, YAML frontmatter, and two special syntaxes for arguments and shell execution.
Do this consistently for a month and the compounding effect is real: onboarding a new teammate becomes handing them a folder of commands instead of a wiki page of "things we usually ask Claude to do." Code review gets more consistent because the review prompt doesn't drift between sessions. And your own sessions get noticeably faster once the boilerplate instructions are gone and you're typing two words instead of two paragraphs.
If you want a structured, hands-on path through this and the rest of Claude Code's workflow — hooks, subagents, MCP servers, and the full command surface — our Claude Code Tutorial for Beginners course on teachyou.ai walks through building a real command library step by step, alongside the other automation features that make Claude Code far more than a chat window in your terminal.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.
Related reading