You type /fix-issue 123
and Claude Code expands it into a full prompt with your team's standards attached. Custom slash commands are the cheapest automation in Claude Code — one markdown file, zero config — and they changed in a way that's easy to miss: custom commands have been merged into skills. Your .claude/commands/
files keep working, but the merged model explains several things that look like bugs (arguments not expanding, tool grants vanishing mid-task) and adds features commands never had.
Here is the current behavior, verified against the official docs as of late July 2026.
Drop a markdown file into your project:
<!-- .claude/commands/fix-issue.md -->
---
description: "Fix a GitHub issue by number"
---
Fix GitHub issue $ARGUMENTS following our coding standards.
Read the issue with `gh issue view`, locate the relevant code,
implement the fix, and add a regression test.
Typing /fix-issue 123
sends that content with $ARGUMENTS
replaced by 123
.
The merge means this file and a skill at .claude/skills/fix-issue/SKILL.md
both create /fix-issue
and behave the same way. What a skill adds on top:
If a command and a skill share a name, the skill takes precedence. My practical rule: throwaway personal shortcuts can stay in commands/
; anything you will grow or share belongs in skills/
. (How to write a description that actually triggers is its own topic — I covered it in the SKILL.md guide.)
$ARGUMENTS
, $0
, and the quoting rules Three expansion rules cover almost every confusion I see:
1. $ARGUMENTS is the whole string as typed.
/fix-issue 123 high-priority
→ $ARGUMENTS
becomes 123 high-priority
.2. Indexed access is zero-based and shell-quoted. $ARGUMENTS[0]
(or the shorthand $0
) is the first argument. Quoting groups words:
/migrate-component "search bar" React Vue
→ $0 = search bar, $1 = React, $2 = Vue
3. Missing values behave differently by placeholder type. An indexed placeholder with no matching argument ($2
when you passed one arg) stays in the text unchanged. A named placeholder declared in frontmatter expands to an empty string. If you need a literal dollar-digit in prose ($1.00
), escape it: \$1.00
.
One more safety net: if you pass arguments but the file contains no $ARGUMENTS
at all, Claude Code appends ARGUMENTS: <your input>
to the end, so your input is never silently dropped.
! command``
This is the feature that turns a canned prompt into a grounded one. Lines like:
## Current diff
!`git diff HEAD`
run the shell command before Claude sees anything, and the output replaces the placeholder. Claude receives the actual diff, not an instruction to go fetch it — one fewer tool round-trip, and no chance of the model "summarizing" a diff it never read.
Substitution is a single pass: a command's output cannot emit another !…``
placeholder for a second expansion. Treat it as data, not macros.
allowed-tools
is a one-turn grant, not a session setting The most common surprise in the merged model. Frontmatter like:
allowed-tools: Bash(git add:*), Bash(git commit:*)
pre-approves those tools for the turn that invokes the skill. The grant clears when you send your next message — even though the skill's content stays in context. So "why is Claude asking permission again for the same command?" is usually this: the instructions persisted, the grant did not. Re-invoking the skill re-applies it.
Two related notes:
allowed-tools
doesn't restrict anything. Unlisted tools remain available under your normal permission settings.${CLAUDE_SKILL_DIR}
expands in both the body and allowed-tools
, so a skill can ship a script and pre-approve exactly that script's invocation — no prompt, no wildcard.If you want a grant that survives the whole session, that belongs in your permission rules instead — the allow/deny/ask matching logic is its own minefield.
By default both you and Claude can run any skill. Two frontmatter switches change that:
disable-model-invocation: true
— only you can trigger it. Use for anything with side effects where timing matters: /deploy
, /commit
, /send-release-notes
. You don't want the model deploying because the code "looks ready".user-invocable: false
— only Claude can load it. Use for background knowledge that isn't a meaningful action, like a legacy-system-context
skill.Skill descriptions are always in context so Claude knows what exists; the body loads on invocation.
Once invoked, the rendered content stays in the conversation for the rest of the session (re-invoking an identical skill adds a short "already loaded" note rather than a duplicate). Two consequences:
After context compaction, recent skills are re-attached within a fixed token budget, so a long session can silently drop older ones — details in what survives compaction.
$0
is the $N
stays literal; unmatched named args become empty! command``
runs before Claude reads anything; single passallowed-tools
clears on your next messagecode-review
) .claude/commands/
still works; skills are the recommended pathI maintain Rulestack — versioned rules & skills packs for Claude Code, Cursor, and Codex, kept in sync with changes like this one: https://rulestack.gumroad.com?ref=devto
Daily notes on AI coding workflows live on Bluesky: https://bsky.app/profile/ai-shop.bsky.social