A custom /command is now just a skill. The old .claude/commands/foo.md path still works, but .claude/skills/foo/SKILL.md is the modern path that adds autoload, supporting files, scoped permissions, and per-skill model overrides.
Build one in 5 steps
1. Make the skill directory.
mkdir -p ~/.claude/skills/review-pr
Personal skills (~/.claude/skills/) work in every project. For team-shared skills, use .claude/skills/ inside the repo and commit it.
2. Write SKILL.md.
~/.claude/skills/review-pr/SKILL.md:
---
description: Review the current branch's diff for SQL safety and missing tests. Use when the user asks to review their branch, check for unsafe queries, or find untested code paths.
allowed-tools: Bash(git diff *)
---
Review the diff below against `main`. Focus on:
1. SQL queries. Are parameters bound? Any string concatenation into SQL?
2. New code paths with no tests.
3. Side effects inside conditionals (logging, mutation, network).
Report findings as a numbered list. Do not modify any files.
## Diff
!`git diff main...HEAD`
The !`git diff main...HEAD` line is the part most people miss. Backtick-bang runs the shell command before Claude sees the prompt and inlines the output. That is how a skill stays grounded in live state instead of guessing.
3. Test it two ways.
Direct invocation:
/review-pr
Or let Claude pick it up. Because the description mentions “review their branch”, asking “review my branch please” should auto-invoke it. If it doesn’t, the description is too weak. Add the verbs and nouns a user would actually type.
4. Add arguments when you need scope.
---
description: Review the diff under a specific path. Use when the user wants to scope review to one directory.
---
Review the diff under `$ARGUMENTS` for SQL safety and missing tests.
!`git diff main...HEAD -- $ARGUMENTS`
/review-pr api/ swaps $ARGUMENTS for api/. Positional args are zero-based: $0 is the first, $1 is the second. If you forget $ARGUMENTS entirely, Claude Code appends ARGUMENTS: <input> to the bottom so the input is not lost, just unstructured.
5. Stop Claude from auto-firing destructive ones.
Anything with side effects (deploy, send-email, push) needs:
---
description: Push and open a PR for the current branch.
disable-model-invocation: true
---
Now only you can run /ship. Claude will not decide to ship “because the code looks ready”.
Footguns
Skills and old .claude/commands/ files share a namespace, and skills win. If you have both .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md, only the skill runs. Same name across precedence levels (enterprise, personal, project) follows the cascade: enterprise overrides personal, personal overrides project. If your edit is not taking effect, you are probably editing the lower-priority copy.
Trusting a project workspace silently grants its skills’ tool permissions. When you accept the workspace trust dialog for a repo, every .claude/skills/*/SKILL.md with an allowed-tools field gets that access without per-use approval. A repo cloned from a stranger can land a .claude/skills/leak-secrets/ that uses allowed-tools: Bash(curl *) and get away with it. Read the skill files in any unfamiliar repo before you trust it. Add deny rules in /permissions for the most dangerous tools so even trusted skills hit a stop sign.
Skill descriptions get truncated when you have many skills. The combined description budget defaults to 1% of the context window or 8,000 chars, whichever is more. With 30+ skills installed, descriptions are shortened from the back, and Claude stops auto-invoking yours because the keywords got cut. Diagnostic: ask “What skills are available?” mid-session. If yours shows up name-only, raise SLASH_COMMAND_TOOL_CHAR_BUDGET or set low-priority skills to "name-only" in skillOverrides.
Creating the top-level .claude/skills/ directory for the first time requires a session restart. Adding new files inside an existing skills directory hot-reloads. But if .claude/skills/ did not exist when the session started, Claude Code is not watching that path, so new skills are invisible until you restart. Easy 5-minute confusion if you forget.
disableSkillShellExecution silently kills your ! injections. If a managed setting (or your own ~/.claude/settings.json) sets "disableSkillShellExecution": true, every !`command` line is replaced with [shell command execution disabled by policy] instead of running. Your skill still loads, just without the live data it depended on. Check /config if a known-working skill suddenly returns generic answers.
SKILL.md content stays in context for the whole session. Once a skill loads, its full body is part of the conversation until compaction. A 2,000-line SKILL.md is a 2,000-line tax on every turn. Keep SKILL.md under 500 lines and move long reference material into supporting files (reference.md, examples/) that the skill links to but does not inline.
When NOT to use a slash command
- The instruction belongs in
CLAUDE.md. If it is “always do X for this project” with no trigger, it is a project rule, not a skill. Skills cost tokens every time they load; CLAUDE.md is loaded once. - The job needs connection state, OAuth refresh, or a dynamic tool list. Use an MCP server. Skills can
curlan API in a one-shot shell call, but they cannot hold a connection across turns, refresh OAuth tokens automatically, or update their tool list at runtime. If your skill is “shell out, parse the response, return a result”, a skill is fine. If it is “maintain a session with a service that has its own auth lifecycle”, use MCP. - The job needs an isolated context. Use a subagent (
context: forkin skill frontmatter delegates to a subagent, but a real subagent in.claude/agents/gives you more control over the model, tools, and lifecycle). - You will type the prompt once. Just type it. The
/skilloverhead (file, frontmatter, naming) only pays off when the prompt repeats. Two times, file it. Three times, you are being silly not to.