Concepts

Slash Commands and Hooks

How to define slash commands in Claude Code plugins using commands/*.md files, and how to configure lifecycle hooks (pre-tool-call, post-tool-call) in SKILL.md frontmatter for automated behaviors.

Slash commands and hooks are two complementary mechanisms for controlling Claude Code behavior. Commands give users explicit control — type /deploy and Claude runs your deployment workflow. Hooks give developers automated control — every time Claude calls a specific tool, your hook runs before or after it without any user intervention. Together, they cover both ends of the interaction spectrum: deliberate user actions and invisible guardrails.

Slash Commands

What They Are

A slash command is a markdown file in a plugin’s commands/ directory that defines an action users can invoke by typing /command-name in Claude Code. Unlike skills (which activate automatically based on context), commands require explicit invocation. This makes them ideal for workflows that should only run when the user specifically requests them.

Common use cases for commands:

  • Deployment workflows (/deploy, /rollback)
  • Code generation from templates (/scaffold, /generate-api)
  • Analysis and reporting (/audit, /coverage-report)
  • Repository maintenance (/clean, /update-deps)
  • Documentation generation (/generate-docs, /api-docs)

Command File Structure

Command files live in the commands/ directory of a plugin:

my-plugin/
└── commands/
    ├── deploy.md
    ├── scaffold.md
    ├── audit.md
    └── update-deps.md

Each file name (minus the .md extension) becomes the command name. A file named deploy.md creates the /deploy command. Use kebab-case for multi-word commands: update-deps.md creates /update-deps.

Command Frontmatter

Every command file begins with YAML frontmatter that defines its metadata:

---
name: deploy
description: "Deploy the current branch to staging or production with pre-flight checks"
user-invocable: true
argument-hint: "<environment>"
---
FieldRequiredTypeDescription
nameYesstringCommand identifier (must match filename)
descriptionYesstringOne-line summary shown in the / menu
user-invocableNobooleanWhether the command appears in the / menu (default: true)
argument-hintNostringHint text shown after the command name in the menu

The description Field

The description serves double duty: it is displayed in the Claude Code / menu as help text, and it helps Claude understand when to suggest the command. Write descriptions that are concise but informative:

# Good: tells the user what it does and when to use it
description: "Run security audit on all source files and generate a SARIF report"

# Too vague
description: "Security stuff"

# Too long for a menu item
description: "This command performs a comprehensive security audit of the entire codebase including static analysis, dependency checking, secret scanning, and generates a detailed SARIF format report suitable for GitHub Advanced Security integration"

Aim for 50-100 characters. The description should answer “what does this do?” in one glance.

The argument-hint Field

When a command accepts arguments, the argument-hint field tells users what to provide:

# Single argument
argument-hint: "<environment>"       # /deploy staging
argument-hint: "<file-path>"         # /analyze src/index.ts
argument-hint: "<component-name>"    # /scaffold UserProfile

# Multiple arguments
argument-hint: "<source> <target>"   # /migrate users orders

Inside the command body, arguments are accessible through substitution variables:

VariableValue
$ARGUMENTSThe full argument string
$0First argument
$1Second argument
$2 through $9Subsequent arguments

Hidden Commands

Setting user-invocable: false hides a command from the / menu while keeping it available for programmatic invocation by other commands, skills, or agents:

---
name: internal-validate
description: "Internal validation step used by the deploy command"
user-invocable: false
---

This is useful for building multi-step workflows where a top-level command delegates to hidden helper commands.

Command Body

The body of a command file contains the instructions Claude follows when the command is invoked. Write it in the same directive style as skill bodies — clear, specific, and structured.

Example: A complete deploy command

---
name: deploy
description: "Deploy current branch to staging or production with pre-flight checks"
argument-hint: "<environment>"
---

## Pre-Flight Checks

Before deploying to `$0`, verify all of the following:

1. **Clean working tree.** Run `git status` and confirm no uncommitted changes.
   If there are uncommitted changes, stop and ask the user to commit or stash.

2. **Tests passing.** Run `npm test` and confirm all tests pass. If any test
   fails, stop and report the failures. Do not deploy with failing tests.

3. **Build succeeds.** Run `npm run build` and confirm it completes without
   errors. Check that the `dist/` directory is populated.

4. **Environment validation.** Confirm the target environment is valid:
   - `staging` — deploys to staging.example.com
   - `production` — deploys to example.com (requires confirmation)
   - Any other value — reject with an error message

## Deployment

If all pre-flight checks pass:

### Staging

```bash
npm run deploy:staging

Verify the deployment by checking https://staging.example.com/health.

Production

Production deployments require explicit user confirmation. Before running the deploy command, ask the user: “This will deploy to production. Continue?”

npm run deploy:production

After deployment:

  1. Check https://example.com/health
  2. Verify the deployed version matches the current git SHA
  3. Monitor error rates for 2 minutes using npm run monitor

Post-Deploy

Report the deployment status:

  • Environment deployed to
  • Git SHA deployed
  • Health check result
  • Any warnings from the build or deploy process

### Command Design Patterns

#### The Workflow Command

Orchestrates a multi-step process with checkpoints:

```markdown
---
name: release
description: "Prepare and publish a new release with changelog and tags"
argument-hint: "<version>"
---

## Steps

1. Validate that `$0` is a valid semver version
2. Update version in package.json
3. Generate changelog from commits since last tag
4. Create git tag v$0
5. Push tag and trigger CI pipeline
6. Wait for CI to pass, then publish to npm

The Analysis Command

Gathers data and produces a report:

---
name: coverage-report
description: "Generate test coverage report with gap analysis"
---

## Analysis

1. Run test suite with coverage: `npm run test -- --coverage`
2. Parse the coverage report from `coverage/lcov-report/index.html`
3. Identify files below 80% line coverage
4. For each under-covered file, identify the specific untested code paths
5. Produce a prioritized list of files needing additional tests

The Generator Command

Creates files from templates or specifications:

---
name: scaffold
description: "Generate a new feature module with tests, types, and documentation"
argument-hint: "<feature-name>"
---

Create the following files for the `$0` feature:

1. `src/features/$0/index.ts` — barrel export
2. `src/features/$0/$0.ts` — main implementation
3. `src/features/$0/$0.test.ts` — test file with basic structure
4. `src/features/$0/$0.types.ts` — TypeScript interfaces
5. `src/features/$0/README.md` — feature documentation

Follow the patterns established in `src/features/auth/` as a reference
implementation.

Hooks

What They Are

Hooks are lifecycle callbacks that execute automatically when Claude uses specific tools. They are configured in a skill’s YAML frontmatter and run shell commands before or after a tool call, without any user interaction.

Hooks are conceptually similar to Git hooks or CI pipeline hooks: they are invisible guardrails that enforce policies, validate actions, or perform side effects automatically.

Common use cases for hooks:

  • Linting files before they are written
  • Running tests after code is edited
  • Validating configuration files before they are saved
  • Logging tool usage for audit trails
  • Formatting code after generation

Hook Types

Two hook types are available:

HookTimingUse Case
pre-tool-callBefore the tool executesValidation, permission checks, input sanitization
post-tool-callAfter the tool executesFormatting, testing, logging, verification

Hook Configuration

Hooks are defined in the hooks field of a SKILL.md’s frontmatter:

---
name: safe-writer
description: |
  When writing TypeScript files. Automatically formats and lints
  all written files.
allowed-tools: Read, Write, Edit, Bash(npm:*), Glob
version: 1.0.0
author: Dev Team
license: MIT
hooks:
  pre-tool-call:
    - tool: Write
      command: "echo 'Writing file: ${CLAUDE_TOOL_INPUT_FILE}'"
  post-tool-call:
    - tool: Write
      command: "${CLAUDE_PLUGIN_ROOT}/scripts/format-and-lint.sh ${CLAUDE_TOOL_INPUT_FILE}"
    - tool: Edit
      command: "${CLAUDE_PLUGIN_ROOT}/scripts/format-and-lint.sh ${CLAUDE_TOOL_INPUT_FILE}"
---

Hook Structure

Each hook entry specifies:

FieldDescription
toolThe tool name this hook applies to (e.g., Write, Edit, Bash)
commandShell command to execute

Path Variables in Hooks

Hooks support the same path variables as skill bodies, plus tool-specific variables:

VariableDescription
${CLAUDE_PLUGIN_ROOT}Root directory of the plugin
${CLAUDE_SKILL_DIR}Directory containing the SKILL.md
${CLAUDE_TOOL_INPUT_FILE}File path argument passed to the tool

Always use ${CLAUDE_PLUGIN_ROOT} to reference scripts and resources within your plugin. This ensures hooks work correctly regardless of where the plugin is installed:

# Correct: portable path
command: "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"

# Wrong: hardcoded path
command: "/home/user/.claude/plugins/my-plugin/scripts/validate.sh"

Hook Examples

Auto-Format on Write

Format every file Claude writes using Prettier:

hooks:
  post-tool-call:
    - tool: Write
      command: "npx prettier --write ${CLAUDE_TOOL_INPUT_FILE} 2>/dev/null || true"
    - tool: Edit
      command: "npx prettier --write ${CLAUDE_TOOL_INPUT_FILE} 2>/dev/null || true"

The || true suffix prevents hook failures from blocking Claude’s workflow. Formatting is nice to have but should not stop execution.

Test After Edit

Run relevant tests whenever Claude edits a source file:

hooks:
  post-tool-call:
    - tool: Edit
      command: "${CLAUDE_PLUGIN_ROOT}/scripts/run-related-tests.sh ${CLAUDE_TOOL_INPUT_FILE}"

The helper script (run-related-tests.sh) might look like:

#!/bin/bash
# Find and run test file corresponding to the edited source file
SOURCE_FILE="$1"
TEST_FILE="${SOURCE_FILE%.ts}.test.ts"

if [ -f "$TEST_FILE" ]; then
  npx vitest run "$TEST_FILE" --reporter=verbose 2>&1 | tail -20
fi

Validation Gate

Prevent Claude from writing invalid configuration:

hooks:
  pre-tool-call:
    - tool: Write
      command: "${CLAUDE_PLUGIN_ROOT}/scripts/validate-config.sh ${CLAUDE_TOOL_INPUT_FILE}"

If the pre-tool-call hook exits with a non-zero status, the tool call is blocked. This creates a hard gate that prevents invalid files from being written.

Audit Logging

Log every Bash command Claude executes for compliance:

hooks:
  pre-tool-call:
    - tool: Bash
      command: "echo \"$(date -Iseconds) BASH: ${CLAUDE_TOOL_INPUT_FILE}\" >> ${CLAUDE_PLUGIN_ROOT}/audit.log"

Hook Design Principles

Keep hooks fast. Hooks run synchronously — Claude waits for them to complete before proceeding. A hook that takes 30 seconds to run will make Claude feel sluggish. If you need long-running validation, consider running it asynchronously or only on specific file patterns.

Make hooks resilient. Add 2>/dev/null, || true, or explicit error handling to prevent hook failures from crashing Claude’s workflow. A formatting hook should not prevent Claude from completing a task because Prettier is not installed.

Scope hooks narrowly. Apply hooks to specific tools rather than all tools. A formatting hook should apply to Write and Edit, not to Read or Glob.

Use hooks for policies, not features. Hooks are best for enforcing invariants (code is always formatted, configs are always valid, actions are always logged). If the behavior is the primary purpose of the skill, put it in the skill body instructions instead.

Commands vs Skills vs Hooks: Choosing the Right Mechanism

Understanding when to use each mechanism avoids confusion and produces cleaner plugin designs:

MechanismTriggerVisibilityBest For
CommandUser types /nameExplicit, in menuOne-off workflows, generators, reports
SkillClaude auto-detects relevanceInvisible to userDomain expertise, coding patterns
HookTool call occursInvisible to userValidation, formatting, logging

Practical example: Consider a plugin for a Go project.

  • Command (/go-bench): User explicitly requests a benchmark run with specific parameters.
  • Skill (go-patterns/SKILL.md): Automatically activates when Claude writes Go code, guiding it to follow idiomatic patterns.
  • Hook (post-tool-call on Write): Runs gofmt on every .go file Claude writes, ensuring consistent formatting without the user asking.

All three can coexist in the same plugin, each handling a different aspect of the Go development workflow.

Building Reusable Commands

Commands That Call Other Commands

Commands can reference other commands in their instructions:

---
name: full-release
description: "Run the complete release pipeline"
argument-hint: "<version>"
---

Execute these steps in order:

1. Run `/lint-check` to ensure code quality
2. Run `/test-all` to verify all tests pass
3. Run `/changelog $0` to generate the changelog
4. Run `/deploy production` to deploy the release

Commands That Delegate to Agents

Commands can delegate work to specialized agents:

---
name: security-review
description: "Comprehensive security audit of the current codebase"
---

Delegate this work to the security-auditor agent. Pass all findings back
as a structured report with severity ratings.

Commands with Dynamic Context

Commands can use DCI to adapt behavior based on the current environment:

---
name: setup
description: "Initialize development environment based on project type"
---

## Detected Environment

!`[ -f package.json ] && echo 'Node.js project' || echo 'Not a Node.js project'`
!`[ -f requirements.txt ] && echo 'Python project' || echo 'Not a Python project'`
!`[ -f go.mod ] && echo 'Go project' || echo 'Not a Go project'`

Based on the detected project type above, run the appropriate setup sequence.

Script Conventions

When plugins include shell scripts referenced by hooks or commands, follow these conventions:

  1. Make scripts executable. All .sh files must have chmod +x set. CI validation enforces this.

  2. Use portable paths. Always reference scripts via ${CLAUDE_PLUGIN_ROOT}:

command: "${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.sh"
  1. Include shebangs. Every script should start with #!/bin/bash or #!/usr/bin/env bash.

  2. Handle missing dependencies. Scripts should check for required tools and fail gracefully:

#!/bin/bash
if ! command -v prettier &> /dev/null; then
  echo "prettier not found, skipping formatting"
  exit 0
fi
prettier --write "$1"

Next Steps