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.
What you get from a team command library
Section titled “What you get from a team command library”- A
/pre-prskill that runs your merge gates and returns a verdict with evidence, ready to commit. - A
/fix-issueskill 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
/nameruns when names collide, including the/reviewtrap. - 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 |
| Frontmatter | Same fields as a skill except name and paths | All fields |
| Supporting files (scripts, templates, reference docs) | No | Yes, in the skill directory |
| Same name as the other form | Loses | Wins |
| Portable to other agents | No | The 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.
Where commands live and which one runs
Section titled “Where commands live and which one runs”| Location | Path | Loads in |
|---|---|---|
| Enterprise | .claude/skills/NAME/SKILL.md inside the managed settings directory | Every user on machines your organisation manages |
| Personal | ~/.claude/skills/NAME/SKILL.md | All your projects on this machine; not cloud sessions or routines |
| Project | .claude/skills/NAME/SKILL.md | This repository, for everyone who clones it |
| Nested | SUBDIR/.claude/skills/NAME/SKILL.md | Sessions started in SUBDIR, or once Claude touches a file there |
| Plugin | PLUGIN_DIR/skills/NAME/SKILL.md | Wherever 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
deployskill 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-reviewskill replaces/code-review, yet/review, the bundled alias of/code-review, never runs your skill. Do not name a team commandreview; call it/team-reviewor/pre-prso nobody has to guess which one ran. - Plugin skills never collide, because the plugin name prefixes them.
Build a /pre-pr command step by step
Section titled “Build a /pre-pr command step by step”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.
-
Create the skill directory in the repository root:
Terminal window mkdir -p .claude/skills/pre-pr -
Save this as
.claude/skills/pre-pr/SKILL.md, adjusting the threenpm runscripts 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: trueallowed-tools: Bash(git diff *) Bash(npm run *)---## Branch under check!`git diff --stat origin/main...HEAD`## Instructions1. 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 eachThe
!`git diff --stat origin/main...HEAD`line runs before Claude sees the skill, so the instructions arrive with the real diff inlined. Replaceorigin/mainwith your default branch; if the ref does not exist, the injection fails and aborts the command.allowed-toolspre-approves the injectedgit diffand the gate commands for this turn only; without it, an injected command that needs approval aborts the whole invocation outside auto mode. -
Start Claude Code in the repository and run the command:
/pre-prClaude Code watches existing skill directories, so an edit to
SKILL.mdapplies without a restart. If you created.claude/skills/for the first time during the session, run/reload-skillsfirst. -
Commit the skill so the whole team gets
/pre-pron their nextgit pull:Terminal window git add .claude/skills/pre-pr/SKILL.mdgit commit -m "Add /pre-pr merge-gate command"
Pass arguments to a command
Section titled “Pass arguments to a 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: trueallowed-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.
Decide who may trigger each command
Section titled “Decide who may trigger each command”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:
| Frontmatter | You can invoke | Claude can invoke | Use it for |
|---|---|---|---|
| (default) | Yes | Yes | Checklists and procedures that are safe to run whenever they match |
disable-model-invocation: true | Yes | No | Anything with side effects: commit, push, deploy, send a message, /pre-pr |
user-invocable: false | No | Yes | Background 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-toolsgrants, 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, usedisallowed-tools; to block them everywhere, use deny rules.allowed-toolsis not gated by workspace trust. A project skill’s grant applies even in aclaude -prun in a folder you never trusted. Reviewallowed-toolsin 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: forkagent: Explore---
Find every call site of $ARGUMENTS in this repository.Group them by package. For each group, give the file:line of one representative calland 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.
How do you know a custom command works?
Section titled “How do you know a custom command works?”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/:
name: skills-linton: pull_request: paths: [".claude/**"]permissions: contents: readjobs: 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.pyThe 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.
Share commands beyond one repository
Section titled “Share commands beyond one repository”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.
When custom commands misbehave
Section titled “When custom commands misbehave”| Symptom | Likely cause | Recovery |
|---|---|---|
/name is missing from the / menu | The skill sits in a nested .claude/skills/ that has not loaded yet, or you created the top-level skills directory mid-session | Run /add-dir SUBDIR or /reload-skills; run /skills to see what loaded |
| Frontmatter has no effect | Opening --- not on line 1, YAML that does not parse, or a misspelt field name | Run scripts/check_skills.py; start Claude Code with --debug to see the parse error |
A teammate’s /deploy behaves differently from yours | A personal skill of the same name shadows the project skill on one machine | Rename 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 skill | Rename the command, for example to /team-review |
| Claude runs a side-effecting command unasked | disable-model-invocation: true is missing or misspelt | Add it, and check it with the CI lint |
The command aborts with Shell command failed for pattern | An !`command` injection exited non-zero, and any failure aborts the whole invocation | Append || true to commands that exit 1 on findings; fix the command otherwise |
The command aborts with Shell command permission check failed | An injected command hit a deny rule, or needed approval outside auto mode | Add a matching rule to allowed-tools; deny rules still win |
| The skill stops shaping Claude’s behaviour late in a long session | After compaction Claude Code keeps only the first 5,000 tokens of each re-attached skill, within 25,000 tokens in total, most recent first | Re-invoke the skill; keep bodies short; move hard rules into hooks |
| Claude stops picking the right skill once you have dozens | Descriptions are dropped to fit the listing budget (1% of the context window), least-used first | Put 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 |
Where to go next with custom commands
Section titled “Where to go next with custom commands”- Custom subagents: write the agent types a
context: forkcommand runs on. - Hooks and automation: enforce with a hook what a command can only ask for.
- Review automation: wire
/code-reviewand your own review commands into pull requests. - Memory system: decide what belongs in
CLAUDE.mdand what should move into a skill. - Skills best practices: description writing, size and structure across agents.