Your own slash commands
A custom slash command is a markdown file whose contents get sent as your prompt when you
type /name. You have typed “run the tests and list only the failing ones by name” several
times now. It is four lines, it is the same four lines every time, and it belongs in a file.
A file, a name, and the contents sent as your prompt. That is the entire mechanism.
Write one
In stockroom:
mkdir -p .claude/commands
.claude/commands/failing.md:
---
description: Run the test suite and list the failing tests by name
allowed-tools: Bash(npm test)
---
Run `npm test`. Then list every failing test as `file > describe > test name`, one per
line, and nothing else. If everything passes, say "all green".
Now type /failing:
test/store.test.ts > JsonStore > names the file when the database is malformed
test/util.test.ts > paginate > gives the first ten rows for page one
test/util.test.ts > paginate > gives the second ten for page two
test/util.test.ts > paginate > gives the short last page
test/util.test.ts > dates > gives the day a local timestamp falls on
test/reports.test.ts > lowStock > lists what is below its reorder level
test/reports.test.ts > lowStock > says how short each one is
test/reports.test.ts > lowStock > carries the last receipt
test/reports.test.ts > history > runs the balance forward
Nine lines and no commentary on a fresh clone, because the file said so. Vitest already
prints this information; what the command bought you is the shape you wanted, every time,
without asking. Run it after you have fixed things and it says all green, which is the
same specification doing the same job.
allowed-tools in the frontmatter pre-approves those tools for the turn that runs the
command, so /failing does not stop to ask about npm test. The grant lasts one turn.
Where they live
.claude/commands/<name>.md- this project. Commit it; the team gets it.~/.claude/commands/<name>.md- you, every project.
Subdirectories work and do not change the name you type.
Commands and skills have been merged.
.claude/commands/deploy.mdand.claude/skills/deploy/SKILL.mdboth give you/deployand behave the same way. The difference is what the next lesson is about: a skill has adescriptionthat lets Claude decide to load it on its own, and a directory it can keep supporting files in. This lesson is the half you always trigger yourself.
Arguments
$ARGUMENTS is replaced by whatever you type after the command name.
.claude/commands/why.md:
---
description: Explain why a function exists, using the file and its history
argument-hint: [function name]
---
Find `$ARGUMENTS` in src/. Then:
1. Say what it does, in two sentences.
2. Run `git log -S "$ARGUMENTS" --oneline` and say which commit introduced it and why,
from the commit message.
3. Run `grep -rn "$ARGUMENTS" src/ test/` and say who calls it. If nothing does, say so
first and loudly.
/why averageCost and /why startOfDay now do a job you did by hand twice in lesson 14,
and the second one answers “nobody calls this” without you having to think to ask.
Positional arguments work too: $1, $2, or the longer $ARGUMENTS[0]. argument-hint
is the text shown in autocomplete, and it exists so that in three months you remember what
/why wants.
Injecting real output
The best feature in this lesson, and the least known. A line like this:
!`git diff HEAD`
runs the command before the prompt reaches Claude, and replaces the line with its output. The prompt arrives with the data already in it. No tool call, no permission prompt, no turn spent deciding to look.
A review command that is grounded in your actual working tree:
---
description: Review my uncommitted changes before I commit them
allowed-tools: Bash(git *)
---
## The diff
!`git diff HEAD`
## Files touched
!`git diff HEAD --name-only`
## Instructions
Review the diff above. List anything that looks unintended: debug output, formatting
churn, a file touched for no reason, a change with no test. If the diff is empty, say
there is nothing to review.
Compare that with typing “review my changes”. Same intent, but this one cannot skip looking, cannot look at the wrong thing, and costs one turn instead of three.
What makes a good slash command?
It has a fixed shape you want back. /failing is worth writing because “nine lines,
no commentary” is a specification. “Help me with the tests” is not.
You would type it more than about five times. Below that, typing it is cheaper than maintaining it.
It has a verb in it. Commands that produce, check, or run age well. Commands that “analyse” or “consider” turn into a prompt you will edit every time you use it, at which point it may as well be a prompt.
The failure mode is a directory of eleven commands, four of which you have forgotten and two of which no longer match the codebase. Delete them the same way you would delete a stale script.
Keeping Claude from running it
By default Claude can also invoke your commands on its own when they look relevant. For anything with a side effect, that is not what you want:
---
description: Deploy stockroom to the warehouse pi
disable-model-invocation: true
allowed-tools: Bash(ssh *) Bash(rsync *)
---
disable-model-invocation: true means only you can type /deploy. Claude cannot decide
that the code looks ready. Use it for anything that pushes, deploys, sends, or spends.
Your turn
Write /failing in your stockroom clone, exactly as above, then run it.
Check:
/failingprints one line per currently failing test and nothing else. No preamble, no summary at the end.How many lines that is depends on where you are. Nine on a fresh clone; if you have followed lessons 12 and 14 it says
all green, and that is the command working, not the command broken. What you are checking is the shape of the output, which is the only thing the file specified.If it prints a paragraph of explanation before the list, “and nothing else” is not strong enough and you should say so more plainly.
Then write a second one that uses !`command` injection. Take the review command above,
put it in .claude/commands/precommit.md, make a change somewhere in src/ that you
would not want in a commit, add a console.log next to it, and run /precommit.
Check: it names the
console.logwithout you mentioning it, and it does so on the first turn, without a tool call to read the diff. If you see it rungit diffas a tool, the backtick-bang line is malformed and Claude is compensating by going to look.
Recap
A command is a markdown file that becomes your prompt. .claude/commands/ for the project,
~/.claude/commands/ for you.
$ARGUMENTS and $1 take input. !`command` runs a shell command first and drops the
output into the prompt, which is the difference between asking it to look and handing it the
thing. allowed-tools stops it asking permission for the turn.
Write one when the output has a shape you want back every time. Add
disable-model-invocation: true to anything with a side effect.
Next: the same file format, with a description that lets Claude decide when it is needed.