Slash Commands and Custom Skills: Syntax, Frontmatter, and Invocation
Slash commands extend Claude Code with custom workflows. This lesson covers the exact file format (.claude/commands/*.md), frontmatter fields, variable substitution patterns, bundled commands, and how custom commands integrate with the agent loop.
Learning objectives
- Understand the difference between built-in commands, bundled skills, and custom commands
- Write a custom command using the exact .claude/commands/ file format
- Use frontmatter fields (description, argument-hint, allowed-tools) correctly
- Implement variable substitution patterns ($ARGUMENTS, $1, $2)
- Organize commands in subdirectories for namespacing
ToolDix original visual
Frame
Name the outcome and constraints.
Build
Try one bounded workflow.
Review
Keep evidence, revise, and share.
Three types of commands in Claude Code
Claude Code has three command categories:
Built-in commands are hardcoded into the CLI itself. Examples: /clear (start a new session), /context (visualize context usage), /model (switch AI model), /login (authenticate). These execute fixed logic and cannot be customized per project.
Bundled skills are shipped with Claude Code as prompt-based workflows. Examples: /batch (decompose large changes across worktrees), /code-review (multi-agent code review), /deep-research (fan web searches and synthesize). Bundled skills give Claude adaptive instructions and let it spawn subagents or iterate. They cannot be modified but can be overridden via plugins.
Custom commands are files you create at .claude/commands/ in your project or user config. They are your primary tool for encoding repeatable workflows. A custom /deploy command encodes your team's deployment process. A /validate command encodes your data pipeline checks. Custom commands can be shell scripts, Claude prompts, MCP tool invocations, or subagent delegations.
This lesson focuses on custom commands, which you write and maintain.
File location and naming
Custom commands live in one of two places:
Project scope (affects this project only):
.claude/commands/
├── deploy.md
├── test.md
├── data/
│ └── validate.md
└── ci/
└── lint-check.md
User scope (available in all projects):
~/.claude/commands/
├── common-tasks.md
└── team/
└── onboarding.md
Subdirectories create namespaces. A command at .claude/commands/data/validate.md becomes /data-validate (with a hyphen joining the path). A command at .claude/commands/ci/lint-check.md becomes /ci-lint-check.
File names are kebab-case and must match the regex ^[a-z0-9-]+$ (lowercase letters, numbers, hyphens only). File extension must be .md.
Project-scoped commands override user-scoped commands with the same name. If you have both ~/.claude/commands/deploy.md and .claude/commands/deploy.md, running /deploy uses the project version.
Frontmatter schema: exact fields and requirements
Each command file starts with YAML frontmatter. This metadata tells Claude Code how to invoke the command and how to present it.
Minimal example:
---
description: "Run the application test suite."
---
npm test
Complete example with all fields:
---
description: "Deploy the application to staging."
argument-hint: "version number (optional)"
allowed-tools:
- Bash
- Read
- Glob
allowed-models:
- claude-3-5-sonnet-20241022
disable-model-invocation: false
---
# Deploy to staging
This command runs pre-flight checks and deploys to the staging environment.
Set up the deployment:
- Verify git is clean
- Build the application
- Run smoke tests
Then deploy:
- Push the Docker image
- Update the service
Field definitions:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| description | string | Yes | One-line human-readable summary. Shown in / command menu and help text. No markdown. |
| argument-hint | string | No | Describes what arguments the command accepts. Example: "path/to/file" or "version or branch name". Shown in help. |
| allowed-tools | array of strings | No | List of tool names the command is allowed to use. If omitted, no restriction (command can use any tool). Values must be exact tool names like Bash, Read, Edit, Bash, Grep, Glob. |
| allowed-models | array of strings | No | List of model IDs the command can invoke. Overrides the user's current model selection. If omitted, uses user's chosen model. |
| disable-model-invocation | boolean | No | If true, Claude Code will not invoke the model for this command. The command body is treated as a pure prompt or shell script, not adaptive instructions. Default: false. |
The description field is required; others are optional.
| Field | Type | Required | Example |
|---|---|---|---|
description | string | Yes | "Deploy to staging and verify" |
argument-hint | string | No | "version or branch name" |
allowed-tools | array | No | ["Bash", "Read", "Edit", "Glob"] |
allowed-models | array | No | ["claude-3-5-sonnet-20241022"] |
disable-model-invocation | boolean | No | true |
The command body: prompts, scripts, and syntax
After the frontmatter closing ---, the rest of the file is the command body. The body tells Claude Code what to do when the command is invoked.
Type 1: Prompt-based command (adaptive)
Claude Code reads the body as instructions for the model. The model receives the instructions and decides what to do (read files, run commands, etc.). Use this when the task requires adaptation.
---
description: "Review the current changes for security issues."
allowed-tools:
- Read
- Bash
---
Review the changes in the current git diff for security issues.
Look for:
- Hardcoded secrets (API keys, passwords)
- SQL injection vulnerabilities
- Unsafe string operations
- Missing input validation
Use git diff to see what changed, then analyze the changed code.
Report any issues found.
When you run /security-review, Claude Code reads these instructions, understands the goal (security review), and orchestrates the work: run git diff, read the changed files, analyze them, report.
Type 2: Shell script command (deterministic)
The body is one or more shell commands to execute. Use this for fixed sequences that don't require adaptation.
---
description: "Run all linters and formatters."
allowed-tools:
- Bash
---
npm run lint
npm run format
npm run type-check
When you run /lint-all, Claude Code executes each command in sequence. If any command fails, the sequence stops (unless you use || true to ignore failures).
Type 3: Mixed (prompts with commands)
Combine prompts and commands. Claude Code alternates between interpreting the prompt and running shell lines:
---
description: "Deploy with pre-flight checks."
---
First, verify the git state:
```bash
git status --porcelain
If there are uncommitted changes, stop and report the error.
Otherwise, run the build:
npm run build
Then deploy to staging:
npx vercel deploy --prod
## Variable substitution: $ARGUMENTS, $1, $2
Custom commands accept arguments from the user. The command body can reference these arguments using substitution variables.
**$ARGUMENTS:** The entire argument string the user passed.
```yaml
---
description: "Run tests matching a pattern."
argument-hint: "test name pattern"
---
npm test -- $ARGUMENTS
Invoked as /run-test auth, it becomes:
npm test -- auth
$1, $2, $3...: Positional arguments (space-separated).
---
description: "Grep for a pattern in a file."
argument-hint: "pattern file"
---
grep "$1" "$2"
Invoked as /search "const foo" src/main.js, it becomes:
grep "const foo" src/main.js
Note: Always quote variables like "$ARGUMENTS" and "$1" to handle arguments with spaces correctly.
Worked example: a data validation command
Your data team needs a repeatable way to validate datasets before loading them to the warehouse.
File: .claude/commands/data/validate-dataset.md
---
description: "Validate a dataset against the schema and report issues."
argument-hint: "path/to/dataset.csv"
allowed-tools:
- Read
- Bash
- Grep
---
I'll validate the dataset at $ARGUMENTS against our schema.
Here's what I'll do:
1. Read the schema from schemas/schema.yaml
2. Read the dataset header and first 100 rows
3. Check that all required columns are present
4. Check that no columns have > 10% NULL values
5. Validate that numeric columns contain only numbers
6. Report any mismatches or issues
Run the validation script:
```bash
python scripts/validate_data.py "$ARGUMENTS" schemas/schema.yaml
If the validation passes, report: "Dataset is valid and ready to load." If it fails, show the detailed report and suggest fixes.
Invoked as: `/data-validate sales_2024.csv`
Claude Code:
1. Reads the instructions
2. Infers it should read the schema and dataset
3. Runs the validation script with the argument substituted
4. Interprets the output
5. Reports whether the dataset is valid
If the validation fails, Claude Code adapts (reads the schema, checks for issues, suggests fixes) without hardcoding every validation step.
## Bundled commands: how they compare to custom commands
Bundled commands are shipped with Claude Code. They work similarly to custom commands but are distributed as part of the product. As of v2.1.215, key bundled commands include:
| Command | Purpose | Behavior |
|---------|---------|----------|
| `/batch` | Decompose large changes across worktrees | Splits a task into 5-30 independent subtasks and spawns a subagent for each |
| `/code-review` | Multi-agent code review | Spawns subagents to review code from different angles (security, performance, style) |
| `/deep-research` | Fan web searches and synthesize | Searches multiple angles, cross-checks, and synthesizes findings |
| `/verify` | Verification loop (requires explicit invocation) | Runs a verification command (e.g., test suite) and loops until pass |
| `/loop` | Repeat a prompt on an interval | Polls a goal repeatedly, useful for waiting for an async operation |
Bundled commands cannot be edited per-project, but you can:
- Disable them via `skillOverrides` in settings
- Override them by creating a custom command with the same name
- Create variant commands that wrap them
## Command invocation flow: how Claude Code executes a command
When you type `/command-name argument1 argument2`:
1. **Parse the invocation.** Claude Code splits on the first space to separate command name from arguments
2. **Find the command.** Search project-scope commands, then user-scope, then built-in, then bundled
3. **Load the file.** Read the `.md` file and parse its frontmatter
4. **Validate scope.** If the command specifies `allowed-tools`, verify the current permission mode allows those tools. If not, ask for permission
5. **Substitute variables.** Replace `$ARGUMENTS`, `$1`, `$2`, etc. in the body
6. **Invoke the command.** If the body is a prompt, send it to the model with the allowed tools. If it's shell, execute the shell lines. If mixed, alternate
7. **Report results.** Output the result of the command (model response or command output)
## Common patterns: building reusable commands
**Pattern 1: The checker**
A command that verifies something and reports pass/fail:
```yaml
---
description: "Check if the project can be deployed."
allowed-tools:
- Bash
- Read
---
Check the deploy readiness:
1. All tests pass:
```bash
npm test
- No uncommitted changes:
git status --porcelain
- Build succeeds:
npm run build
If all pass: "Project is ready to deploy." If any fail: "Project is not ready. Issues found."
**Pattern 2: The formatter**
A command that applies formatting to code:
```yaml
---
description: "Format all code to team standards."
allowed-tools:
- Bash
---
npm run format
npm run lint -- --fix
Pattern 3: The guide
A command that prompts Claude to follow a specific process:
---
description: "Create a new feature following the team process."
argument-hint: "feature name"
---
Create a new feature named "$ARGUMENTS" following our process:
1. Create a feature branch named feature/$ARGUMENTS
2. Run the feature scaffolder
3. Update README with the new feature description
4. Create a test file for the feature
5. Add the feature to the CHANGELOG
Be specific about file locations and naming conventions used in this repo.
Common mistake
Creating too many similar commands that do almost the same thing. /test, /test-unit, /test-integration -- now new team members are confused about which to use. Instead, make one command accept arguments: /test unit, /test integration, or just /test for all tests. This follows the Unix philosophy of tools doing one thing well, parameterized.
Sources and license context
These references informed the lesson. ToolDix adds its own explanation, workflow, and practice rather than reproducing source material. Every link below leaves ToolDix and opens the publisher's own site in a new tab.
- Commands - Claude Code Docs (opens code.claude.com in a new tab)External · code.claude.com (Anthropic terms apply)
- Glossary - Claude Code Docs (opens code.claude.com in a new tab)External · code.claude.com (Anthropic terms apply)
- How Claude Code works (opens code.claude.com in a new tab)External · code.claude.com (Anthropic terms apply)
Keep going
Read these next on ToolDix.
Original lessons that build on what you just read.