teachyou.ai academy
← All posts
Claude Codedeveloper toolsAI coding assistantCLI workflowsproductivity

Building Custom Claude Code Slash Commands

Pramod Dutta · Jul 5, 2026 · 14 min read

Claude Code slash commands let you save a prompt once and run it forever with a single line like /review-pr or /gen-tests. They are plain Markdown files stored in a .claude/commands/ folder, so creating one takes no more effort than writing a note. This guide walks through the file format, arguments, namespacing, bash and file injection, and a handful of real commands you can copy into a project today.

What Claude Code slash commands actually are

A slash command is a Markdown file. That is the entire mental model. When you type /fix-issue 142 in the Claude Code CLI, Claude Code looks for a file named fix-issue.md, reads its contents as a prompt template, substitutes any arguments you passed, and sends the result to the model as if you had typed it yourself.

There are two kinds of slash commands:

  • Built-in commands that ship with Claude Code, such as /clear, /compact, /model, /agents, /permissions, /init, /review, and /cost. These are implemented in the CLI itself and are not files you can edit.
  • Custom commands that you author, stored as Markdown files under .claude/commands/ in a project or ~/.claude/commands/ in your home directory.

This article is about the second kind: custom claude code slash commands you write yourself.

The value of custom slash commands is not that they let you do something impossible otherwise, you could always paste the same prompt every time. The value is that they turn a paragraph of context (the coding standards, the exact steps, the tone you want, the tools that are allowed) into a name you can type in two seconds. That matters more the larger your team gets, because a slash command committed to the repo becomes a shared, versioned habit instead of something living only in one engineer's head.

Where slash command files live

Claude Code checks two locations for command files, and the distinction is the first thing to get right.

Project commands live in .claude/commands/ at the root of your repository. Because this folder is inside the project, it gets committed to git along with everything else. Anyone who clones the repo and runs Claude Code inside it immediately has access to the same commands. This is the right place for anything tied to how your specific codebase works: a command that runs your test suite in a particular way, a command that follows your team's PR description template, a command that knows your deploy steps.

Personal commands live in ~/.claude/commands/, under your home directory. These are available in every project you open Claude Code in, regardless of which repo you are sitting in. This is the right place for commands about how you personally like to work: a commit message generator styled the way you write, a command that summarizes a long file before you read it, a scratch command for drafting release notes.

Claude Code shows the scope in the command list. When you run /help or start typing /, project commands are labeled (project) and personal commands are labeled (user). If a project command and a personal command share the same name, the project one wins for that repo, which lets a team override an individual's personal default without asking them to delete anything.

A minimal setup looks like this:

your-repo/
  .claude/
    commands/
      fix-issue.md
      commit.md

There is no registration step. Drop a .md file in that folder and it is immediately available the next time you type /.

The simplest possible command

Create .claude/commands/explain.md with nothing but a prompt:

    Explain what the following code does, in plain language,
    as if talking to a junior engineer who is new to this codebase.

Now /explain in the CLI sends that exact instruction to Claude. That is a complete, working slash command. Everything else in this guide is additive on top of this base case: arguments, frontmatter, bash injection, file injection, and namespacing.

Passing arguments into a command

Static prompts are useful but limited. Most real commands need to take input, like an issue number, a file path, or a short description. Claude Code gives you two ways to do this.

`$ARGUMENTS` captures everything typed after the command name as a single string. Given .claude/commands/fix-issue.md:

    Find and fix issue #$ARGUMENTS.

    Steps:
    1. Search the codebase for code related to this issue.
    2. Identify the root cause, not just the symptom.
    3. Write a fix with a short test that would have caught it.
    4. Summarize the change in two sentences.

Typing /fix-issue 142 replaces $ARGUMENTS with 142. Typing /fix-issue auth token refresh loop replaces it with the whole phrase auth token refresh loop.

Positional arguments ($1, $2, $3, and so on) let you split input into named slots, similar to shell script arguments. This is useful when a command always expects a fixed shape of input:

    Review the diff between $1 and $2.

    Focus only on $3 if it is provided, otherwise review the
    whole diff. Flag anything that looks like a security issue,
    a missing test, or an unhandled error path.

Typing /review-diff main feature/payments security fills $1 with main, $2 with feature/payments, and $3 with security. This is more precise than $ARGUMENTS when a command has a fixed contract, and it reads more like calling a function than pasting free text.

You can document the expected shape with the argument-hint frontmatter field, covered next, so the CLI shows you the expected arguments as you type.

Frontmatter: description, tools, and argument hints

Every command file can start with YAML frontmatter, the same pattern used in most static site generators. It is optional, but for anything you plan to keep or share, fill it in.

    ---
    description: Review a diff between two branches for security and test gaps
    argument-hint: [base-branch] [compare-branch] [focus-area]
    allowed-tools: Bash(git diff:*), Read, Grep
    model: claude-sonnet-4-5
    ---

    Review the diff between $1 and $2.

    Focus only on $3 if it is provided, otherwise review the
    whole diff. Flag anything that looks like a security issue,
    a missing test, or an unhandled error path.

Each field does one job:

  • `description` is what shows up next to the command name when you browse / in the CLI. Without it, Claude Code falls back to the first line of the file, which is usually worse. Always set this.
  • `argument-hint` shows the expected arguments as placeholder text while you type the command, so you and your teammates do not have to open the file to remember the order of $1, $2, $3.
  • `allowed-tools` restricts which tools Claude is permitted to use while running this command, scoped down from your normal permission settings. This is the field that matters most for safety: a command meant only to read and summarize code has no business being allowed to run arbitrary bash or write files, so lock it down explicitly rather than relying on your general project permissions.
  • `model` pins the command to a specific model regardless of whichever model your session is currently using. This is handy for commands you want to always run cheap and fast (a quick lint-style pass) or always run on your strongest model (an architecture review before a release).
  • `disable-model-invocation` (set to true) prevents Claude itself from calling this command as a tool during an agentic turn, restricting it to manual, human-typed invocation only. This matters once you understand that slash commands are not purely a human-facing feature, covered below.

Slash commands are also tools Claude can call itself

This is the part that surprises people coming from other CLIs. A custom slash command with a description in its frontmatter is not just something you type, it is also something Claude Code can decide to invoke on its own mid-conversation, the same way it decides to call Read or Bash. If you ask Claude to "review this PR the way we always do" and a /review-pr command exists with a clear description, Claude may choose to run it itself as part of fulfilling your request.

This is genuinely useful: it means your team's house style for reviews, tests, and commits gets applied consistently even when nobody explicitly typed the command. It also means the description field is doing double duty, it is documentation for humans and a decision signal for the model, so write it as a clear, specific sentence about what the command does and when it applies, not a vague label.

If you have a command that should never run without an explicit human decision (something destructive, something that posts externally, something expensive), set disable-model-invocation: true so it only fires when a person types the slash themselves.

Injecting bash output and file contents

Two prefixes let a command pull in live context instead of relying on Claude to go fetch it with separate tool calls.

Bash injection with a leading ! runs a shell command before the prompt is sent, and splices its stdout directly into the text Claude sees. This requires Bash to be present in allowed-tools for that command, since running it is a real shell execution, not a suggestion.

    ---
    description: Draft a commit message from the currently staged diff
    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`

    Write a commit message for these changes. Use a short
    imperative subject line under 60 characters, then a blank
    line, then two or three sentences on the why, not the what.
    No em dashes. No mention of any AI tool.

Typing /commit now hands Claude the actual current diff instead of asking it to go run git diff itself, which saves a full tool-call round trip and guarantees the command always looks at exactly the state you intended.

File injection with a leading @ pulls in the contents of a specific file by path:

    Compare the implementation in @src/auth/session.ts against
    the interface described in @docs/auth-spec.md and list any
    place they have drifted apart.

Both prefixes can appear multiple times in the same command file, and can be mixed with $ARGUMENTS or $1/$2 so the injected content and the user-supplied arguments sit in the same prompt.

Namespacing commands with subdirectories

Once a project accumulates more than five or six commands, a flat list gets hard to scan. Subdirectories inside .claude/commands/ give you namespacing for free, no extra configuration:

.claude/commands/
  frontend/
    component.md
    story.md
  backend/
    migration.md
    endpoint.md
  git/
    commit.md
    pr-description.md

The command names become /frontend:component, /backend:migration, /git:commit, and so on, and the CLI's command list groups them by folder so related commands stay visually together. This is worth doing as soon as you have more than one team or more than one concern generating commands, front-end scaffolding and backend scaffolding rarely benefit from sitting in the same flat namespace.

MCP servers can add slash commands too

If your project connects to an MCP server that exposes prompts, those prompts show up automatically as slash commands in the form /mcp__servername__promptname. You do not author these as Markdown files, they come from the server's prompt definitions, but they behave the same way at the point of use: type the slash, fill in any arguments, and the prompt runs. This is worth knowing so you are not surprised by commands appearing in your / list that you did not write yourself, and so you know where to look (the MCP server's configuration, not .claude/commands/) if you need to change one.

A few commands worth building first

If you are starting from zero, these four cover most of the recurring prompts engineers end up retyping by hand.

A test generator that follows your project's actual test conventions instead of a generic pattern:

    ---
    description: Generate tests for a file following this repo's test conventions
    argument-hint: [file-path]
    allowed-tools: Read, Write, Bash(npm test:*)
    ---

    Read @$1 and the closest existing test file in the same
    directory to learn our testing conventions (framework,
    naming, mocking style). Write tests for every exported
    function and every branch in $1 that isn't already covered.
    Run the test file afterward and fix anything that fails.

A PR description writer that reads the actual diff instead of guessing:

    ---
    description: Draft a PR description from the diff against main
    allowed-tools: Bash(git diff:*), Bash(git log:*)
    ---

    Diff against main:
    !`git diff main...HEAD`

    Recent commits on this branch:
    !`git log main..HEAD --oneline`

    Write a PR description with a Summary section (2-3 bullets)
    and a Test Plan section (a checklist). Do not invent
    functionality that isn't in the diff.

A dependency-aware code reviewer scoped to just one file:

    ---
    description: Review a single file for bugs and missed edge cases
    argument-hint: [file-path]
    allowed-tools: Read, Grep
    disable-model-invocation: true
    ---

    Review @$1 line by line. List concrete bugs and missed edge
    cases only, no style opinions, no praise. For each finding
    give the line number, the problem, and the fix in one
    sentence.

And a bug report drafter that turns a rough description into something a teammate can act on:

    ---
    description: Turn a rough bug description into a structured bug report
    argument-hint: [rough description]
    ---

    Turn this into a structured bug report: $ARGUMENTS

    Include: Title, Steps to Reproduce (numbered), Expected
    Behavior, Actual Behavior, and a guess at Severity
    (low/medium/high) with one sentence of reasoning.

Commit all four to .claude/commands/ and every engineer on the repo gets them the moment they pull, no setup instructions required beyond having Claude Code installed.

Troubleshooting commands that don't show up or don't behave

A handful of issues account for almost every "my slash command isn't working" report.

  • The command doesn't appear in the `/` list at all. Check the file is actually inside .claude/commands/ (project) or ~/.claude/commands/ (personal), not one level up or down. Also check the extension is .md, not .markdown or no extension.
  • The command appears but arguments aren't substituted. Confirm you're using $ARGUMENTS or $1/$2 exactly, including the dollar sign, and that you're not mixing tabs and spaces in a way that breaks the frontmatter block above it.
  • Bash injection lines print literally instead of running. This almost always means allowed-tools doesn't include Bash (or the specific Bash(command:*) pattern you need), so Claude Code refuses the execution and leaves the raw text in place. Add the tool permission to the frontmatter.
  • A project command isn't overriding a personal command with the same name, or vice versa. Run /help to see the full resolved list with (project) and (user) tags, that tells you unambiguously which file is actually winning.
  • Claude keeps invoking a command you only wanted run manually. Add disable-model-invocation: true to the frontmatter so it only fires when a person types the slash.

FAQ

Where exactly do I put a custom Claude Code slash command file? Project-scoped commands go in .claude/commands/ at your repo root and get committed to git, so the whole team gets them. Personal commands go in ~/.claude/commands/ in your home directory and follow you across every project on your machine. Same file format, different folder.

Can a slash command run shell commands automatically? Yes. Prefix a line with ! and wrap the command in backticks, for example ` !git diff , and Claude Code runs it and splices the output into the prompt before sending it. You must include Bash (or a scoped Bash(git diff:*) pattern) in the command's allowed-tools` frontmatter or the execution will be blocked.

What's the difference between `$ARGUMENTS` and `$1`, `$2`? $ARGUMENTS captures everything typed after the command name as one string, useful for free-form input like a description or a search query. $1, $2, $3 split the input into positional slots, useful when a command always expects a fixed number of distinct inputs, like a base branch and a compare branch.

Can Claude call my custom slash commands on its own, without me typing them? Yes, if the command has a description in its frontmatter, Claude Code can invoke it as a tool during an agentic turn when it judges the command matches what you asked for. Set disable-model-invocation: true in the frontmatter if a command should only ever run when a human explicitly types the slash.

Do slash commands work the same way in every project, or are they project-specific? Both exist. Commands in .claude/commands/ are scoped to that one repository and travel with it through git. Commands in ~/.claude/commands/ are scoped to you personally and apply everywhere you use Claude Code. If both define a command with the same name, the project version wins inside that repo.

How is a namespaced command like `/frontend:component` created? Just by folder structure. Put the file at .claude/commands/frontend/component.md and Claude Code automatically names the command /frontend:component and groups it with other commands from the same subdirectory in the command list. No separate registration or config file is needed.