Skip to content

Custom commands and skills in Claude Code

A custom command in Claude Code is a Markdown file that becomes a slash command: the older .claude/commands/NAME.md or the current form, a skill at .claude/skills/NAME/SKILL.md that adds supporting files, invocation control and automatic loading. Both accept arguments and frontmatter; a committed file gives every teammate the same /NAME workflow. Below, NAME is the command name, SUBDIR a repository subdirectory and PLUGIN_DIR a plugin’s root directory.

This page is for developers who keep pasting the same checklist into Claude Code, and for tech leads who want those checklists to run the same way for eight people. The situation is familiar: one engineer’s pre-PR routine catches the missing test and the unparameterised query, everyone else’s does not, and review becomes the place where that difference shows up.

The page assumes Claude Code already runs in your repository (see configuring Claude Code). To install skills other people wrote rather than write your own, start with installing and managing skills.

  • A /pre-pr skill that runs your merge gates and returns a verdict with evidence, ready to commit.
  • A /fix-issue skill that takes an issue number as a named argument.
  • A rule for who may trigger each command: you, Claude or both.
  • The precedence rules that decide which /name runs when names collide, including the /review trap.
  • A 30-line CI check that catches the frontmatter mistakes Claude Code ignores without a warning.
  • A way to show a command does its job without reading every transcript.

Command file or skill: which should you write?

Section titled “Command file or skill: which should you write?”

Write a skill for anything new. The Claude Code skills documentation says custom commands “have been merged into skills”: .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md both create /deploy, and existing command files keep working (checked against v2.1.283).

.claude/commands/NAME.md.claude/skills/NAME/SKILL.md
Invoked as/NAME; a subfolder adds a colon prefix, so commands/test/unit.md is /test:unit/NAME, or the frontmatter name
FrontmatterSame fields as a skill except name and pathsAll fields
Supporting files (scripts, templates, reference docs)NoYes, in the skill directory
Same name as the other formLosesWins
Portable to other agentsNoThe SKILL.md layout is the Agent Skills open standard; invocation control, context: fork and !`command` injection are Claude Code extensions

Skills load cheaply: only the description sits in context until the skill runs, then the full body enters the conversation and stays there across turns. Every line of the body is a recurring token cost, so write standing instructions, not narration.

LocationPathLoads in
Enterprise.claude/skills/NAME/SKILL.md inside the managed settings directoryEvery user on machines your organisation manages
Personal~/.claude/skills/NAME/SKILL.mdAll your projects on this machine; not cloud sessions or routines
Project.claude/skills/NAME/SKILL.mdThis repository, for everyone who clones it
NestedSUBDIR/.claude/skills/NAME/SKILL.mdSessions started in SUBDIR, or once Claude touches a file there
PluginPLUGIN_DIR/skills/NAME/SKILL.mdWherever the plugin is enabled, as /PLUGIN_NAME:NAME

When two definitions share a name, this is what runs (per the skills reference, v2.1.283):

  • Enterprise beats personal, and personal beats project. A deploy skill in ~/.claude/skills/ shadows the team’s .claude/skills/deploy/ on your machine only. Your teammates run the project version and you do not.
  • A skill beats a command file of the same name.
  • Your skill replaces a bundled skill, but not its aliases. A project code-review skill replaces /code-review, yet /review, the bundled alias of /code-review, never runs your skill. Do not name a team command review; call it /team-review or /pre-pr so nobody has to guess which one ran.
  • Plugin skills never collide, because the plugin name prefixes them.

The command below does not ask Claude for an opinion on your diff. It runs the gates your CI runs and reports each exit code, so the person opening the pull request reads evidence instead of prose.

  1. Create the skill directory in the repository root:

    Terminal window
    mkdir -p .claude/skills/pre-pr
  2. Save this as .claude/skills/pre-pr/SKILL.md, adjusting the three npm run scripts to your project’s gate names:

    ---
    description: Runs this repository's merge gates on the current branch and reports evidence. Use before opening a pull request.
    disable-model-invocation: true
    allowed-tools: Bash(git diff *) Bash(npm run *)
    ---
    ## Branch under check
    !`git diff --stat origin/main...HEAD`
    ## Instructions
    1. Run these gates in order and record the exact command and exit code of each:
    `npm run typecheck`, `npm run lint`, `npm run test`.
    2. If a gate fails, show the first error with its file and line. Do not edit code.
    3. For each changed file under `src/`, name the test that exercises it, or list the file as untested.
    4. Reply in exactly this shape and nothing else:
    VERDICT: READY or BLOCKED
    | Gate | Command | Exit code |
    Untested files: <list or "none">
    Look here first: at most three file:line locations with one reason each

    The !`git diff --stat origin/main...HEAD` line runs before Claude sees the skill, so the instructions arrive with the real diff inlined. Replace origin/main with your default branch; if the ref does not exist, the injection fails and aborts the command. allowed-tools pre-approves the injected git diff and the gate commands for this turn only; without it, an injected command that needs approval aborts the whole invocation outside auto mode.

  3. Start Claude Code in the repository and run the command:

    /pre-pr

    Claude Code watches existing skill directories, so an edit to SKILL.md applies without a restart. If you created .claude/skills/ for the first time during the session, run /reload-skills first.

  4. Commit the skill so the whole team gets /pre-pr on their next git pull:

    Terminal window
    git add .claude/skills/pre-pr/SKILL.md
    git commit -m "Add /pre-pr merge-gate command"

$ARGUMENTS expands to everything typed after the command name. $0, $1 and so on pick positional arguments, with shell-style quoting for multi-word values. For anything a teammate will reuse, declare named arguments in arguments and add an argument-hint so autocomplete shows what the command expects:

---
description: Fix a GitHub issue test-first. Use when given an issue number to fix.
argument-hint: "[issue-number]"
arguments: [issue]
disable-model-invocation: true
allowed-tools: Bash(gh issue view *)
---
Fix GitHub issue #$issue.
1. Run `gh issue view $issue --comments` and restate the expected behaviour in one sentence.
2. Write a failing test that reproduces it. Run it and show that it fails.
3. Make the smallest change that turns the test green, then run the full test suite.
4. Stop without committing. Report the test name, the suite result and the files changed.

Save it as .claude/skills/fix-issue/SKILL.md and run /fix-issue 482. Two behaviours to know: an indexed placeholder with no matching argument, such as $1 when you passed one value, stays in the text as a literal $1, while a named placeholder expands to an empty string. If no placeholder receives the input, Claude Code appends it at the end as ARGUMENTS: VALUE, where VALUE is what you typed.

By default both you and Claude can invoke a skill: you type /name, and Claude loads it when a request matches the description. Two fields change that:

FrontmatterYou can invokeClaude can invokeUse it for
(default)YesYesChecklists and procedures that are safe to run whenever they match
disable-model-invocation: trueYesNoAnything with side effects: commit, push, deploy, send a message, /pre-pr
user-invocable: falseNoYesBackground knowledge such as an api-conventions or legacy-billing-context skill

A disable-model-invocation skill also drops out of Claude’s context entirely, which saves listing space. If Claude tries to run it anyway, Claude Code blocks the call.

Two fields are easy to misread:

  • allowed-tools grants, it does not restrict. It pre-approves the listed tools for the turn that invokes the skill and clears on your next message. Every other tool stays callable under your normal permission rules. To remove tools while the skill is active, use disallowed-tools; to block them everywhere, use deny rules.
  • allowed-tools is not gated by workspace trust. A project skill’s grant applies even in a claude -p run in a folder you never trusted. Review allowed-tools in any repository’s .claude/skills/ before you run Claude Code there, and see skill supply-chain security for skills you install from elsewhere. To turn off !`command` injection for user, project and plugin skills, set "disableSkillShellExecution": true; an organisation puts it in managed settings, where users cannot override it.

Hand heavy work to a subagent with context: fork

Section titled “Hand heavy work to a subagent with context: fork”

A command that reads half the codebase fills your main conversation with file contents. Add context: fork and the skill runs as a separate subagent that starts from the skill text alone, without your conversation history, and returns only its result:

---
description: Find every caller of a function or module and summarise how each uses it. Use before changing a public API.
argument-hint: "[symbol]"
context: fork
agent: Explore
---
Find every call site of $ARGUMENTS in this repository.
Group them by package. For each group, give the file:line of one representative call
and say whether it relies on the current return type, error behaviour or side effects.
End with the list of call sites that would break if the signature changed.

The forked subagent runs in the background by default, with the narrower tool set background subagents get; set background: false when you want to wait for the answer in the same turn. Explore is a built-in read-only agent. To write your own agent types with their own tools, model and permission mode, see custom subagents.

Prompts that write and migrate commands for you

Section titled “Prompts that write and migrate commands for you”

You rarely need to write a skill by hand. After a session in which you and Claude worked out a procedure, ask Claude to capture it.

The last prompt needs the skill-creator plugin from the official marketplace: run /plugin install skill-creator@claude-plugins-official in Claude Code first.

Seeing /pre-pr produce output tells you the file loaded, not that the command does its job. Check four things, none of which requires reading a full transcript.

1. The file is well formed. Claude Code silently ignores a frontmatter field whose name is misspelt, reads no frontmatter at all when the opening --- is not on line 1, and loads the body with empty metadata when the YAML does not parse. Each of those turns a disable-model-invocation: true deploy command back into one Claude may run on its own. Add this check as scripts/check_skills.py:

import pathlib, sys, yaml
SKILL_KEYS = {"name", "description", "when_to_use", "argument-hint", "arguments",
"disable-model-invocation", "user-invocable", "allowed-tools", "disallowed-tools",
"disallowedTools", "version", "model", "effort", "context", "agent", "background", "hooks", "paths", "shell",
"metadata", "license", "compatibility"}
COMMAND_KEYS = SKILL_KEYS - {"name", "paths"}
files = [(p, SKILL_KEYS) for p in pathlib.Path(".").glob("**/.claude/skills/*/SKILL.md")
if "node_modules" not in p.parts]
files += [(p, COMMAND_KEYS) for p in pathlib.Path(".claude/commands").rglob("*.md")]
errors = []
for path, allowed in files:
text = path.read_text(encoding="utf-8")
if not text.startswith("---\n"):
errors.append(f"{path}: frontmatter must open on line 1")
continue
try:
meta = yaml.safe_load(text.split("---\n", 2)[1]) or {}
except yaml.YAMLError as exc:
errors.append(f"{path}: YAML does not parse ({exc.problem})")
continue
if not isinstance(meta, dict) or not meta.get("description"):
errors.append(f"{path}: missing description")
else:
for key in sorted(set(meta) - allowed):
errors.append(f"{path}: unknown field '{key}' is ignored by Claude Code")
print("\n".join(errors) or f"{len(files)} files OK")
sys.exit(1 if errors else 0)

The key list is the skills frontmatter reference as of v2.1.283, plus the disallowedTools spelling and the version field that the v2.1.283 parser also reads; update it when Claude Code adds a field. The skills glob covers the top-level and nested .claude/skills/ directories; plugin skills are checked by claude plugin validate, below. Run it on every pull request that touches .claude/:

.github/workflows/skills-lint.yml
name: skills-lint
on:
pull_request:
paths: [".claude/**"]
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: pip install pyyaml
- run: python scripts/check_skills.py

The job needs no secret and runs no agent. claude plugin validate is not a substitute for it. In our test on v2.1.283, claude plugin validate --strict . run from the repository root flagged a SKILL.md with no frontmatter or with a blank first line, but passed a misspelt disable-model-invokation: true and a YAML block that does not parse, and pointed at .claude/skills directly it checked zero files. Keep it as a second step, and once you package skills as a plugin, run claude plugin validate --strict PLUGIN_DIR on the plugin as well.

2. The command makes a difference. Run three realistic requests in fresh sessions with the skill enabled and again with it off, then compare. A fresh session matters because the context left over from writing the skill hides gaps in its instructions. The skill-creator prompt above automates this comparison; for skills shipped in a plugin, claude plugin eval runs the same with/without comparison over the cases in the plugin’s evals/ directory (a case.yaml, or a prompt.md plus graders/*.md, per case) and exits non-zero when a case scores below --threshold (default 1.0, checked on v2.1.283). In CI, pass --trust-plugin and a realistic threshold such as --threshold 0.8.

3. The output is checkable. A command that ends in a fixed shape, such as VERDICT plus a gate table with exit codes, lets a reviewer confirm the claim by re-running one command instead of reading the session. Treat a command whose output is free prose as unfinished.

4. The library stays affordable. Every skill description costs context on every turn. Run /skill-doctor to see each skill’s context cost and how often it is used, and turn off the ones nobody invokes.

Who signs off: put .claude/ under a CODEOWNERS entry for the tech lead or platform owner. A shared command changes how every teammate’s agent behaves, and its allowed-tools grants permissions, so it deserves the same review as a CI workflow change. Cover .github/ and scripts/check_skills.py with the same entry and require code-owner review in branch protection: on the pull_request trigger GitHub runs the workflow and the script from the pull request itself, so without that review a pull request can weaken the lint in the same diff that adds the skill.

Committing .claude/skills/ covers one repository. Two cases need more:

  • Cloud sessions and routines do not read ~/.claude/skills/. A routine that invokes a skill that exists only on your laptop reports that the skill was not found. Commit the skill to the repository, or enable it for your claude.ai account.
  • Several repositories need the same commands. Package them as a plugin and publish it through a team marketplace; see building plugins and running a team plugin marketplace. Plugin skills are namespaced, so they never shadow a repository’s own commands.

The same SKILL.md body works in other agents that implement Agent Skills. Building and publishing your own skills shows the cross-tool layout for Codex and Cursor; drop the Claude Code-only fields when you port one.

SymptomLikely causeRecovery
/name is missing from the / menuThe skill sits in a nested .claude/skills/ that has not loaded yet, or you created the top-level skills directory mid-sessionRun /add-dir SUBDIR or /reload-skills; run /skills to see what loaded
Frontmatter has no effectOpening --- not on line 1, YAML that does not parse, or a misspelt field nameRun scripts/check_skills.py; start Claude Code with --debug to see the parse error
A teammate’s /deploy behaves differently from yoursA personal skill of the same name shadows the project skill on one machineRename or delete the personal copy in ~/.claude/skills/; personal beats project
/review ignores your custom command/review is the alias of the bundled /code-review, and aliases never run your skillRename the command, for example to /team-review
Claude runs a side-effecting command unaskeddisable-model-invocation: true is missing or misspeltAdd it, and check it with the CI lint
The command aborts with Shell command failed for patternAn !`command` injection exited non-zero, and any failure aborts the whole invocationAppend || true to commands that exit 1 on findings; fix the command otherwise
The command aborts with Shell command permission check failedAn injected command hit a deny rule, or needed approval outside auto modeAdd a matching rule to allowed-tools; deny rules still win
The skill stops shaping Claude’s behaviour late in a long sessionAfter compaction Claude Code keeps only the first 5,000 tokens of each re-attached skill, within 25,000 tokens in total, most recent firstRe-invoke the skill; keep bodies short; move hard rules into hooks
Claude stops picking the right skill once you have dozensDescriptions are dropped to fit the listing budget (1% of the context window), least-used firstPut the use case first in each description, set rarely used skills to "name-only" in skillOverrides, and turn off unused ones found by /skill-doctor