Custom Slash Commands & Skills
Package your own workflows
- Place a custom command in the right directory and predict its exact /name from the filename
- Write YAML frontmatter (description, allowed-tools, model, argument-hint) that scopes a command correctly
- Use $ARGUMENTS, positional $0/$1, backtick-bang bash interpolation, and @file embedding to build dynamic prompts
- Decide when a workflow should be a model-invokable skill versus a manual-only command, and use disable-model-invocation to force manual
- List, sort, and hot-reload your commands with /skills and /reload-plugins
Custom slash commands and skills let you package a workflow once — the prompt, the tools it may use, the files and shell output it needs — and replay it with a single /name. This lesson covers where these files live, their YAML frontmatter, dynamic arguments and bash/file interpolation, and the one decision that separates a skill from a plain command: whether Claude is allowed to invoke it on its own.
- 1The mental model: a saved prompt that can carry tools and context
- 2Where commands live, and how the name is derived
- 3YAML frontmatter: the four keys
- 4Dynamic arguments: $ARGUMENTS and $0/$1
- 5Live context: bash interpolation and @file embedding
- 6Skills vs. commands: who pulls the trigger
- 7Listing and reloading: /skills and /reload-plugins
The mental model: a saved prompt that can carry tools and context
You have already written the perfect prompt three times this week — "look at the git diff, review it for security issues, and use only read-only tools." A custom slash command is that prompt, saved to a file, replayed with /name. Nothing more mysterious than a snippet.
But a Claude Code command is richer than a text snippet, because it can carry three things a bare prompt can't:
- Tool permissions — declare exactly which tools the command may use, so it runs without prompting you.
- Live context — splice in the output of a shell command, or the contents of a file, at the moment you invoke it.
- Arguments — pass values on the command line (
/fix-issue 123 high) and slot them into the prompt.
The file is just Markdown — a body (the prompt Claude receives) and an optional YAML frontmatter block at the top (the configuration). That's the whole format.
The one idea to hold onto before the details: a command is a prompt you trigger by typing /name. A skill is the same file in a slightly different place that Claude can also trigger on its own when the task fits. Same content, same /name invocation — the difference is who gets to fire it. We'll return to that distinction at the end; everything in between applies to both.
Key insight
Command, skill — same machinery
Don't overthink the vocabulary. A SKILL.md and a legacy commands/*.md use the same frontmatter keys and the same dynamic features ($ARGUMENTS, backtick-bang, @file). The only real difference is the directory it lives in and whether Claude may invoke it autonomously. Learn the machinery once; it applies to both.
Where commands live, and how the name is derived
There are two formats and two scopes. The format determines whether Claude can invoke it autonomously; the scope determines who can see it.
| Location | Format | Scope | Autonomous? |
|---|---|---|---|
.claude/skills/<name>/SKILL.md | Skill (recommended) | Project (in git) | Yes — model-invokable |
~/.claude/skills/<name>/SKILL.md | Skill (recommended) | Personal (all projects) | Yes — model-invokable |
.claude/commands/<name>.md | Command (legacy) | Project (in git) | No — manual only |
~/.claude/commands/<name>.md | Command (legacy) | Personal (all projects) | No — manual only |
The official guidance is explicit: .claude/commands/ is the legacy format; the recommended format is .claude/skills/<name>/SKILL.md, which supports the same /name invocation plus autonomous invocation by Claude. The CLI still fully supports both, so you'll see plenty of commands/ directories in the wild.
How the name is derived:
- For a legacy command, the filename minus
.mdbecomes the command name..claude/commands/refactor.md→/refactor. - For a skill, the directory name is the command name.
.claude/skills/refactor/SKILL.md→/refactor. (The file inside is always literallySKILL.md.)
Project-scoped files (under ./.claude/) are checked into git, so the whole team gets them. Personal files (under ~/.claude/) follow you across every project on your machine.
Subdirectories organize, but do NOT namespace. As your library grows you'll want folders, and custom commands support them:
.claude/commands/
├── frontend/
│ ├── component.md # Creates /component (project:frontend)
│ └── style-check.md # Creates /style-check (project:frontend)
├── backend/
│ ├── api-test.md # Creates /api-test (project:backend)
│ └── db-migrate.md # Creates /db-migrate (project:backend)
└── review.md # Creates /review (project)Here is the gotcha that trips people up: the subdirectory appears in the command's description, but it does NOT become part of the command name. .claude/commands/backend/api-test.md is invoked as /api-test, not /backend/api-test or /backend:api-test. The backend/ folder surfaces as context in the description (handy when scanning / autocomplete) but the name comes from the filename alone. The practical consequence: folders organize, they don't disambiguate. frontend/test.md and backend/test.md both want to be /test — and collide. Keep leaf filenames unique within a scope.
Tip
Start with a command, graduate to a skill
A one-line .claude/commands/foo.md is the fastest way to prototype — no subdirectory needed. When the workflow earns its keep and you want Claude to reach for it automatically, move it to .claude/skills/foo/SKILL.md. The body and frontmatter carry over almost verbatim.
Note
One name wins
If a project command and a personal command share a name, only one /name exists. Keep names distinct, or expect the scopes to collide. Use /skills (covered later) to see exactly what resolved. The same is true across subdirectories — the folder never changes what you type.
YAML frontmatter: the four keys
The body of the file is the prompt. The optional YAML frontmatter at the top — fenced by --- lines — configures the command. Four keys matter:
| Key | Purpose | Example |
|---|---|---|
description | One line shown in / autocomplete and to Claude when deciding to invoke a skill | Run a security vulnerability scan |
allowed-tools | Tools the command may use without prompting you (permission-rule syntax) | Read, Grep, Glob, Bash(git diff *) |
model | Pin a specific model for this command | claude-opus-4-7 |
argument-hint | Placeholder text shown in autocomplete to remind you what args to pass | [issue-number] [priority] |
A fully-configured command looks like this:
---
allowed-tools: Read, Grep, Glob
description: Run security vulnerability scan
model: claude-opus-4-7
---
Analyze the codebase for security vulnerabilities including:
- SQL injection risks
- XSS vulnerabilities
- Exposed credentials
- Insecure configurationsTwo things deserve emphasis. First, allowed-tools uses permission-rule syntax, the same scoping you'd write in a permissions rule — so Bash(git status *) permits only git status ..., not arbitrary shell. Scope it tightly; a command that needs to read and diff doesn't need write access. Second, description is load-bearing for skills: it's the text Claude reads when deciding, on its own, whether this skill fits the task at hand. A vague description means a skill that never fires (or fires at the wrong time). Write it like a trigger sentence.
Watch out
allowed-tools is not a free pass to run anything
allowed-tools: Bash grants the whole Bash tool without prompts — usually more than you want. Prefer narrow rules like Bash(git add *), Bash(git commit *). The command can still only do what its prompt directs, but a tight allowed-tools is your guardrail if the prompt (or an argument) goes somewhere unexpected.
Dynamic arguments: $ARGUMENTS and $0/$1
A static prompt is useful; a parameterized one is reusable. Custom commands support two placeholder styles, and you'll often combine them.
$ARGUMENTS — everything you type after the command name, as one string. Good when the argument is free-form:
---
argument-hint: [test-pattern]
description: Run tests with optional pattern
---
Run tests matching pattern: $ARGUMENTS
1. Detect the test framework (Jest, pytest, etc.)
2. Run the matching tests
3. If they fail, analyze, fix, and re-run to verifyInvoke with /test auth and $ARGUMENTS becomes auth.
Positional $0, $1, $2, … — individual arguments by index, split on spaces. Good when arguments have distinct roles:
---
argument-hint: [issue-number] [priority]
description: Fix a GitHub issue
---
Fix issue #$0 with priority $1.
Check the issue description and implement the necessary changes.Invoke with /fix-issue 123 high and the prompt resolves to "Fix issue #123 with priority high." — $0 is 123, $1 is high.
The two styles answer different questions. Use $ARGUMENTS when you want the whole tail (a search query, a free-text description). Use positional when each slot means something specific and you want to phrase them differently in the prompt. The argument-hint frontmatter is purely a UX nicety — it shows the expected shape in autocomplete but enforces nothing.
Example
Both at once
Nothing stops you from using $0 for the first, role-specific argument and $ARGUMENTS for the rest of the line — but they overlap ($ARGUMENTS includes $0). Pick the style that reads cleanest for the command and stay consistent. For a single free-form input, $ARGUMENTS is almost always the right call.
Live context: bash interpolation and @file embedding
The most powerful feature is injecting fresh context at invocation time, so Claude starts the task already holding the relevant state. Two mechanisms:
Backtick-bang — !`command` — runs a shell command and inlines its stdout into the prompt before Claude sees it. This is how you hand Claude the current git status or diff without it having to go fetch them:
---
allowed-tools: Bash(git add *), Bash(git status *), Bash(git commit *)
description: Create a git commit
---
## Context
- Current status: !`git status`
- Current diff: !`git diff HEAD`
## Task
Create a git commit with an appropriate message based on the changes above.When you type /git-commit, the !`git status` and !`git diff HEAD` run first; their output is spliced into the text, and Claude receives a prompt that already contains the live diff. Note the frontmatter — for the backtick-bang commands to run without prompting, the relevant Bash(...) rules must be in allowed-tools.
@path embeds a file's contents directly into the prompt:
---
description: Review configuration files
---
Review the following configuration files for issues:
- Package config: @package.json
- TypeScript config: @tsconfig.json
Check for security issues, outdated dependencies, and misconfigurations.Here @package.json and @tsconfig.json are replaced by the actual file contents at invocation. The distinction is worth keeping straight: ! runs a command and inlines its output; @ reads a file and inlines its text. Both happen before Claude reasons, which is exactly what makes the command feel like it already understands your situation.
Watch out
Mind your secrets
@.env or !`printenv` will splice real secret values straight into the conversation transcript on disk. Never embed credential-bearing files or commands that dump them. Embed @package.json, not @.env; run !git status``, not a command that prints tokens.
Tip
Why interpolation beats 'go read it'
You could write a prompt that says 'run git diff and review it,' and Claude would. But !`git diff HEAD` puts the diff in the prompt deterministically, every time, with zero extra tool round-trips — faster, cheaper, and immune to Claude deciding to look somewhere else first.
Skills vs. commands: who pulls the trigger
Now the distinction we deferred. Mechanically, skills and commands share everything — frontmatter, arguments, interpolation. The difference is invocation authority.
- A legacy command (
.claude/commands/*.md) fires only when you type/name. It's a macro you trigger by hand. - A skill (
.claude/skills/<name>/SKILL.md) can also be invoked by Claude autonomously — when a task matches the skill'sdescription, Claude can reach for it on its own, no typing required. You can still trigger it manually with/nametoo.
This is why the recommended format is a skill: a well-described skill is a capability Claude discovers and applies exactly when it's relevant, instead of a command you have to remember to run.
Sometimes that autonomy is unwanted — a deploy command, a destructive cleanup, anything you want a human firmly in the loop for. To keep a skill manual-only, set disable-model-invocation: true in its frontmatter:
---
description: Tear down and rebuild the local database
disable-model-invocation: true
---
Drop the dev database, re-run all migrations, and reseed.With that flag, the skill is still listed and still works via /db-reset, but Claude will never invoke it on its own. The decision rule is simple: make it a model-invokable skill when you'd be happy for Claude to apply it automatically; make it a command (or a skill with disable-model-invocation: true) when a human must pull the trigger.
Key insight
Description quality = invocation quality
For a model-invokable skill, the description is the trigger. 'Generate API docs' is vague — Claude won't know when it applies. 'Generate OpenAPI docs from the route handlers in src/api when the user asks to document endpoints' tells Claude exactly when to fire. Write descriptions as conditions, not titles.
Listing and reloading: /skills and /reload-plugins
Two commands keep you in control of your library.
/skills lists every skill available in the session, with the tokens each one costs to keep loaded. Because skill definitions consume context, this is also a budgeting tool:
- Press
tto sort by tokens — surface the heaviest skills, the ones worth trimming or making more on-demand. - Press Space to hide a skill you don't want loaded in this session.
Use /skills whenever a command isn't showing up (did the file land in the right place?) or when /context says skills are eating more of your window than you expected.
/reload-plugins hot-reloads all active plugins to apply pending changes without restarting Claude Code — it reports counts for each reloaded component and flags any load errors. This closes the authoring loop: edit a skill file, run /reload-plugins, and try the updated /name immediately, no session restart, no lost context.
Together these two make iterating on a command tight: write the file, /reload-plugins, test, adjust, repeat — and use /skills t to keep the whole set lean.
Tip
The fast authoring loop
Keep one terminal in your Claude session and your editor on the SKILL.md. Edit → /reload-plugins → invoke /name to test → /skills t to check its token cost. You never restart, so your conversation context survives the whole iteration.
Try it: Build a /scope-review command, then promote it to a skill
Package a real workflow end to end. 1) Create a legacy command: make .claude/commands/scope-review.md. Give it frontmatter: a tight allowed-tools (e.g. Read, Grep, Glob, Bash(git diff *)), a description, and an argument-hint like [area]. In the body, use !`git diff --name-only HEAD~1` to inline changed files, @package.json to embed config, and $ARGUMENTS to focus the review on a named area. Invoke it with /scope-review auth and confirm the diff and file contents arrived in the prompt without Claude having to go fetch them. 2) Prove the naming rule: move the file into .claude/commands/review/scope-review.md. Confirm it is STILL invoked as /scope-review, not /review/scope-review, and note where the folder appears in the description. 3) Promote to a skill: recreate it as .claude/skills/scope-review/SKILL.md (directory name = command name). Sharpen the description into a trigger condition (e.g. 'Review the diff for a given area when the user asks to review recent changes'). Confirm via /skills that it's listed; press t to see its token cost. 4) Control invocation: add disable-model-invocation: true, run /reload-plugins, and reason about what changed — the skill still works via /scope-review but Claude can no longer fire it autonomously. 5) Reflect: write three sentences — one feature you used (!, @, or an argument) and what it saved you; why the subdirectory didn't change the name; and one real workflow of yours you'd make a model-invokable skill versus one you'd keep manual-only, and why.
Key takeaways
- 1Recommended location is .claude/skills/<name>/SKILL.md (project) or ~/.claude/skills/ (personal) — model-invokable; .claude/commands/*.md (and ~/.claude/commands/) is the legacy, manual-only format. For legacy, filename minus .md = command name; for skills, the directory name is the command name.
- 2YAML frontmatter keys: description (autocomplete + skill trigger), allowed-tools (permission-rule syntax, scope it tight), model (pin a model), argument-hint (autocomplete shape only).
- 3Dynamic prompts: $ARGUMENTS = the whole tail as one string; $0/$1/$2 = positional args; !`command` inlines a shell command's stdout; @path embeds a file's contents — all resolved before Claude reasons.
- 4Subdirectories (e.g. commands/backend/api-test.md) appear in the description but do NOT namespace the name — it's still /api-test. Folders organize; they don't disambiguate, so keep leaf names unique.
- 5Skills can be invoked by Claude autonomously (driven by their description); commands are manual-only. Set disable-model-invocation: true to keep a skill manual-only for risky or destructive workflows.
- 6/skills lists skills (t = sort by tokens, Space = hide); /reload-plugins hot-reloads pending changes without restarting — the core authoring loop.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You create the file .claude/commands/backend/api-test.md. How do you invoke it, and what role does the 'backend' folder play?
2.In a command file you write the line: Current status: !`git status`. What happens when you invoke the command?
3.You have a skill that drops and rebuilds the local database. You want it available via /db-reset but you never want Claude to run it on its own. What's the correct approach?
4.Your /skills list shows the context window filling up, and after editing a SKILL.md you want to test the change without losing your conversation. Which commands help, and how?
Go deeper
Hand-picked sources to keep learning
Built-in commands plus the custom-command section: file locations, frontmatter, and how the recommended SKILL.md format relates to legacy commands/.
The authoritative source for custom-command syntax: $ARGUMENTS, positional $0/$1, !`bash` interpolation, @file embedding, frontmatter examples, and subdirectory namespacing.
The recommended SKILL.md format: autonomous (model) invocation, description as trigger, and disable-model-invocation for manual-only skills.
Companion read: when project knowledge belongs in CLAUDE.md versus a packaged skill or command.
When to reach for a skill (on-demand knowledge) versus a hook (guarantees) versus CLAUDE.md (advice).
Source repository, issues, and release notes for the CLI.