Claude Code commands: what changed, and how to write one
Custom commands have been merged into skills. Your .claude/commands/*.md files still work, and
most tutorials describe only the older half of the feature.
A file at .claude/commands/deploy.md and a skill at
.claude/skills/deploy/SKILL.md both create /deploy and behave the same way.
Here is what actually changed, the precedence order people get wrong, and the arguments gotcha that bites if
you assume shell conventions.
Nothing you already have is broken
If you've read a tutorial about .claude/commands/, it isn't wrong, it's describing the older
half of a feature that grew. Existing .claude/commands/*.md files keep working. There is no
migration deadline and nothing to rewrite today.
One rule worth knowing if you end up with both: if a skill and a command share a name, the skill
wins. With .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md
both present, /deploy runs the skill.
What the skill form adds:
- A directory, so the command can ship supporting files instead of being one lonely markdown file.
- Frontmatter that controls who invokes it, so you decide whether it's yours to type or Claude's to reach for.
- Automatic invocation, so Claude can load the skill when it's relevant instead of waiting for you to type the name.
Worth being precise about what is not a reason to move: files in .claude/commands/
support the same frontmatter, so context: fork, model and
allowed-tools all work there too. The execution controls are not the differentiator.
If your command is five lines of instructions, leave it where it is. If it needs to ship supporting files, or you want Claude to reach for it without being asked, move it.
Where the file goes
| Scope | Path | Applies to |
|---|---|---|
| Enterprise | see managed settings | everyone in your organization |
| Personal | ~/.claude/skills/<name>/SKILL.md | all your projects |
| Project | .claude/skills/<name>/SKILL.md | this project only |
| Plugin | <plugin>/skills/<name>/SKILL.md | wherever the plugin is enabled |
The precedence order is the one people get wrong. Enterprise overrides personal, and
personal overrides project. So a deploy skill in your ~/.claude/skills/ shadows
the project's own deploy for you and nobody else, which makes for a confusing bug report when
a teammate sees different behavior from the same repo.
The other collision worth knowing: a skill at any of these levels also overrides a bundled one with the
same name, so a code-review skill in your project replaces the built-in
/code-review. Plugin skills sidestep all of this by namespacing:
my-plugin/skills/deploy/SKILL.md becomes /my-plugin:deploy and coexists with a
project deploy.
Nested directories work too. A skill at apps/web/.claude/skills/ becomes available when Claude
reads a file under apps/web, and if it collides with a root-level skill it appears as
apps/web:deploy. That's the monorepo answer: each package ships the commands that only make
sense inside it.
The smallest useful command
A skill is a markdown file with frontmatter. This is the whole thing:
---
description: Deploy the application to production
disable-model-invocation: true
---
Run the deploy script, then confirm the health check passes
before reporting success.
disable-model-invocation: true is the one to understand. By default Claude can decide to load
a skill when it seems relevant. Set this and it only ever runs when you type /deploy.
Anything with side effects should have it. You do not want a deploy triggering because the
conversation drifted near the topic.
The inverse is user-invocable: false, which hides the skill from the / menu and
leaves it as background knowledge Claude can pull in on its own.
Arguments, and the off-by-one that will get you
Pass arguments after the command and substitute them in the body. There are four ways, and one of them is a trap:
| Placeholder | What it gives you |
|---|---|
$ARGUMENTS | everything passed, as one string |
$ARGUMENTS[0] | the first argument, zero-indexed |
$0 | shorthand for $ARGUMENTS[0], so also the first argument |
$issue | a named argument declared in frontmatter |
$0 is the first argument, not the command name. If you have written shell
scripts, your instinct says $0 is the program and $1 is the first argument. Here
the numbering is zero-based over the arguments themselves, so $0 is the first and
$1 is the second. It's the substitution I've gotten wrong most often.
Named arguments read better than positions and are worth the extra line:
---
description: Open a PR for an issue
argument-hint: [issue-number] [branch]
arguments: [issue, branch]
---
Open a pull request for issue $issue from branch $branch.
argument-hint is only the autocomplete hint, it doesn't validate anything.
arguments is what actually creates the $issue and $branch
placeholders, mapped by position in order.
A few others are worth knowing. ${CLAUDE_SKILL_DIR} is the one to remember if you took the
supporting-files route: it resolves to the skill's own directory, so a bundled script can be referenced
regardless of the working directory. ${CLAUDE_SESSION_ID} gives the current session, useful
for logging or session-scoped files, and ${CLAUDE_EFFORT} the active effort level if you want
the instructions to adapt.
Running shell commands inside a command
You can inject live output into the command before Claude reads it. Inline, backticks after a
!:
Current branch: !`git branch --show-current`
For several commands, use a fenced block opened with three backticks and a !:
```!
node --version
npm --version
git status --short
```
The inline form only fires when the ! is at the start of a line or directly after
whitespace. Write KEY=!`cmd` and nothing runs, you just get the literal text. That's a deliberate guard rather than a bug, and it fails silently, which is the part that costs you time.
This is the feature that turns a command from a saved prompt into something that gathers its own context. A review command that opens with the diff already inlined beats one that asks Claude to go and fetch it.
Two safety notes. Setting disableSkillShellExecution to true in
settings.json turns this off for skills and commands from
user, project, plugin and additional-directory sources, replacing each command with a placeholder. Bundled
and managed skills are unaffected. And skills synced from a claude.ai account never execute
shell commands locally regardless of that setting.
The frontmatter worth knowing
Beyond description and the invocation controls, the fields that change behavior most:
-
allowed-toolspre-approves tools for the turn that invokes the skill, so a review command can read files without prompting you six times. The grant clears when you send your next message, which is the right scope: it can't leak into the rest of the session. -
disallowed-toolsremoves tools while the skill is active. Useful for an autonomous loop that should never stop to ask a question. -
context: forkruns the skill in its own subagent context, so a long research command doesn't flood your main thread. It runs in the background by default on v2.1.218 and later; earlier versions always blocked the turn until it finished. modeloverrides the session model for the rest of that turn, andeffortapplies while the skill is active.-
pathslimits automatic activation to files matching a glob, so a skill about your API layer loads only when Claude is actually in it.
One sizing note: description and when_to_use are truncated at 1,536 characters
combined in the skill listing, and the listing budget can shorten them further before that cap applies.
Put the trigger condition first, because that's the part that survives.
What I actually keep
Across my own setup the commands that earn their place fall into two groups.
Workflow commands with side effects, all carrying
disable-model-invocation: true: handoff creation, delegating a task to another model, kicking
off a review pass. These are things I want to fire deliberately and never by inference.
Context-gathering commands that lean on shell injection, where the value is that the command arrives already knowing the state of the repo rather than asking for it. The shape is small enough to show in full:
---
description: Review the working tree against our conventions
argument-hint: [optional-focus]
allowed-tools: Read Grep
disable-model-invocation: true
---
## Changes
```!
git diff --stat HEAD
git diff HEAD
```
Review the diff above. $ARGUMENTS
Flag correctness bugs first, then anything that contradicts
the conventions in CLAUDE.md. Skip style the linter enforces.
Everything that makes it worth a file is in that frontmatter and that fenced block: the diff is already
inlined when Claude starts reading, allowed-tools means it can open the files it needs without
six permission prompts, and disable-model-invocation keeps it from firing on its own.
What I dropped: commands that were a saved paragraph of instructions. If it doesn't take arguments, gather
context, or change execution, it's a snippet, and a snippet doesn't need a file. The
same restraint that applies to CLAUDE.md applies here: every
command is one more thing listed in your / menu and one more description competing for
Claude's attention.
Frequently asked questions
Where do custom Claude Code commands live?
Personal skills go in ~/.claude/skills/<name>/SKILL.md and apply to all your projects; project skills go in .claude/skills/<name>/SKILL.md and apply to that repo only. Existing .claude/commands/*.md files still work and create the same slash command. If a skill and a command share a name, the skill takes precedence.
How do I pass arguments to a Claude Code command?
Use $ARGUMENTS for everything passed as one string, $ARGUMENTS[0] or the shorthand $0 for the first argument, or declare named arguments in the arguments frontmatter field to get placeholders like $issue. Note that the numbering is zero-based over the arguments, so $0 is the first argument rather than the command name.
Can a Claude Code command run shell commands?
Yes. Use a backtick-wrapped command after an exclamation mark inline, or a fenced block opened with an exclamation mark for several commands, and the output is injected before Claude reads the file. The inline form only runs when the exclamation mark is at the start of a line or directly after whitespace. Setting disableSkillShellExecution to true in settings disables this for user, project and plugin skills.
What is the difference between a command and a skill in Claude Code?
They are now the same feature. A file at .claude/commands/deploy.md and a skill at .claude/skills/deploy/SKILL.md both create /deploy and work the same way. The skill form adds a directory for supporting files, frontmatter controlling whether you or Claude invokes it, and the ability for Claude to load it automatically when relevant. Command files support the same frontmatter, so execution controls such as running in a subagent are not a reason to move.
How do I stop Claude from running a command automatically?
Set disable-model-invocation to true in the skill's frontmatter. Claude can otherwise load a skill on its own when it seems relevant, which you do not want for anything with side effects such as a deploy.