Claude Code Slash Commands: Custom Commands, $ARGUMENTS, and Where They Went
A markdown file becomes a /command you can run with arguments — the cheapest leverage in Claude Code. But commands are a moving target since the skills merge: locations, frontmatter, and collision rules all changed. Current state, verified against the docs.
Custom slash commands are the cheapest leverage in Claude Code: a markdown file becomes a /command you can run with arguments, and the prompt you refined over twenty attempts stops living in your paste buffer. They are also a moving target — commands were merged into the skills system, which changed where they live, what frontmatter they take, and what wins when names collide. This is the current state: the format, the argument syntax people get wrong, and the reasons a command you just wrote does not show up.
Where commands live now
Two mechanisms produce a /name you can type, and both are current:
.claude/commands/deploy.md # command file (project) ~/.claude/commands/deploy.md # command file (personal, all projects) .claude/skills/deploy/SKILL.md # skill (project) ~/.claude/skills/deploy/SKILL.md # skill (personal, all projects)
Anthropic's docs are explicit that custom commands have been merged into skills: a command file and a skill with the same name “both create /deployand work the same way”, existing .claude/commands/ files keep working, and skills add the extras — a directory for supporting files, control over whether you or the model invokes it, and automatic loading when the description matches the task. New work should be a skill; the command-file format below is still worth knowing because thousands of repos (ours included) carry them.
- A command file is markdown; the body is the prompt. The file name is the command name.
- Command files support the same frontmatter as skills —
description,argument-hint,allowed-tools,model,disable-model-invocation,context— exceptnameandpaths, which are ignored in a command file. - For the full skill side of this —
SKILL.md, descriptions that trigger auto-load, supporting files — see how to write a Claude Code skill.
Arguments: the syntax, and the off-by-one everyone hits
--- description: Fix a GitHub issue by number argument-hint: [issue-number] --- Fix GitHub issue $ARGUMENTS following our coding standards. Read the issue, implement, add tests, commit.
$ARGUMENTSexpands to everything typed after the command name, as one string. If the placeholder is absent, arguments are still delivered — appended to the end asARGUMENTS: <value>.- Positional access is zero-based.
$0(short for$ARGUMENTS[0]) is the first argument,$1the second. Every shell-trained instinct says$1is first; here it is not, and the failure is silent — your command just operates on the wrong value. - Quoting is shell-style:
/my-cmd "hello world" secondmakes$0=hello world. - An indexed placeholder with no matching argument stays in the text unchanged —
$2literally appears in the prompt. Named arguments (declared with anarguments:frontmatter list, used as$issue) expand to empty instead. Two different unmatched behaviours; design for whichever you would rather debug. - Literal dollar amounts need escaping: write
\$1.00or the$1inside it becomes an argument.
Dynamic context: run shell before the prompt is sent
The !`command` syntax executes a shell command when the slash command is invoked, and splices the output into the prompt in its place. This is the difference between a command that says “look at the git status” and one that already contains it:
--- description: Write a commit for the staged changes allowed-tools: Bash(git *) --- Current status: !`git status --short` Staged diff: !`git diff --cached` Write a conventional-commit message for these changes and commit.
Paths in commands should use ${CLAUDE_PROJECT_DIR} (the project root, same value hooks receive) so the command works regardless of the cwd the session happens to be in. Skills additionally get ${CLAUDE_SKILL_DIR} for files shipped next to the skill.
Why your command is not showing up
- Wrong directory or extension. It must be
.claude/commands/(or~/.claude/commands/) and the file must be.md. Acommands/directory at repo root, or a.txtfile, is silently ignored. - Name collision — the skill wins. With both
.claude/commands/deploy.mdand.claude/skills/deploy/SKILL.md,/deployruns the skill. Across levels, a personal skill overrides a project skill with the same name. If your edit “does nothing”, check you are editing the copy that actually wins. - It shadows a bundled command's name but not its alias. A custom
code-reviewreplaces the bundled/code-review— but the bundled alias/reviewstill runs the built-in, never yours. user-invocable: falsehides it from the/menu on purpose — that flag means only the model may load it. The inverse,disable-model-invocation: true, keeps it out of the model's hands and reserves it for you.- Plugin commands are namespaced —
/my-plugin:deploy, not/deploy. If you installed something and its command is “missing”, type the prefix.
Reference: the skills documentation (which now covers custom commands) and the built-in commands reference. If what you are really packaging is knowledge the agent should load on its own, not a command you type, write it as a skill with a good description — the dividing line is covered in the skills guide.