BROKE → BUILT EST. 2026 · BUILDING IN PUBLIC · $0 → $4.99
Guides Jul 1, 2026

How to Create a Custom Slash Command in Claude Code

Create a custom slash command in Claude Code: a Markdown file in .claude/commands becomes /name. Covers args, shell injection, frontmatter, and the gotchas.

BROKE → BUILT · GUIDE How to Create a CustomSlash Command in ClaudeCode broke2builtai.com
Short answer

To create a custom slash command in Claude Code, add a Markdown file at .claude/commands/<name>.md in your repo (a project command) or ~/.claude/commands/<name>.md (a personal command). The filename minus .md becomes the command name, so review.md is invoked by typing /review, and the file's contents are used as the prompt. Add $ARGUMENTS or $1, $2 for inputs, a line starting with ! to inject shell output, and optional YAML frontmatter (description, argument-hint, allowed-tools) at the top.

Or skip the work: Meta-Prompt + System Prompt Architect does it in seconds →

I retyped the same “review this diff for correctness bugs, be terse, cite file:line, skip style nits” prompt maybe forty times before it clicked that Claude Code has a built-in fix for exactly this. Custom slash commands. A command is just a Markdown file with a prompt in it — you type /name, it fires, and you never hand-type that paragraph again.

Here’s the whole thing, end to end, including the two gotchas that eat everyone’s first afternoon.

What a custom slash command actually is

A custom slash command is a Markdown file holding a reusable prompt template. That’s it. You drop a .md file in the right folder and its name becomes a slash command you invoke inside Claude Code by typing /name. The file’s contents get sent as your prompt.

The filename IS the command name. .claude/commands/review.md becomes /review. Drop the .md, whatever’s left is your command. No registration step, no config to edit — the file existing in the right place is the whole install.

Two things trip people up, so let’s kill them early: where the file lives, and what goes inside it.

Where to put the file (project vs personal)

Location sets scope. There are exactly two spots that register:

Project command — lives in the repo:

.claude/commands/<name>.md

This shows up as a “project” command, gets committed with the repo, and everyone who clones it gets the command for free. Use it for workflows specific to one codebase — the deploy dance, the test-runner incantation, the PR template your team argues about.

Personal command — lives in your home directory:

~/.claude/commands/<name>.md

This shows up as a “user” command and follows you into every project on your machine. Use it for your own habits — the review style you like, the commit-message format you always want, the “explain this file like I’m tired” prompt.

Making one from scratch is a two-liner:

mkdir -p .claude/commands
cat > .claude/commands/review.md <<'EOF'
Review the current git diff for correctness bugs. Be terse.
Cite file:line for every finding. Skip style nits.
EOF

Open the command menu, type /review, and it runs that prompt against your current session.

Gotcha number one, up front: it has to be .claude/commands (project) or ~/.claude/commands (personal). A stray commands/ folder sitting somewhere else in your repo won’t register, and there’s no error telling you why — the command just silently doesn’t exist. I’ve burned real minutes staring at a file that was one directory off.

Namespacing with subfolders

Once you have more than a handful of commands, a flat folder gets ugly. Subfolders fix it — they namespace with a colon. The folder name becomes a prefix:

.claude/commands/git/commit.md   ->   /git:commit
.claude/commands/git/pr.md       ->   /git:pr
.claude/commands/review/sec.md   ->   /review:sec

I group mine by area — git/, review/, deploy/. When you type /git: the menu filters to that group, which beats scrolling past thirty top-level commands to find the one you want.

Passing arguments

A static prompt is useful. A prompt that takes input is where this stops being a shortcut and starts being tooling. There are two ways to feed a command arguments.

$ARGUMENTS inserts everything typed after the command as one blob:

Write a conventional-commit message for this change: $ARGUMENTS

Then /commit fixes the null deref in the auth guard drops that whole tail into the prompt.

$1, $2, $3 … insert individual positional arguments:

Open a PR from branch $1 into $2 with the title "$3".

Then /pr feature-x main "Add feature X" maps feature-x to $1, main to $2, and the quoted string to $3. Reach for positional args when the shape matters and order is meaningful; reach for $ARGUMENTS when you just want the freeform rest of the line.

Dynamic content: shell output and file contents

Two more injections turn a frozen prompt into a live one that reads the world at the moment you run it.

A line that begins with ! runs a shell command and injects its output straight into the prompt:

Current repo state:
!git status
Summarize what's staged versus unstaged, then suggest the next commit.

Reference a file’s contents with @path:

Review @src/auth.ts for security issues and missing input validation.

Important: the ! shell injection only works if Bash is permitted for that command — otherwise the line prints as literal text instead of running. You grant that in frontmatter (next section). It’s the same permission mindset you use when wiring external tools; if you’ve ever added an MCP server to Claude Code, the “grant the capability or it no-ops” pattern will feel familiar.

Frontmatter: the optional config block

At the very top of the file you can put YAML frontmatter to tune the command. Every field is optional:

  • description — shown in the command menu and help
  • argument-hint — e.g. [pr-number], hints the expected args
  • allowed-tools — restrict which tools the command may use
  • model — pin a specific model for this command
  • disable-model-invocation — stop the model from auto-running it

Here’s a real, working example. .claude/commands/fix-issue.md:

---
description: Fix a GitHub issue by number
argument-hint: [issue-number]
allowed-tools: Bash(gh issue view:*), Read, Edit
---
Fix issue #$1. First run: !gh issue view $1
Then locate the offending code, implement the fix, and explain what changed.

Type /fix-issue 482. It pulls the issue with gh, reads the relevant code, edits it, and explains the change. The allowed-tools line is the load-bearing part — it’s what lets !gh issue view actually execute instead of printing as text. Note how narrow the grant is: Bash(gh issue view:*) permits exactly that command family and nothing else, which is how you keep a command from wandering.

Commands vs Skills — don’t confuse them

Quick correctness note, because people mix these two up: custom slash commands are USER-typed prompt templates. You decide when they fire by typing /. Skills (a SKILL.md file) are MODEL-triggered — Claude decides to invoke them based on context, without you asking. Same underlying idea of packaged, reusable instructions, but the trigger is the whole difference. If you want a thing you personally invoke by typing a slash, that’s a command, and that’s this guide.

The part that makes commands worth building

A slash command is only ever as good as the prompt inside it. A vague /review gives vague reviews. The commands I actually keep around are the ones with tight, specific instructions — hard constraints, an explicit output format, what to cite, what to ignore. That’s real prompt engineering, and it’s the exact same muscle that makes a good project CLAUDE.md file worth having. If you want to sharpen the instructions themselves, my Meta-Prompt Architect pack is the reusable-prompt system I lean on to write these so the words carry weight instead of vibes.

Which raises cost, because once you’ve got five or ten commands you fire them constantly and the token meter moves. I run Claude Code on the z.ai GLM Coding Plan to keep that cheap — the full walkthrough is in the GLM API setup guide. That plan link (https://z.ai/subscribe?ic=BWTG6TRYYQ) is a referral, not a discount — disclosed plainly, and it helps fund our compute. I only mention it because cheap tokens are what make a wall of custom commands practical to run all day.

Troubleshooting the 3 things that actually break

1. The command doesn’t show up. The file isn’t under .claude/commands (project) or ~/.claude/commands (personal). A commands/ folder anywhere else won’t register, and it fails silently. Double-check the exact path before you debug anything else.

2. A ! or @ line prints as literal text. The command lacks permission. Shell injection needs Bash allowed — add allowed-tools: Bash(...) to the frontmatter; file references need Read. No permission means Claude Code treats the line as plain prompt text and moves on.

3. Wrong command name. The name is the filename minus .md, and subfolders add a folder: prefix. If you expected /commit but the file sits in git/, the real name is /git:commit. Rename the file or call it by its namespaced name.

Where this fits

Slash commands are one leg of a Claude Code setup that’s tuned to you instead of generic. The other legs: a solid project instructions file so Claude knows your codebase before you type a word (that’s the CLAUDE.md guide), and MCP servers for the external tools your commands lean on (that’s adding an MCP server). Build a handful of commands for the workflows you actually repeat, keep the prompts sharp, namespace them once they multiply — and you stop retyping the same paragraph for the rest of your life.

Frequently asked

Where do custom slash commands go in Claude Code?

Two locations register. Project commands live in .claude/commands/<name>.md inside the repo and get shared with your team; personal commands live in ~/.claude/commands/<name>.md and follow you across every project on your machine. A commands/ folder anywhere else won't be picked up.

How do I pass arguments to a slash command?

Use $ARGUMENTS to insert everything typed after the command, or $1, $2, $3 for individual positional arguments. So /pr main 'Add feature' maps main to $1 and 'Add feature' to $2. Add an argument-hint in the frontmatter to remind yourself of the expected shape.

What's the difference between a slash command and a skill?

A custom slash command is a user-typed prompt template — you decide when to run it by typing /name. A skill (a SKILL.md file) is model-triggered: Claude decides to invoke it based on context. Same reusable-instructions idea, but the trigger is completely different.

Why is my ! shell command printing as text instead of running?

The command doesn't have permission to use Bash, so Claude Code treats the line as literal text. A line starting with ! only runs if the tool is allowed — add allowed-tools: Bash(...) to the file's frontmatter. The same applies to @file references, which need Read permission.

Some links may be referral links, always marked. Full disclosure →