Last updated: July 2026. Reflects Claude Code 2.1.x, the .claude/skills/ format that replaced .claude/commands/, and the namespacing bug that is still open.
TL;DR
Claude Code slash commands are two different things wearing one name: built-in session controls like /clear, /compact, /context, and /model, and custom commands you write yourself as Markdown files. The custom ones used to live in .claude/commands/name.md; Anthropic now calls that the legacy format and points you at .claude/skills/name/SKILL.md instead. Both still produce /name, both take the same frontmatter (description, argument-hint, allowed-tools, model, disable-model-invocation), and both support $ARGUMENTS, ! for injecting shell output, and @ for injecting files. The single highest-value trick is bash injection: a command that runs !`git diff --cached` before Claude thinks is worth more than a command with a beautifully worded prompt. The thing that will waste your afternoon is subdirectory namespacing, which the docs describe and the CLI does not implement the way they describe it.
The word “slash command” covers two unrelated things
This is the source of most of the confusion I see, so it goes first.
When you type / in a Claude Code session, the autocomplete list mixes two categories that have nothing in common except the prefix. Built-in commands are session controls compiled into the CLI. They manipulate the conversation, the model, or your config. You cannot write one, override one, or change what one does. Custom commands are Markdown files on your disk that expand into a prompt. They are text. Claude reads them the same way it reads anything else you type.
The practical consequence: /compact is not a prompt, it is a mechanism, and you cannot ask Claude to run it for you. Your /deploy command is a prompt, so Claude can invoke it on its own if you let it. Those two facts drive most of the design decisions later in this post.
The built-in commands worth memorizing
There are a lot of built-ins. I use maybe eight of them with any regularity, and three of those are context management, which is where the actual leverage is.
| Command | What it does | When I reach for it |
|---|---|---|
/clear | Wipes conversation history, starts from empty context | New task in the same repo. Stops the last task’s decisions bleeding into this one. |
/compact | Summarizes older messages and replaces them with the summary | Mid-task, when context is filling but I still need the history. |
/context | Shows what is currently eating your context window | Before I decide between /clear and /compact. |
/model | Switches the model for the session | Dropping to a cheaper model for mechanical work. |
/cost | Token usage and spend for the session | After a long agentic run, to see what it cost. |
/rewind | Restores earlier conversation state | When a run went sideways and I want the state from four turns ago. |
/agents | Manages subagent definitions | Setting up a repo, rarely after. |
/init | Generates a CLAUDE.md from the codebase | First run in an unfamiliar repo. The output always needs editing. |
The ordering advice everyone gives is “run /compact when you get the warning.” That is backwards. By the time the warning fires, the compaction has to summarize a context that is already 90% full, and the summary it produces is correspondingly lossy. Check /context around the halfway mark and compact then, while there is still room for a summary that keeps detail. I wrote more about how context windows actually degrade in the post about the anti-hallucination prompt that backfired on me.
One more built-in note: /clear does not destroy anything. The old conversation stays on disk and you can resume it by session ID. It is not a delete, it is a fresh window.
Where custom commands live, and why there are now two answers
Every guide written before 2026 tells you the same thing: drop a Markdown file in .claude/commands/, the filename becomes the command name, done. review.md gives you /review.
That still works. Anthropic’s current docs also label it the legacy format and point you at .claude/skills/<name>/SKILL.md instead. The two formats produce the same slash command and accept the same frontmatter. The difference is that a skill can also be invoked autonomously by Claude when it decides the skill is relevant, whereas the mental model for a command file is that you type it.
| Location | Scope | Committed to git? |
|---|---|---|
.claude/commands/x.md | This project, everyone on the repo | Yes |
.claude/skills/x/SKILL.md | This project, everyone on the repo (current format) | Yes |
~/.claude/commands/x.md | You, every project | No, it is your home dir |
~/.claude/skills/x/SKILL.md | You, every project (current format) | No |
| Plugin | Distributed to other people, namespaced /plugin__command | Separate repo |
My rule after a year of this: anything that encodes how this repo works goes in the project directory and gets committed. Anything that encodes how I work goes in ~/.claude/. The mistake I made early was putting a /test command in my home directory that assumed Vitest. It fired in a Go repo and Claude confidently went looking for a package.json. Personal commands need to be framework-agnostic or they are landmines.
Anatomy of a custom command
A command file is YAML frontmatter plus a prompt body. Here is a complete one:
--- description: Create a git commit with a conventional message allowed-tools: Bash(git add:*), Bash(git commit:*), Bash(git diff:*) argument-hint: [optional message] model: haiku --- ## Staged changes !`git diff --cached` ## Recent commit style !`git log --oneline -10` Write a Conventional Commits message for the staged changes above. Match the style of the recent commits. If $ARGUMENTS is non-empty, use it as the subject line instead of generating one. Commit. Do not push.
The frontmatter fields, in order of how much they matter:
| Field | Purpose | Default if omitted |
|---|---|---|
description | Shown in autocomplete and /help. Also required for Claude to invoke the command itself. | First line of the prompt body |
allowed-tools | Restricts which tools the command can use | Inherits from the conversation |
argument-hint | Shows expected args in autocomplete | None |
model | sonnet, haiku, a full model ID, or inherit | Inherits from the conversation |
disable-model-invocation | true means only a human typing the command can trigger it | false |
description looks like documentation and is actually load-bearing. Claude can invoke your custom commands on its own through the SlashCommand tool, but only ones that have a description populated. Leave it out and your command becomes type-only. That is sometimes what you want, and there is a cleaner way to say so.
disable-model-invocation: true is the cleaner way. Put it on anything with side effects you would not want an agent triggering mid-run: /deploy, /commit, anything that talks to production. The failure mode without it is not dramatic, it is quiet. Claude decides your /deploy command is a reasonable next step in a long agentic loop, and you find out from the notification. Related reading on why I do not let agents run unsupervised: AI coding agents will run a poisoned issue for you.
model: haiku on mechanical commands is the cheapest win in this whole post. Linting, formatting, generating a commit message from a diff — none of that needs a frontier model. The model reverts when the command finishes; it does not stick to your session or your settings.
Bash injection is the feature that actually matters
Everything above is plumbing. This is the part that changes what your commands can do.
Prefix a backtick-wrapped shell command with ! inside a command file and Claude Code runs it before the model sees the prompt, then substitutes the output inline. The model does not decide to run git diff; the diff is already sitting in its context when it starts.
## Context - Branch: !`git rev-parse --abbrev-ref HEAD` - Status: !`git status --short` - Failing tests: !`npm test 2>&1 | tail -40` Fix the failing tests above. Change the implementation, not the assertions, unless the assertion is provably wrong.
The difference in reliability is not subtle. A prompt that says “check the test output and fix the failures” gives the model a choice about how to gather context, and it will sometimes run the wrong command, sometimes run a narrower subset, sometimes decide it already knows. A prompt with the output pre-injected removes the choice. This is the same principle I keep landing on across every agentic setup: constrain the input, do not rely on a better model. I made that argument at length in why the feedback loop beats a smarter model.
Two things that bit me. First, whatever you inject counts against your context window, so !`git diff` on a 4,000-line refactor will eat the session before Claude writes a line. Pipe through head or --stat. Second, the shell command has to be in allowed-tools or the whole thing fails at expansion time, and the error is easy to misread as the command not existing.
File references work the same way with @. Review @src/auth/session.ts against @docs/auth.md pulls both files into the prompt. Useful when the file set is fixed; useless when it is not, because you cannot glob.
Arguments, and a docs inconsistency to watch
$ARGUMENTS captures everything the user typed after the command name. That is the safe one and it behaves the way you expect.
Positional arguments are where I would slow down. Community guides and most examples use $1 for the first argument and $2 for the second. Anthropic’s own SDK page shows an example where /fix-issue 123 high resolves to $0="123" and $1="high" — zero-indexed. I have not found a version note explaining the difference, so I stopped guessing: write a throwaway command whose entire body is first=$1 second=$2 zero=$0, run it with two known arguments, and read what comes back on your version. Thirty seconds, and it beats debugging a command that silently interpolates the wrong string into a gh call.
Worth knowing regardless: if you reference more placeholders than the user supplied, the unmatched ones stay in the prompt as literal text. So a missing argument does not error, it hands Claude a prompt containing the characters $2. If your command has optional arguments, say so in the body — “if $2 is empty or still reads as $2, default to X.”
Namespacing: the documented behavior and the real behavior
You can organize commands into subdirectories:
.claude/commands/ ├── frontend/ │ ├── component.md │ └── style-check.md ├── backend/ │ └── db-migrate.md └── review.md
What the current docs say happens: the subdirectory shows up in the command’s description, but the command name is unchanged. frontend/component.md gives you /component, not /frontend:component.
Older documentation described a colon-namespaced form, /project:frontend:component, and there is an open issue on the Claude Code repo (#2422) from people who tried it, got “command not found,” and reasonably assumed they had misconfigured something. If you read a guide that promises colon namespacing, that guide is describing documentation rather than behavior.
Which leaves you with a flat namespace, so two commands with the same filename in different subdirectories collide. Prefix the filenames instead — fe-component.md, be-migrate.md. Ugly, works today.
Plugin commands are the exception and are properly namespaced as /pluginname__commandname, which is why installing two plugins that both ship a /review does not break anything.
One more collision to know about: Claude Code ships bundled skills including code-review and verify. Create .claude/commands/code-review.md and yours shadows the bundled one silently. The command list shows the name once and you have no indication which one you are running. If you want your own review workflow, give it a different name. I go through what I actually run before merging agent-written code in this post on AI code review tools.
Restricting tools, and the allowed-tools syntax question
allowed-tools is the guardrail that makes a read-only command actually read-only. allowed-tools: Read, Grep, Glob on an audit command means it physically cannot edit a file, which matters more than a prompt that politely asks it not to.
Anthropic’s documentation examples use comma-separated lists, including for scoped bash permissions like Bash(git add *), Bash(git status *). Some community guides insist the field is space-separated and that commas silently whitelist nothing. I have not been able to reproduce the silent-failure claim, and the official examples use commas, so commas are what I write. But if you ever hit a command that asks for permission on a tool you explicitly allowed, that is the first line to test — swap the separator before you go looking for anything more exotic.
Scoped bash patterns are the part worth getting right. Bash(git:*) allows every git subcommand, including git push --force. Bash(git add:*) and Bash(git commit:*) as separate entries is more typing and considerably narrower. If you want enforcement that survives a command file someone edits later, that belongs in hooks rather than frontmatter — I covered that setup in Claude Code hooks: the guardrails I actually ship.
Running commands without the interactive session
You do not have to open a session to fire a command. claude -p '/lint' runs it headless and exits, which makes commands usable from aliases, git hooks, and CI.
alias clint="claude -p '/lint'" alias ccommit="claude -p '/commit'"
This is where model: haiku compounds. A pre-commit alias that runs a Haiku-backed lint-and-fix costs close to nothing and finishes in the time it takes to switch windows. The same alias on a frontier model is a habit you will drop within a week because of the latency.
Two cautions. Headless runs still hit whatever permissions your settings enforce, so a command that needs approval will hang waiting for an answer nobody is there to give — allowed-tools is not optional in this mode. And if you are wiring this into CI, remember the output is a full model response, not a exit code you can branch on, unless your command explicitly instructs the model to end with a machine-readable line.
What I would actually build first
If you are starting from zero, resist writing fifteen commands in an afternoon. Most of them will never fire. The ones that stuck for me share a shape: repetitive, mechanical, and dependent on context I would otherwise have to paste in by hand.
- A commit command. Injects
!`git diff --cached`and!`git log --oneline -10`, writes a message matching your repo’s existing style. Haiku. This alone justifies the setup. - A test-fix command. Injects the failing test output, tells Claude to fix implementation rather than assertions. Sonnet, because this needs reasoning.
- A read-only audit command.
allowed-tools: Read, Grep, Glob, pointed at whatever you care about — dependency drift, missing error handling, unbounded queries. - A CI-failure command. Injects
!`gh run view --log-failed`. The value is entirely in the injection; a model that has the actual failure log behaves completely differently from one that is guessing.
Notice that three of the four are mostly bash injection and barely any prompt. That is the pattern. The commands that failed for me were the ones where I tried to write a clever prompt and gave the model no fresh context to work from. If you find yourself polishing prose in a command file, you are probably optimizing the wrong half of it. The same lesson shows up in how I trimmed model output generally, which I wrote about in how I stopped Claude from rambling.
Frequently asked questions
Where do I put custom slash commands in Claude Code?
Project commands go in .claude/commands/name.md and are shared with everyone on the repo. Personal commands go in ~/.claude/commands/name.md and follow you across projects. Anthropic now treats both as the legacy format and recommends .claude/skills/name/SKILL.md and ~/.claude/skills/name/SKILL.md instead. Both formats produce the same /name command and the CLI still supports both.
What is the difference between a slash command and a skill?
Functionally they have converged. A file at .claude/commands/deploy.md and a skill at .claude/skills/deploy/SKILL.md both create /deploy and both accept the same frontmatter. The skill format is the current recommendation and carries the expectation that Claude may invoke it autonomously when it judges the skill relevant, while a command file reads more like something you type. If you want the type-only behavior explicitly, set disable-model-invocation: true.
Can Claude run my custom slash commands by itself?
Yes, through the SlashCommand tool, but only for user-defined commands that have the description frontmatter field populated. Built-in commands like /compact and /init are not available to it. To see exactly which of your commands are eligible on your version, run claude --debug and trigger a query. To block a specific command from autonomous invocation, add disable-model-invocation: true.
How do I pass arguments to a Claude Code slash command?
Use $ARGUMENTS in the command body to capture everything typed after the command name. Positional placeholders also work, but the indexing is inconsistent between sources: community guides use $1 for the first argument, while Anthropic’s SDK documentation shows an example resolving the first argument to $0. Test it on your version with a throwaway command before relying on it. Placeholders with no matching argument are left in the prompt as literal text rather than erroring.
Why is my namespaced slash command not found?
Because the colon-namespaced form does not work as older documentation described it. A file at .claude/commands/frontend/component.md creates /component, not /project:frontend:component. The subdirectory affects the description shown in autocomplete, not the command name. This is tracked as an open issue on the Claude Code repository. Since the namespace is effectively flat, prefix filenames to avoid collisions between subdirectories.
How do I run a shell command inside a slash command?
Prefix a backtick-wrapped command with ! in the command body, so that a status check is written as an exclamation mark followed by git status wrapped in backticks. Claude Code executes it before the model sees the prompt and substitutes the output inline. The command must be permitted by allowed-tools or expansion fails. Keep the output small, because injected text counts against your context window — a full git diff on a large change can consume the session before any work happens.
Can I use a cheaper model for a specific slash command?
Yes. Set model in the frontmatter to haiku, sonnet, a full model ID, or inherit. The setting applies only while the command runs and reverts afterward; it is not written to your settings. Mechanical commands like linting, formatting, and commit-message generation run well on the cheapest tier, and the latency difference is what determines whether you keep using them.