Claude Code slash commands: the built-ins worth knowing, and why your custom /command is a skill now
Treat the commands folder as code Claude can call, not as macros. Audit which files leave model invocation on, check the turn count in scripted runs instead of trusting the exit code, and put the guarantees for anything with side effects in permissions and hooks rather than in an invocation flag.
Contents
The old Claude Code page for custom slash commands now serves a different document. Fetch code. and you get “Extend Claude with skills”, byte for byte the same file as the skills page, with one line near the top: “Custom commands have been merged into skills.” Your .claude/commands/ folder still works. Per the changelog, the merge did not change behavior. Read the other way round, that means a file in that folder already was, and still is, something Claude can run on its own unless you opt out. And a skill that splices in a shell command can fail before Claude ever reads it while claude -p exits 0 and calls the run a success.
Both came out of a throwaway repository, Claude Code 2.1.319 and git 2.43.0 on Linux, every run non-interactive with claude -p and Haiku. Runs are shown with their output. Where a claim rests on the docs alone, it says so, and where I am reasoning past both, that is marked too.
Which Claude Code built-in commands are worth knowing?
The ones that manage the session itself, because no skill can replace them. The commands reference lists over a hundred entries of three kinds. Most are built-in commands, fixed logic coded into the CLI. A few are marked Workflow, such as /deep-research, which fans work out across subagents. Twenty are marked Skill: bundled skills such as /code-review, /doctor, /debug, /loop and /simplify, which per the docs are “a prompt handed to Claude”, the same mechanism as a skill you write.
Anthropic’s own walk-through of a session is a fair shortlist. To set up a repository: /init, /memory, /permissions, /mcp. During a task: /plan, /model and /effort, /context to see what fills the window and /compact to shrink it, /btw for a side question that stays out of the history. Before shipping: /diff for the working tree, /code-review for the current diff, /security-review for your branch against the default branch on origin (it needs that remote). When something breaks: /rewind to a checkpoint, /doctor for a setup checkup. Per the same reference, three more matter once you write your own: /skills lists what is loaded, /reload-skills picks up a skills folder created mid-session, and /skill-doctor reports what each skill costs in context and how often it runs.
Two rules from the docs change how you type them. Any command runs only when it starts your message. For skills there is a second half: typed after plain text, as in “go ahead and /deploy to staging”, the name runs nothing directly but counts as permission for Claude to run that skill while it answers. And a skill you write under a built-in’s name replaces the built-in in a local terminal, but not its aliases.
Is a Claude Code custom command a skill now?
Yes, in everything but the folder layout. Per the docs, .claude/ and .claude/ “both create /deploy and work the same way”. A command file accepts the same frontmatter as a skill except name and paths. What a skill folder adds is room for supporting files: reference docs Claude reads only when needed, scripts it runs without loading them.
When both exist under the same name, the skill wins. I put a command file and a skill called zt-hello side by side, the command told to reply COMMAND and the skill told to reply SKILL:
$ claude -p "/zt-hello" --model haiku
SKILLSubfolders of .claude/commands/ still namespace with a colon. A file at .claude/ answered to /zt-ns:zt-comp. Skill folders do not nest that way, which matters when you migrate (below).
Can Claude run an old .claude/commands file on its own?
Yes, and it is not new. The changelog entry for 1.0.123 added a SlashCommand tool “which enables Claude to invoke your slash commands”, and the 2.1.3 entry that merged commands into skills says it came “with no change in behavior”. What the merge changed is the description you read: a command file is now documented as a skill, and a skill’s description sits in Claude’s context so Claude can decide when to load it. If you think of the folder as macros, that is easy to skim past. I asked Claude to list the zt- skills it could invoke:
$ claude -p "List the names of every skill you can invoke with the Skill tool whose name starts with zt-. Names only." --model haiku
zt-hello
zt-ship
zt-ns:zt-compzt-ns:zt-comp is the command file, and it has no frontmatter at all. Per the docs, a missing description falls back to the first non-empty line of the body, so even a bare prompt file gets listed. Told to call it through the Skill tool, Claude got back Launching skill: zt-ns:zt-comp and answered NESTED. The four missing from that list, zt-args, zt-diff, zt-diffsafe and zt-deploy, are the ones that set disable-model-invocation: true.
So a command file you wrote as a macro to trigger by hand is also something Claude may decide to run when the conversation looks like it fits. For /summarize that is harmless. For a file that deploys, sends or deletes, it is behavior you may never have planned for, and nothing in the file itself tells you.
How do you write a Claude Code skill with arguments?
A folder with a SKILL.md, YAML frontmatter between --- lines, and the instructions as Markdown. Everything after the command name arrives as $ARGUMENTS; $0, $1 and so on index into it with shell-style quoting. My test skill:
---
description: Echo arguments test
disable-model-invocation: true
---
Reply with exactly this line and nothing else: FIRST=[$0] SECOND=[$1] THIRD=[$2] ALL=[$ARGUMENTS]$ claude -p '/zt-args "hello world" second' --model haiku
FIRST=[hello world] SECOND=[second] THIRD=[$2] ALL=["hello world" second]The quoted pair stayed one argument, and $2, with nothing at that position, stayed as literal text, as the docs say it should. Three more details from the docs, before you write prose with dollar signs in it:
- If no placeholder receives the arguments, Claude Code appends them as an
ARGUMENTS:line, so Claude still sees what you typed. - Named arguments come from an
argumentslist in the frontmatter (arguments: [issue, branch]makes$issuethe first one); a named placeholder with nothing passed expands to an empty string, unlike$2above. - A literal price such as
$1.00in the body needs one backslash,\$1.00, or its$1expands to your second argument when there is one.
The two fields you will set in nearly every skill are description (what the skill does and when to use it; description plus when_to_use is cut at 1,536 characters in the listing, so the key use case goes first) and argument-hint, the placeholder shown during autocomplete. Two variables are worth using in any skill that runs a script, because they resolve the same way whatever directory the session is in:
${CLAUDE_SKILL_DIR} the folder that holds this SKILL.md
${CLAUDE_PROJECT_DIR} the project rootWhat does disable-model-invocation actually stop in Claude Code?
It stops Claude from loading the skill through the Skill tool on its own initiative, and Claude Code enforces that itself rather than asking the model to behave. With the flag set, the description leaves Claude’s context entirely, which is why zt-deploy was missing from the list above. When I told Claude to invoke it anyway, the Skill tool returned this (from the stream-json log of the run):
<tool_use_error>Skill zt-deploy cannot be used with Skill tool due to
disable-model-invocation. Ask the user to run /zt-deploy themselves — it
cannot be invoked via the Skill tool. Do not replicate this skill's workflow
by other means — it is reserved for explicit user invocation.</tool_use_error>The block itself is real: the Skill tool refuses, whatever the model wanted. The last sentence is a different kind of thing. “Do not replicate this skill’s workflow by other means” is text the model reads, not a check the harness runs, and a sentence is weighed, not enforced. The flag closes the Skill tool path. It does not take away the shell, the files or the network that the skill’s steps would use. And it is not absolute even there: per the docs, writing /zt-deploy after plain text in your own message counts as your permission, and since 2.1.110 that works for skills with this flag too.
The other field does the opposite job, and the names make them easy to confuse:
| Frontmatter | You can type /name | Claude can invoke it | Description in Claude’s context |
|---|---|---|---|
| (default) | Yes | Yes | Yes |
disable-model-invocation: true | Yes | Not on its own | No |
user-invocable: false | No | Yes | Yes |
user-invocable: false is for background knowledge, such as how a legacy system works, that Claude should apply but nobody needs as a command. It hides the skill from the / menu and, per the docs, ignores you typing its name. It does nothing to stop Claude.
The frontmatter flag is also not the only switch. Per the docs, a skillOverrides entry does the same without editing the file, which is the route for a skill checked into a shared repository. The /skills menu writes it to .claude/, your own uncommitted settings for that project:
{
"skillOverrides": { "deploy": "user-invocable-only" }
}"off" instead hides the skill from Claude and from the / menu, and typing its full name returns an error rather than running it. A separate lever, a deny rule such as Skill(deploy *) in your permission settings, takes the skill away from the Skill tool whatever its frontmatter says.
Does allowed-tools in a Claude Code skill restrict anything?
No. It pre-approves. The docs are explicit: allowed-tools lets Claude use the listed tools without asking “during the turn that invokes this skill”, the grant clears when you send your next message, and “it does not restrict which tools are available”. A deploy skill with allowed-tools: Bash(git push *) does not confine Claude to git push. It removes the prompt for git push, for one turn.
The field that removes tools is disallowed-tools, and it too lasts only while the skill is active. Permanent limits live in permission rules: deny and ask rules still override a skill’s allowed-tools.
One more property, from the same page: “Workspace trust doesn’t gate this field.” A project skill’s allowed-tools applies even in a claude -p run in a folder you never trusted, which belongs on the list of what a cloned repository’s settings can still do. Read the frontmatter of skills in a repository before you run Claude Code in it.
For anything with side effects, here is my reading of the layers; the docs describe each one but do not rank them. disable-model-invocation decides who may start the skill. Permission rules decide which commands may run at all, whoever starts them. A hook runs every time its event fires, whether or not Claude is following the skill, which is why the docs’ own fix for “Claude skipped a rule that must hold every time” is to move the rule into one. Use the first for intent, the second and third for the guarantee.
Why does claude -p exit 0 when a Claude Code skill’s shell command fails?
Because the skill aborts before Claude runs a single turn, and nothing in the headless run counts that as an error. The docs’ own first example triggers it: git diff HEAD fails in a repository with no commits, and a failed injected command aborts the whole skill. That example opens with !`git diff HEAD`, which runs the command before Claude sees anything and splices its output into the prompt. In a freshly initialized repository:
$ claude -p "/zt-diff" --model haiku
Shell command failed for pattern "!`git diff HEAD`": [stderr]
fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'That much the docs describe: a failed command aborts the invocation, and Claude never sees the skill. (With the default shell, any non-zero exit counts, except exit code 1 from search and comparison commands such as grep.) What they do not mention is how a scripted run reports it:
$ claude -p "/zt-diff" --model haiku < /dev/null > /dev/null; echo "exit=$?"
exit=0
$ claude -p "/zt-diff" --model haiku --output-format json
{"is_error": false, "subtype": "success", "num_turns": 0, "result": "Shell command failed for pattern ..."}The JSON is trimmed to the four fields that matter. Exit code 0, is_error false, subtype success. A cron job or CI step that checks either one records a pass for a skill that never ran. The tell is num_turns: 0 and a result that starts with “Shell command failed for pattern”, so check for those if you call skills from scripts.
The fix in the skill is the one the docs give for commands expected to exit non-zero: append || true. With it, git’s error text still lands in the prompt (Claude Code merges stderr into the injected output under the default shell, so the 2>&1 below is only explicit), and Claude can see why the diff is empty:
## Current changes
!`git diff HEAD 2>&1 || true`The same skill with that line loaded and answered normally. One related rule from the docs: outside auto mode, an injected command that your permission rules do not allow aborts the skill too, rather than prompting. Pre-approve it in allowed-tools.
How do you move .claude/commands files to Claude Code skills?
There is no deadline: command files keep working. Move a file when the command needs supporting files. Review all of them now, because of the invocation default above. A sequence that works:
-
Find the command files Claude can invoke. Anything whose frontmatter does not turn the flag on. A plain grep over the whole file is fooled by an example in the body and misses
yesorTrue, which the docs accept, so this reads only the frontmatter block:find .claude/commands ~/.claude/commands -name '*.md' 2>/dev/null | while read -r f; do awk 'NR==1 && $0!="---" {exit} NR>1 && $0=="---" {exit} NR>1 && tolower($0) ~ /^disable-model-invocation: *(true|yes|on|1) *$/ {found=1} END {exit !found}' "$f" || echo "model-invocable: $f" doneIn my test repository it listed the three command files that leave the flag off, including one that mentions the flag only in its body, and skipped one that sets it to
Yes. It is a text check: it does not see a skill that shadows the file under the same name, or askillOverridesentry. Pointed at my own personal skills folder for this post, the same grep found no skill there that sets the flag. Whether that is right depends on what each skill does; the check only puts the question in front of you. -
Add the flag to everything with side effects, whether or not you move the file.
-
Move what needs a folder.
mkdir -p .claude/, thenskills/ deploy git mv .claude/for a tracked file, or plaincommands/ deploy. md .claude/ skills/ deploy/ SKILL. md mvfor one Git does not track. Moving rather than copying matters less than it seems, since the skill would win the name clash anyway, but two files with one name is confusion you will meet later. -
Rename namespaced commands on purpose.
.claude/iscommands/ frontend/ component. md /frontend:component. A skill takes its command name from its folder or itsnamefield, so the moved version gets a new name you choose, such asfrontend-component. Anyone with the old name in their fingers needs telling. -
Validate the frontmatter. A file whose YAML does not parse still loads, with every field silently dropped, including the flag you just added. Run the validator on both folders,
.claude/skillsand.claude/commands, never on a single skill folder (that is treated as a plugin and fails for lack of a manifest):$ claude plugin validate .claude/skills Validating skill: .claude/skills/zt-broken/SKILL.md ✘ Found 1 error: ❯ frontmatter: YAML frontmatter failed to parse: YAML Parse error: Unexpected character. At runtime this skill loads with empty metadata (all frontmatter fields silently dropped).That is the output for a deliberately broken test skill, with the path shortened. On the same folder without it, the run printed
✔ Validation passed. On.claude/commandsit also warns about a command file with no frontmatter block, and passes with warnings.
A file in .claude/commands/ reads like a macro, something you run. As a skill it is something you and Claude can both run, and both is the default. Usually that default is what you want. It is the point of a skill. The files worth a second look are the ones written as macros, on the assumption that the only one who would ever run them was you.
01 / Discussion
No comment section here — all discussions happen on X.
02 / More articles
Claude Code vs Gemini CLI: seven ways developers split the work, and which ones survived June 18
The 2025 setups that paired the two tools leaned on a free Google login and a bigger context window. The login went to another tool in June, and Claude Code caught up on the window. What decided which setups survived was the account behind each call, not the tools themselves.
Claude Code background subagents and your usage limit: Anthropic's 99% status-line claim, and the cache writes you can measure
Isolation is the point of a subagent, and in my transcripts most of its bill was five-minute cache writes. Measured on a Max plan: background subagents about 11.5% of priced usage, 64% of that in cache writes. A Python script to reproduce it, and four levers with the part of the bill each one acts on.
Claude Code worktrees: what claude --worktree isolates, and the ports, databases and installs it doesn't
Tested on Claude Code 2.1.316: two worktrees collided on one port, Docker Compose planned a separate Postgres volume for each, scripted runs left locked checkouts and leftover branches, and the default base skipped the unpushed commit.