KaairosGet the kit
Guide · 6 min read

Claude Code slash commands with arguments

Type input after a command name and Claude Code substitutes it into the prompt. $ARGUMENTS takes the whole string; $0 and $1 pick out positional arguments.

A custom slash command in Claude Code is a Markdown file whose body is a prompt, and you feed it input by typing after the command name. $ARGUMENTS in the body is replaced with everything you type; for several distinct inputs, positional placeholders pick them apart — $0 is the first argument, $1 the second, and so on. Put the file in .claude/commands/ to scope a command to one project, or ~/.claude/commands/ to use it everywhere. So /fix-issue 4192 runs .claude/commands/fix-issue.md with $ARGUMENTS set to 4192.

This guide covers the command file anatomy, the difference between $ARGUMENTS and positional arguments (including the off-by-one that trips people up), where commands live, five commands worth stealing, and when to reach for a skill instead.

The command file anatomy

The whole file can be nothing more than a prompt, but a few optional frontmatter fields make it nicer to use: description shows in the command menu, argument-hint shows the expected input during autocomplete, allowed-tools pre-approves tools so the command does not stop to ask, and model pins a model for that command. The body is the prompt Claude runs, and it can pull in live data: a ` !command line injects the output of a shell command before Claude reads the prompt, and @path references a file. Subdirectories namespace commands, so .claude/commands/git/review.md becomes /git:review`.

---
description: Summarize a file for a new contributor
argument-hint: [path]
---
Read @$0 and explain what it does in plain language: the main
entry points, the data it touches, and anything non-obvious.
Keep it under ten bullet points.

$ARGUMENTS vs positional arguments

Use $ARGUMENTS when the command takes one blob of input — an issue number, a topic, a free-text instruction. Use positional placeholders when it takes several inputs that mean different things, such as a source and a target. The full reference is short:

PlaceholderExpands to
$ARGUMENTSEverything typed after the command, as one string
$0, $1, $2The first, second, third argument (0-based)
$ARGUMENTS[0]The same as $0, written out explicitly
$nameA named argument declared in arguments: frontmatter
argument-hintFrontmatter that shows expected args in autocomplete

Multi-word values use shell-style quoting, so /deploy "us east" prod makes $0 expand to us east and $1 to prod. If you would rather name your inputs than count them, declare arguments: [service, environment] in frontmatter and write $service and $environment in the body; the names map to positions in order, which reads better than $0 and $1 in a longer prompt.

One behaviour worth knowing: an indexed placeholder with no matching argument is left untouched, so $1 stays the literal text $1 when you passed only one argument. That is a good reason to prefer $ARGUMENTS whenever the number of inputs can vary, and to keep positional placeholders for commands that always take a fixed set. The argument-hint field is a small quality-of-life win here too: it shows the expected inputs inline as you type the command, so a compare command can prompt you for [file-a] [file-b] instead of leaving you to remember the order.

The off-by-one to watch forPositional arguments are 0-based: $0 is the first argument, $1 the second. Older guides and existing command files sometimes assume $1 is the first argument — the pre-merge convention. If a positional command grabs the wrong value, that off-by-one is the first thing to check, or sidestep it entirely with $ARGUMENTS or named arguments.

Project vs personal scope

A command in .claude/commands/ belongs to the repository: check it into git and everyone on the team gets it, and it can encode project-specific things like your exact test command. A command in ~/.claude/commands/ is personal and follows you across every project — good for your own habits, like a /scratch or a preferred commit style. When a project and a personal command share a name, the project one takes precedence, which is usually what you want when a repo has its own convention. Subdirectories add a namespace on top of this, so grouping related commands under .claude/commands/git/ gives you /git:review and /git:commit, which keeps the command menu readable once you have more than a handful.

Pull in live context with shell output and file references

Two pieces of syntax turn a static prompt into one grounded in your actual repository, and both example commands below lean on them. A line beginning with ! runs a shell command and inlines its output before Claude reads the prompt, so ` !git diff --staged hands Claude the real staged diff instead of asking it to go and look. Because that command runs automatically, pair it with an allowed-tools entry such as Bash(git diff:*)` so it does not stop for a permission prompt every time you run the command.

A @ reference points Claude at a file — @src/index.ts reads that file, and @$0 reads whichever file the caller named as the first argument. Between the two, ! for command output and @ for file contents, most commands can assemble exactly the context they need at invocation time, so the prompt describes the task while the syntax supplies the material. That is what separates a command that reliably does the work from one that opens by asking you to paste something in.

Five commands worth stealing

Each of these is a complete file. Drop it in .claude/commands/ under the filename shown, and invoke it with the leading slash.

fix-issue.md — take a number and drive it to a PR:

---
description: Fix a GitHub issue by number
argument-hint: [issue-number]
---
Fix issue #$ARGUMENTS:
1. Run `gh issue view $ARGUMENTS` to read it.
2. Find the relevant files and implement the fix.
3. Add a test, run the suite, then commit and open a PR.

review.md — review the uncommitted diff, no arguments needed:

---
description: Review the uncommitted diff for bugs
allowed-tools: Bash(git diff:*)
---
Here is the current diff:

!`git diff HEAD`

Review it for correctness, security, and missing tests.
Report findings as a short list, most serious first.

test-file.md — run and fix the tests for one file only, using a positional argument:

---
description: Run and fix tests for a single file
argument-hint: [path]
---
Run the tests for `$0` only, not the whole suite. If any fail,
fix the root cause and re-run until they pass. Show the final output.

commit.md — draft a message from what is staged, without committing:

---
description: Draft a commit message from staged changes
allowed-tools: Bash(git diff:*)
---
Staged changes:

!`git diff --staged`

Write a commit message: a 50-character summary line, then a short
body explaining why. Do not commit; just print the message.

compare.md — two positional arguments that mean different things:

---
description: Compare two files and explain the differences
argument-hint: [file-a] [file-b]
---
Compare @$0 and @$1. Explain what each does, how they differ in
approach, and which one to prefer for new code, with a reason.

Command or skill — which to reach for

A slash command and a skill are now the same file format: custom commands have been merged into skills, so .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md both give you /deploy and both accept the arguments above. Reach for a plain command file when you always trigger the action yourself and it fits in one file. Reach for a skill when you also want Claude to load it automatically when relevant, or when it needs supporting files alongside it. For where both sit among subagents, MCP servers, and plugins, see skills vs subagents vs commands.

And if you need something to run every time with no exceptions — not when you remember to type a command — that is a job for a hook rather than a slash command. The hooks cookbook collects copy-paste examples for exactly that.

The mistake to avoid

The mistake is reaching for positional $1 out of old habit and quietly passing the wrong value, or building elaborate placeholder parsing when $ARGUMENTS and a clear prompt would do. Start with $ARGUMENTS, add positional or named arguments only when a command genuinely takes several distinct inputs, and let the prompt — not clever substitution — carry the intent.

Sources

Keep reading