Level 3: Slash Commands, Skills & Hooks
The definitive guide to extending Claude Code with reusable prompts, auto-loaded knowledge, and deterministic automation -- from basic slash commands to production-grade hook pipelines.
Table of Contents
- Overview: Three Ways to Extend Claude Code
- Part A: Slash Commands (Manual Triggers)
- Part B: Skills (Auto-Invoked Knowledge)
- Part C: Hooks (Automatic Triggers)
- How to Remember the Differences
- Exercises
- Pro Tips from Boris Cherny
- Anti-Patterns
- Official Documentation Links
1. Overview: Three Ways to Extend Claude Code
Claude Code ships with three distinct extension mechanisms. Each solves a different problem, and understanding which to use when is the core skill of Level 3.
| Mechanism | Trigger | Token Cost | Purpose |
|---|---|---|---|
| Commands | You type /name |
Full prompt | Saved prompts you reuse manually |
| Skills | Claude loads automatically (or you type /name) |
Full prompt | Background knowledge and complex workflows |
| Hooks | Fires on lifecycle events | Zero (command type) or minimal (prompt/agent type) | Deterministic automation |
The mental model:
- Commands = What YOU manually trigger (saved prompts)
- Skills = How Claude THINKS (loaded into context when relevant)
- Hooks = What happens AUTOMATICALLY (deterministic actions)
A single workflow might use all three. For example, Boris Cherny's /commit-push-pr command triggers a commit flow. A git-conventions skill teaches Claude the team's commit message style. A PostToolUse hook auto-formats any code Claude writes. Together, they create a seamless pipeline from writing code to shipping a PR.
Part A: Slash Commands (Manual Triggers)
What Commands Are
Slash commands are saved prompt templates that you invoke with /command-name. Think of them as macros for your most common Claude Code interactions. Instead of typing (or pasting) a 10-line prompt every time you want to create a commit, write a LinkedIn post, or run a code review, you save the prompt once and invoke it with a single keystroke.
Commands are the simplest extension mechanism:
- They are plain Markdown files
- They live in a
.claude/commands/directory - They support argument substitution
- They appear in Claude's
/autocomplete menu
Important note: Custom slash commands have been merged into the Skills system. A file at .claude/commands/review.md and a skill at .claude/skills/review/SKILL.md both create /review and work the same way. Your existing .claude/commands/ files continue to work. Skills add optional features like supporting files, frontmatter for invocation control, and automatic loading. This guide covers both approaches.
Creating Commands
Step 1: Create the commands directory
For project-level commands (shared with your team via git):
mkdir -p .claude/commands
For user-level commands (available across all your projects):
mkdir -p ~/.claude/commands
Step 2: Create a Markdown file
Each command is a single .md file. The filename (without .md) becomes the slash command name.
# Creates the /code-review command
nano .claude/commands/code-review.md
Step 3: Write the prompt template
The file contents are the prompt that Claude receives when you invoke the command. Use $ARGUMENTS for dynamic values.
Review the code in $ARGUMENTS for:
1. Security vulnerabilities
2. Performance issues
3. Code style violations
4. Missing error handling
Provide a severity rating (CRITICAL/WARNING/INFO) for each finding.
File Format and Naming
Naming rules:
- Filename becomes the command name (e.g.,
fix-issue.mdcreates/fix-issue) - Use lowercase letters, numbers, and hyphens only
- Maximum 64 characters
- No spaces in filenames (use hyphens instead)
- Nested directories create namespaced commands (e.g.,
commands/frontend/component.mdcreates/frontend:component)
File format:
- Plain Markdown
- Optional YAML frontmatter (same fields as Skills -- see Part B)
- The body is the prompt template
- Can reference other files with
@path/to/filesyntax - Can include
!shell-command`` for dynamic context injection
$ARGUMENTS Substitution
The $ARGUMENTS placeholder is replaced with everything the user types after the command name.
Command file (generate-tests.md):
Write comprehensive unit tests for $ARGUMENTS
Include:
- Happy path tests
- Edge cases (null, empty, boundary values)
- Error handling tests
- Use the existing test framework in this project
Invocation:
/generate-tests src/auth/login.ts
What Claude receives:
Write comprehensive unit tests for src/auth/login.ts
Include:
- Happy path tests
- Edge cases (null, empty, boundary values)
- Error handling tests
- Use the existing test framework in this project
If $ARGUMENTS is not present anywhere in the command file, the arguments are automatically appended to the end of the prompt as ARGUMENTS: <value>.
Positional Arguments
For commands that need multiple distinct inputs, use positional arguments:
| Syntax | Description | Example |
|---|---|---|
$ARGUMENTS |
All arguments as a single string | Everything after /command |
$ARGUMENTS[0] or $0 |
First argument | First space-separated token |
$ARGUMENTS[1] or $1 |
Second argument | Second space-separated token |
$ARGUMENTS[N] or $N |
Nth argument (0-based) | Nth space-separated token |
Command file (migrate-component.md):
Migrate the $0 component from $1 to $2.
Preserve all existing behavior and tests.
Maintain the same file structure and naming conventions.
Update all imports across the project.
Invocation:
/migrate-component SearchBar React Vue
What Claude receives:
Migrate the SearchBar component from React to Vue.
Preserve all existing behavior and tests.
Maintain the same file structure and naming conventions.
Update all imports across the project.
User-Level vs Project-Level Commands
| Location | Path | Scope | Shared via Git? |
|---|---|---|---|
| Project | .claude/commands/*.md |
This project only | Yes |
| Personal | ~/.claude/commands/*.md |
All your projects | No |
| Enterprise | Managed settings (see permissions docs) | All users in org | Yes (admin-managed) |
Priority order when names conflict: Enterprise > Personal > Project
Recommendation: Put team-shared workflows in .claude/commands/ (project level). Put your personal shortcuts in ~/.claude/commands/ (user level).
Invoking Commands
- Type
/in the Claude Code CLI - Start typing the command name -- autocomplete kicks in
- Select the command
- Type any arguments after the command name
- Press Enter
/code-review src/api/users.ts
Important: If you create a new command file while Claude Code is running, you need to restart Claude Code for it to appear in the autocomplete menu. Exit with Ctrl+C and run claude again.
Argument hints: If your command uses YAML frontmatter (the Skills format), you can add an argument-hint field to show usage hints during autocomplete:
---
name: fix-issue
argument-hint: "[issue-number]"
---
Fix GitHub issue $ARGUMENTS following our coding standards.
Complete Example Command Files
Example 1: commit-push-pr (Boris Cherny style)
This is the command Boris Cherny uses "dozens of times every day." It automates the entire commit-to-PR pipeline with dynamic context injection.
File: .claude/commands/commit-push-pr.md
---
name: commit-push-pr
description: Commit all changes, push to remote, and create a pull request.
disable-model-invocation: true
argument-hint: "[optional PR title override]"
---
## Context
Current branch: !`git branch --show-current`
Changed files: !`git diff --name-only`
Staged files: !`git diff --cached --name-only`
Recent commits on this branch: !`git log --oneline -5`
## Instructions
1. Review the changes and stage all relevant files
2. Write a clear, conventional commit message following the patterns from recent commits above
3. Push the branch to origin (create remote branch if needed with -u flag)
4. Create a pull request with:
- A concise title (under 70 characters)
- A description summarizing what changed and why
- Link any relevant issues
If a PR title was provided as an argument, use it: $ARGUMENTS
Do NOT amend previous commits. Create new commits only.
Do NOT force push.
Usage:
/commit-push-pr
/commit-push-pr "Add user authentication endpoints"
Example 2: code-review
File: .claude/commands/code-review.md
---
name: code-review
description: Perform a thorough code review on specified files or the current diff.
disable-model-invocation: true
argument-hint: "[file-or-directory]"
---
## Context
Git diff (staged + unstaged): !`git diff HEAD`
## Instructions
Perform a thorough code review on $ARGUMENTS (or the current diff if no argument provided).
Check for:
1. **Security** - SQL injection, XSS, auth bypass, exposed secrets, insecure defaults
2. **Correctness** - Logic errors, off-by-one, null handling, race conditions
3. **Performance** - N+1 queries, unnecessary re-renders, missing indexes, memory leaks
4. **Maintainability** - Naming clarity, function length, code duplication, coupling
5. **Testing** - Missing test coverage, untested edge cases, flaky test patterns
6. **Standards** - Deviations from project conventions (check CLAUDE.md)
Format each finding as:
**[CRITICAL/WARNING/INFO]** `file:line` - Description
> Suggested fix or explanation
End with a summary: total findings by severity and an overall assessment.
Usage:
/code-review src/api/
/code-review
Example 3: generate-tests
File: .claude/commands/generate-tests.md
---
name: generate-tests
description: Generate comprehensive tests for a file or module.
disable-model-invocation: true
argument-hint: "[source-file]"
---
Generate comprehensive tests for $ARGUMENTS.
## Requirements
1. Read the source file and understand its public API
2. Identify the existing test framework and patterns in this project
3. Create tests covering:
- Happy path for every public function/method
- Edge cases: null, undefined, empty, boundary values
- Error paths: invalid inputs, network failures, timeouts
- Integration: how this module interacts with its dependencies
4. Follow the existing test file naming convention
5. Use the existing assertion style (check nearby test files for patterns)
6. Mock external dependencies, not internal logic
7. Run the tests and fix any failures before finishing
## Output
Place the test file next to the source file following the project's naming convention
(e.g., `*.test.ts`, `*.spec.js`, `*_test.go`).
Usage:
/generate-tests src/services/payment.ts
Example 4: linkedin-post
File: ~/.claude/commands/linkedin-post.md (user-level -- works across projects)
---
name: linkedin-post
description: Write LinkedIn post variations on a topic.
disable-model-invocation: true
argument-hint: "[topic]"
---
Write three LinkedIn post variations about $ARGUMENTS.
Rules:
- Start with a strong hook (question, bold claim, or story)
- Line break after the hook
- Body: 3-5 short paragraphs (1-2 sentences each)
- End with a question or call-to-action
- Keep each post under 200 words
- No hashtags
- No emojis
- Conversational, first-person tone
- Each variation should use a different angle (technical, personal story, contrarian)
Save all three drafts to ./output/linkedin-drafts.md with a separator between each.
Usage:
/linkedin-post the benefits of code review automation
Example 5: analyze-youtube-cc
File: .claude/commands/analyze-youtube-cc.md
---
name: analyze-youtube-cc
description: Download and analyze YouTube closed captions for content creation.
disable-model-invocation: true
argument-hint: "[youtube-url]"
---
## Instructions
1. Download the closed captions from this YouTube video: $ARGUMENTS
```bash
yt-dlp --write-auto-sub --sub-lang en --skip-download -o "%(title)s" "$ARGUMENTS"
Read the downloaded .vtt or .srt file
Create a comprehensive analysis:
- Summary (3-5 sentences capturing the main points)
- Key Takeaways (bulleted list, 5-10 items)
- Notable Quotes (with approximate timestamps)
- Action Items (concrete things a viewer should do)
- Content Structure (how was the video organized? what worked well?)
Save the analysis to
./output/youtube-analysis-<sanitized-title>.mdClean up the downloaded caption file after analysis
**Usage:**
/analyze-youtube-cc https://www.youtube.com/watch?v=Y09u_S3w2c8
#### Example 6: migration-helper
**File: `.claude/commands/migration-helper.md`**
```markdown
---
name: migration-helper
description: Generate a migration plan and execute it for a library or framework upgrade.
disable-model-invocation: true
argument-hint: "[library-name] [from-version] [to-version]"
---
## Context
Current dependencies: !`cat package.json | jq '.dependencies, .devDependencies'`
Node version: !`node --version`
## Instructions
Plan and execute a migration of $0 from $1 to $2.
### Phase 1: Research
- Read the official migration guide for $0 $1 -> $2
- Identify all breaking changes relevant to our codebase
- Search for usages of deprecated APIs in our code
### Phase 2: Plan
- List every file that needs changes
- Categorize changes: automated (search-replace) vs manual (logic changes)
- Identify any dependency conflicts
- Estimate risk level for each change
### Phase 3: Execute
- Update the package version
- Apply automated changes first
- Apply manual changes
- Update any related configuration files
- Update TypeScript types if applicable
### Phase 4: Verify
- Run the full test suite
- Run the linter
- Run the build
- Report any remaining issues
Present the plan before executing. Wait for confirmation before Phase 3.
Usage:
/migration-helper react 18 19
/migration-helper typescript 4.9 5.5
Best Practices for Command Design
Use
disable-model-invocation: truefor any command with side effects (committing, deploying, posting). You do not want Claude deciding on its own to run your deploy command.Include verification steps. Every command that modifies code should end with "run the tests" or "verify the build passes." Do not let the command finish without confirmation that the output is correct.
Use
!command`` for dynamic context. Precomputing git status, file lists, or environment info saves tokens and gives Claude grounded facts instead of asking it to guess.Keep commands focused. A command that does one thing well is better than a command that tries to do everything. If you need a pipeline, use separate commands or combine with hooks.
Add argument hints. The
argument-hintfrontmatter field shows users what input the command expects during autocomplete. This prevents misuse.Reference project conventions. Use
@CLAUDE.mdor@.claude/rules/git-conventions.mdto pull in project rules rather than duplicating them in the command.Put personal commands at user level. Commands like
/linkedin-postor/meeting-notesthat have nothing to do with a specific project belong in~/.claude/commands/.Put team commands at project level. Commands like
/deployor/code-reviewthat encode team conventions belong in.claude/commands/and should be committed to version control.
Part B: Skills (Auto-Invoked Knowledge)
What Skills Are
Skills are the evolution of slash commands. While commands are prompts you trigger manually, skills are knowledge modules that Claude can load automatically when they are relevant to your conversation, OR that you can invoke manually with /skill-name.
The key differences from plain commands:
| Feature | Commands (.claude/commands/) |
Skills (.claude/skills/) |
|---|---|---|
Manual invocation with /name |
Yes | Yes |
| Auto-loaded by Claude when relevant | No | Yes (default) |
| Supporting files (templates, scripts, examples) | No | Yes |
| YAML frontmatter for invocation control | Optional | Optional |
| Directory-based (multiple files) | No (single .md file) | Yes |
Skills follow the open Agent Skills standard, which works across multiple AI tools. Claude Code extends the standard with additional features like invocation control, subagent execution, and dynamic context injection.
When to use a command vs a skill:
- Use a command for simple, single-file prompt templates
- Use a skill when you need supporting files, automatic loading, or invocation control
- Both create a
/nameentry in the slash menu - If a skill and command share the same name, the skill takes precedence
Directory Structure
Each skill lives in its own directory with SKILL.md as the entry point:
.claude/skills/
explain-code/
SKILL.md # Main instructions (required)
deploy/
SKILL.md # Main instructions (required)
checklist.md # Pre-deploy checklist
scripts/
validate-env.sh # Environment validation script
api-conventions/
SKILL.md # Main instructions (required)
reference.md # Detailed API style guide
examples/
good-endpoint.md # Example of a well-designed endpoint
bad-endpoint.md # Example of what to avoid
The SKILL.md file is the only required file. Everything else is optional supporting material that Claude can reference when the skill is active.
YAML Frontmatter Fields
The frontmatter sits between --- markers at the top of SKILL.md:
---
name: my-skill
description: What this skill does and when to use it.
disable-model-invocation: true
user-invocable: false
allowed-tools: Read, Grep, Glob
model: sonnet
context: fork
agent: Explore
argument-hint: "[file-path]"
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate.sh"
---
Your skill instructions here...
Complete frontmatter reference:
| Field | Required | Default | Description |
|---|---|---|---|
name |
No | Directory name | Display name and /slash-command name. Lowercase letters, numbers, hyphens only. Max 64 chars. |
description |
Recommended | First paragraph of content | What the skill does and when to use it. Claude uses this to decide when to auto-load the skill. |
argument-hint |
No | None | Hint shown during autocomplete. Example: [issue-number] or [filename] [format]. |
disable-model-invocation |
No | false |
When true, only you can trigger this skill. Claude cannot auto-load it. Use for workflows with side effects. |
user-invocable |
No | true |
When false, hides the skill from the / menu. Only Claude can load it. Use for background knowledge. |
allowed-tools |
No | All tools | Tools Claude can use without asking permission when this skill is active. Comma-separated. |
model |
No | Inherited | Model to use when this skill is active. |
context |
No | Inline | Set to fork to run in a forked subagent context. |
agent |
No | general-purpose |
Which subagent type to use when context: fork is set. Options: Explore, Plan, general-purpose, or custom agent name. |
hooks |
No | None | Hooks scoped to this skill's lifecycle. Same format as settings.json hooks. |
How Claude Decides When to Load a Skill
Understanding the loading mechanism is critical to writing effective skills.
Step 1: Description scanning. Skill descriptions are loaded into Claude's context at session start so Claude knows what skills are available. The full skill content is NOT loaded yet -- only the descriptions.
Step 2: Relevance matching. When you ask a question or give a task, Claude evaluates whether any skill descriptions match the current context. If a match is found, Claude loads the full SKILL.md content.
Step 3: Invocation. Claude uses the skill's instructions to guide its response.
This means:
- Write descriptions that include keywords users would naturally say
- A skill with
description: "Explains code with visual diagrams and analogies"loads when a user says "how does this work?" or "explain this code" - A vague description like "helpful utility" will rarely match anything
Budget limit: Skill descriptions are loaded into context at 2% of the context window (with a fallback of 16,000 characters). If you have many skills, some may be excluded. Run /context to check for warnings. Override the limit with the SLASH_COMMAND_TOOL_CHAR_BUDGET environment variable.
The invocation control matrix:
| Frontmatter | You can invoke | Claude can invoke | When loaded into context |
|---|---|---|---|
| (defaults) | Yes | Yes | Description always in context; full skill loads when invoked |
disable-model-invocation: true |
Yes | No | Description NOT in context; full skill loads when you invoke |
user-invocable: false |
No | Yes | Description always in context; full skill loads when invoked |
Supporting Files
Skills can include multiple files in their directory. This keeps SKILL.md focused while letting Claude access detailed reference material only when needed.
Best practices for supporting files:
- Keep
SKILL.mdunder 500 lines - Move detailed API docs, large examples, and reference material to separate files
- Reference supporting files from
SKILL.mdso Claude knows they exist:
## Additional resources
- For complete API patterns, see [reference.md](reference.md)
- For usage examples, see [examples/](examples/)
- To run validation, execute [scripts/validate.sh](scripts/validate.sh)
Claude reads supporting files on demand -- they are not automatically loaded into context. This keeps context lean while making detailed information available.
SkillsMP Marketplace
SkillsMP is a community marketplace with over 200,000 Claude Code skills organized by category.
Browsing skills:
- Visit skillsmp.com/categories for organized browsing
- Categories include: code quality, testing, documentation, DevOps, data analysis, content creation, and many more
- Each skill has a description, usage instructions, and installation steps
Installing a skill from GitHub:
# Navigate to your skills directory
cd .claude/skills/
# Clone the skill repository
git clone https://github.com/user/skill-name
# The skill is now available as /skill-name
Installing from a Gist or single file:
mkdir -p .claude/skills/skill-name
curl -o .claude/skills/skill-name/SKILL.md https://raw.githubusercontent.com/user/repo/main/SKILL.md
Sharing your own skills:
- Project skills: commit
.claude/skills/to version control - Plugins: create a
skills/directory in your Claude Code plugin - Managed: deploy organization-wide through managed settings
Running Skills in Subagents
Add context: fork to your frontmatter when you want a skill to run in isolation. The skill content becomes the prompt that drives a new subagent. That subagent does NOT have access to your conversation history -- it starts fresh.
Why use forked context:
- Keeps your main conversation context clean
- Prevents long skill outputs from eating your context window
- Provides isolation for tasks that should not affect your current work
- Results are summarized and returned to your main conversation
Important: context: fork only makes sense for skills with explicit task instructions. If your skill contains guidelines like "use these API conventions" without a concrete task, the subagent receives the guidelines but no actionable prompt, and returns without useful output.
Example: Research skill using Explore agent:
---
name: deep-research
description: Research a topic thoroughly in the codebase
context: fork
agent: Explore
---
Research $ARGUMENTS thoroughly:
1. Find relevant files using Glob and Grep
2. Read and analyze the code
3. Summarize findings with specific file references
4. Identify patterns, inconsistencies, and opportunities
The agent field determines the execution environment:
Explore-- read-only tools (Glob, Grep, Read), optimized for codebase explorationPlan-- read-only, designed for architecture planninggeneral-purpose-- full tool access (Read, Write, Edit, Bash, etc.)- Any custom agent name from
.claude/agents/
Dynamic Context Injection
The !command`` syntax runs shell commands BEFORE the skill content is sent to Claude. The command output replaces the placeholder. Claude sees the actual data, not the command itself.
Example: PR summary skill with live data:
---
name: pr-summary
description: Summarize changes in the current pull request
context: fork
agent: Explore
allowed-tools: Bash(gh *)
---
## Pull request context
- PR diff: !`gh pr diff`
- PR comments: !`gh pr view --comments`
- Changed files: !`gh pr diff --name-only`
## Your task
Summarize this pull request. Include:
1. What changed and why
2. Risk assessment (high/medium/low)
3. Suggested reviewers based on file ownership
When this skill runs:
- Each
!command`` executes immediately (before Claude processes anything) - The output replaces the placeholder in the skill content
- Claude receives the fully-rendered prompt with actual PR data
This is preprocessing -- Claude does not execute these commands. It only sees the final result.
Other dynamic context examples:
Current branch: !`git branch --show-current`
Last 5 commits: !`git log --oneline -5`
Environment: !`node --version && npm --version`
Open issues: !`gh issue list --limit 5 --state open`
Skill vs Command vs Hook -- When to Use Which
| Criteria | Command | Skill | Hook |
|---|---|---|---|
| Trigger | You type /name |
Claude auto-loads OR you type /name |
Fires on lifecycle event |
| Token cost | Full prompt | Full prompt | Zero (command type) |
| Multiple files | No (single .md) | Yes (directory) | N/A (JSON config) |
| Auto-invocation | No | Yes (unless disabled) | Always (when event fires) |
| Side effects | Only what the prompt produces | Only what the prompt produces | Shell command, no LLM |
| Use for | Repetitive manual tasks | Domain knowledge, complex workflows | Formatting, linting, validation, notifications |
| Examples | /commit-push-pr, /generate-tests |
API conventions, code explanation | Auto-format after edit, block rm -rf |
| Can block actions | No | No | Yes (exit code 2) |
| Runs in subagent | No | Yes (with context: fork) |
Yes (agent type hooks) |
| Supporting files | No | Yes | External scripts |
Decision flowchart:
Do you need this to happen every time, without fail?
YES --> Hook (deterministic, no LLM involved)
NO --> Continue
Does Claude need to decide when to apply this knowledge?
YES --> Skill (auto-loaded based on relevance)
NO --> Continue
Is this a prompt you run manually on demand?
YES --> Command (or Skill with disable-model-invocation)
NO --> Probably a Skill with user-invocable: false
Complete Example Skill Files
Example 1: API Conventions (Background Knowledge)
File: .claude/skills/api-conventions/SKILL.md
---
name: api-conventions
description: RESTful API design patterns and conventions for this codebase. Use when writing, reviewing, or modifying API endpoints.
user-invocable: false
---
## API Design Standards
### URL Structure
- Resources are plural nouns: `/users`, `/orders`, `/products`
- Nested resources for relationships: `/users/:id/orders`
- Maximum nesting depth: 2 levels
- Use kebab-case for multi-word resources: `/order-items`
### HTTP Methods
- GET: Read (never modify state)
- POST: Create new resources
- PUT: Full update (replace entire resource)
- PATCH: Partial update (modify specific fields)
- DELETE: Remove resources (soft-delete by default)
### Response Format
All responses follow this structure:
```json
{
"data": {},
"meta": { "page": 1, "total": 100 },
"errors": []
}
Error Codes
- 400: Validation error (include field-level details)
- 401: Authentication required
- 403: Insufficient permissions
- 404: Resource not found
- 409: Conflict (duplicate, version mismatch)
- 422: Unprocessable entity (valid syntax, invalid semantics)
- 500: Server error (never expose internals)
Naming Conventions
- Request/response fields: camelCase
- Database columns: snake_case
- Query parameters: camelCase
- Headers: X-Custom-Header format
For complete reference with examples, see reference.md.
#### Example 2: Deploy to Production (Manual-Only Task)
**File: `.claude/skills/deploy/SKILL.md`**
```yaml
---
name: deploy
description: Deploy the application to production with full verification.
context: fork
disable-model-invocation: true
argument-hint: "[environment: staging|production]"
---
Deploy the application to $ARGUMENTS (default: staging).
## Pre-deployment checklist
1. Verify all tests pass: `npm test`
2. Verify the build succeeds: `npm run build`
3. Check for uncommitted changes: `git status`
4. Verify you are on the correct branch
## Deployment steps
1. Tag the release: `git tag -a v$(date +%Y%m%d-%H%M%S) -m "Deploy to $ARGUMENTS"`
2. Push the tag: `git push origin --tags`
3. Run the deployment script: `./scripts/deploy.sh $ARGUMENTS`
## Post-deployment verification
1. Check the health endpoint: `curl https://$ARGUMENTS.example.com/health`
2. Verify the version matches the tag
3. Run smoke tests: `npm run test:smoke -- --env=$ARGUMENTS`
4. Report the deployment status
For environment-specific configuration, see [checklist.md](checklist.md).
Example 3: Explain Code (Auto-Invoked)
File: ~/.claude/skills/explain-code/SKILL.md
---
name: explain-code
description: Explains code with visual diagrams and analogies. Use when explaining how code works, teaching about a codebase, or when the user asks "how does this work?"
---
When explaining code, always include:
1. **Start with an analogy**: Compare the code to something from everyday life.
Example: "This middleware chain is like an airport security line -- each checkpoint
examines one aspect of the passenger (request) before passing them to the next."
2. **Draw a diagram**: Use ASCII art to show the flow, structure, or relationships.
Request -> Auth Middleware -> Rate Limiter -> Router -> Handler -> Response | | v v 401 Reject 429 Too Many
3. **Walk through the code**: Explain step-by-step what happens when the code runs.
Focus on the "why" behind design decisions, not just the "what."
4. **Highlight a gotcha**: What is a common mistake or misconception about this code?
Keep explanations conversational. For complex concepts, use multiple analogies at
different levels of abstraction.
Example 4: Session Logger (Using String Substitutions)
File: .claude/skills/session-logger/SKILL.md
---
name: session-logger
description: Log important decisions and changes made during this session for handoff notes.
disable-model-invocation: true
---
Log the following activity to logs/${CLAUDE_SESSION_ID}.log:
## Session Activity Entry
**Timestamp:** !`date -u +"%Y-%m-%dT%H:%M:%SZ"`
**Session:** ${CLAUDE_SESSION_ID}
**Context:** $ARGUMENTS
Record:
1. What was decided or changed
2. Why (the reasoning)
3. What files were modified
4. Any follow-up items for the next session
Append to the log file (do not overwrite previous entries).
Example 5: Codebase Visualizer (With Bundled Script)
File: ~/.claude/skills/codebase-visualizer/SKILL.md
---
name: codebase-visualizer
description: Generate an interactive HTML tree visualization of the project structure. Use when exploring a new repo or understanding project layout.
allowed-tools: Bash(python *)
---
# Codebase Visualizer
Generate an interactive HTML tree view of the project structure.
## Usage
Run the visualization script from your project root:
```bash
python ~/.claude/skills/codebase-visualizer/scripts/visualize.py .
This creates codebase-map.html in the current directory and opens it in your
default browser.
What the visualization shows
- Collapsible directories (click to expand/collapse)
- File sizes displayed next to each file
- Color-coded file types
- Directory size totals
- Summary sidebar with file count and type breakdown
This skill bundles a Python script in its `scripts/` directory that Claude executes. The script generates a self-contained HTML file with an interactive codebase tree.
---
## Part C: Hooks (Automatic Triggers) <a name="part-c"></a>
### What Hooks Are <a name="what-hooks-are"></a>
Hooks are user-defined actions that execute automatically at specific points in Claude Code's lifecycle. They are fundamentally different from commands and skills:
- **Commands and skills** feed prompt text into the LLM, consuming tokens
- **Hooks** (command type) run shell commands directly, consuming zero LLM tokens
- **Hooks are deterministic** -- they always run when their event fires, no AI judgment involved
- **Hooks can block actions** -- a PreToolUse hook can prevent Claude from running a dangerous command
Think of hooks as event listeners. Claude Code emits events (file edited, command about to run, session starting), and your hooks respond to those events with shell scripts, validation checks, formatting runs, or notifications.
**The three hook types:**
- `type: "command"` -- Run a shell command. Zero tokens. Deterministic.
- `type: "prompt"` -- Send a prompt to a fast Claude model for yes/no evaluation. Minimal tokens.
- `type: "agent"` -- Spawn a subagent that can use tools to verify conditions. More tokens, more capability.
### Hook Events Reference Table <a name="hook-events-table"></a>
Every hook event, when it fires, what it can match, and whether it can block:
| Event | When It Fires | Matcher Filters | Can Block? |
|-------|---------------|-----------------|------------|
| `SessionStart` | Session begins or resumes | `startup`, `resume`, `clear`, `compact` | No |
| `UserPromptSubmit` | User submits a prompt, before Claude processes it | No matcher support (always fires) | Yes |
| `PreToolUse` | Before a tool call executes | Tool name: `Bash`, `Edit`, `Write`, `Read`, `Glob`, `Grep`, `Task`, `WebFetch`, `WebSearch`, `mcp__*` | Yes |
| `PermissionRequest` | When a permission dialog appears | Tool name (same as PreToolUse) | Yes |
| `PostToolUse` | After a tool call succeeds | Tool name (same as PreToolUse) | No (tool already ran) |
| `PostToolUseFailure` | After a tool call fails | Tool name (same as PreToolUse) | No (tool already failed) |
| `Notification` | Claude sends a notification | `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog` | No |
| `SubagentStart` | A subagent is spawned | Agent type: `Bash`, `Explore`, `Plan`, or custom agent names | No |
| `SubagentStop` | A subagent finishes | Agent type (same as SubagentStart) | Yes |
| `Stop` | Claude finishes responding (not on user interrupt) | No matcher support (always fires) | Yes |
| `TeammateIdle` | Agent team teammate is about to go idle | No matcher support (always fires) | Yes |
| `TaskCompleted` | A task is being marked as completed | No matcher support (always fires) | Yes |
| `ConfigChange` | A configuration file changes during session | `user_settings`, `project_settings`, `local_settings`, `policy_settings`, `skills` | Yes (except `policy_settings`) |
| `PreCompact` | Before context compaction | `manual`, `auto` | No |
| `SessionEnd` | Session terminates | `clear`, `logout`, `prompt_input_exit`, `bypass_permissions_disabled`, `other` | No |
### Hook Configuration in settings.json <a name="hook-config"></a>
Hooks are defined in JSON settings files. The configuration has three levels of nesting:
- Hook event (e.g., "PreToolUse")
- Matcher group (e.g., match only "Bash" tools)
- Hook handler(s) (the shell command or prompt to run)
- Matcher group (e.g., match only "Bash" tools)
**Complete configuration structure:**
```json
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"EventName": [
{
"matcher": "regex-pattern",
"hooks": [
{
"type": "command",
"command": "/path/to/script.sh",
"timeout": 30,
"async": false,
"statusMessage": "Running validation..."
}
]
},
{
"matcher": "another-pattern",
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate this: $ARGUMENTS",
"model": "haiku",
"timeout": 30
}
]
}
]
}
}
Where hooks are defined (scope determines precedence):
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json |
All your projects | No (local to your machine) |
.claude/settings.json |
This project | Yes (commit to repo) |
.claude/settings.local.json |
This project | No (gitignored) |
| Managed policy settings | Organization-wide | Yes (admin-controlled) |
Plugin hooks/hooks.json |
When plugin is enabled | Yes (bundled with plugin) |
| Skill or agent frontmatter | While that component is active | Yes (defined in component file) |
Important: Direct edits to hooks in settings files do not take effect immediately. Claude Code captures a snapshot of hooks at startup. If hooks are modified externally during a session, Claude Code warns you and requires review in the /hooks menu before changes apply. Hooks added through the /hooks menu take effect immediately.
Hook Types: command, prompt, agent
Command Hooks (type: "command")
The most common type. Runs a shell command with zero LLM token cost. Your script receives JSON on stdin and communicates results through exit codes and stdout.
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 30,
"async": false,
"statusMessage": "Running linter..."
}
| Field | Required | Default | Description |
|---|---|---|---|
type |
Yes | -- | Must be "command" |
command |
Yes | -- | Shell command to execute |
timeout |
No | 600 (10 min) | Seconds before canceling |
async |
No | false |
If true, runs in background without blocking |
statusMessage |
No | None | Custom spinner message while hook runs |
once |
No | false |
If true, runs only once per session (skills only) |
Prompt Hooks (type: "prompt")
Sends a prompt to a fast Claude model (Haiku by default) for yes/no evaluation. The model returns a JSON decision: { "ok": true } to allow or { "ok": false, "reason": "..." } to block.
{
"type": "prompt",
"prompt": "Check if all tasks are complete. Context: $ARGUMENTS. Respond with {\"ok\": true} or {\"ok\": false, \"reason\": \"...\"}",
"model": "haiku",
"timeout": 30
}
Prompt hooks work with: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, UserPromptSubmit, Stop, SubagentStop, and TaskCompleted. They do NOT work with TeammateIdle.
Agent Hooks (type: "agent")
Like prompt hooks but with multi-turn tool access. Spawns a subagent that can read files, search code, and inspect the codebase before returning a decision. More capable but more expensive.
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"model": "haiku",
"timeout": 120
}
When to use which hook type:
| Type | Token Cost | Capability | Use When |
|---|---|---|---|
command |
Zero | Run shell scripts | Formatting, linting, notifications, file validation |
prompt |
Minimal | LLM yes/no decision | Evaluating if conditions are met from hook input alone |
agent |
Moderate | Multi-turn tool use | Verification that requires reading files or running commands |
Matchers
Matchers filter WHEN a hook fires. Without a matcher, a hook fires on every occurrence of its event.
Matcher syntax: Matchers are regex patterns. Use "*", "", or omit matcher entirely to match everything.
Examples:
{"matcher": "Bash"} // Only matches the Bash tool
{"matcher": "Edit|Write"} // Matches Edit OR Write
{"matcher": "mcp__github__.*"} // Matches all GitHub MCP tools
{"matcher": "Notebook.*"} // Matches any tool starting with Notebook
What each event matches on:
| Event | Matches Against | Values |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest |
Tool name | Bash, Edit, Write, Read, Glob, Grep, Task, WebFetch, WebSearch, mcp__server__tool |
SessionStart |
How session started | startup, resume, clear, compact |
SessionEnd |
Why session ended | clear, logout, prompt_input_exit, bypass_permissions_disabled, other |
Notification |
Notification type | permission_prompt, idle_prompt, auth_success, elicitation_dialog |
SubagentStart, SubagentStop |
Agent type | Bash, Explore, Plan, or custom agent names |
ConfigChange |
Configuration source | user_settings, project_settings, local_settings, policy_settings, skills |
PreCompact |
Compaction trigger | manual, auto |
UserPromptSubmit, Stop, TeammateIdle, TaskCompleted |
N/A | Always fires; matcher is ignored |
MCP tool matching:
MCP tools follow the pattern mcp__<server>__<tool>. Use regex to match groups:
{"matcher": "mcp__memory__.*"} // All memory server tools
{"matcher": "mcp__.*__write.*"} // Any write tool from any MCP server
{"matcher": "mcp__github__search_repositories"} // Specific MCP tool
Exit Codes
Exit codes are how command-type hooks communicate their decision back to Claude Code.
| Exit Code | Meaning | Behavior |
|---|---|---|
| 0 | Success / Allow | Action proceeds. Claude Code parses stdout for optional JSON output. |
| 2 | Block | Action is blocked. stderr is fed back to Claude as an error message. JSON on stdout is ignored. |
| Any other | Non-blocking error | Action proceeds. stderr is logged (visible in verbose mode with Ctrl+O). |
Exit code 2 behavior by event:
| Hook Event | What Happens on Exit 2 |
|---|---|
PreToolUse |
Blocks the tool call |
PermissionRequest |
Denies the permission |
UserPromptSubmit |
Blocks prompt processing and erases the prompt |
Stop |
Prevents Claude from stopping; continues the conversation |
SubagentStop |
Prevents the subagent from stopping |
TeammateIdle |
Prevents the teammate from going idle |
TaskCompleted |
Prevents the task from being marked completed |
ConfigChange |
Blocks the configuration change (except policy_settings) |
PostToolUse |
Shows stderr to Claude (tool already ran; cannot undo) |
PostToolUseFailure |
Shows stderr to Claude |
Notification |
Shows stderr to user only |
SubagentStart |
Shows stderr to user only |
SessionStart |
Shows stderr to user only |
SessionEnd |
Shows stderr to user only |
PreCompact |
Shows stderr to user only |
Hook Input (JSON via stdin)
When a hook fires, Claude Code sends JSON data to your script's stdin. Every event includes common fields, plus event-specific fields.
Common fields (all events):
{
"session_id": "abc123",
"transcript_path": "/Users/you/.claude/projects/.../transcript.jsonl",
"cwd": "/Users/you/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse"
}
| Field | Description |
|---|---|
session_id |
Current session identifier |
transcript_path |
Path to conversation JSON transcript |
cwd |
Current working directory when the hook was invoked |
permission_mode |
Current permission mode: default, plan, acceptEdits, dontAsk, or bypassPermissions |
hook_event_name |
Name of the event that fired |
Event-specific fields:
| Event | Additional Fields |
|---|---|
SessionStart |
source (startup/resume/clear/compact), model, optional agent_type |
UserPromptSubmit |
prompt (the text the user submitted) |
PreToolUse |
tool_name, tool_input (varies by tool), tool_use_id |
PermissionRequest |
tool_name, tool_input, permission_suggestions |
PostToolUse |
tool_name, tool_input, tool_response, tool_use_id |
PostToolUseFailure |
tool_name, tool_input, tool_use_id, error, is_interrupt |
Notification |
message, title, notification_type |
SubagentStart |
agent_id, agent_type |
SubagentStop |
stop_hook_active, agent_id, agent_type, agent_transcript_path, last_assistant_message |
Stop |
stop_hook_active, last_assistant_message |
TeammateIdle |
teammate_name, team_name |
TaskCompleted |
task_id, task_subject, task_description, teammate_name, team_name |
ConfigChange |
source, file_path |
PreCompact |
trigger (manual/auto), custom_instructions |
SessionEnd |
reason |
Example: Full PreToolUse input for a Bash command:
{
"session_id": "abc123",
"transcript_path": "/Users/you/.claude/projects/.../transcript.jsonl",
"cwd": "/Users/you/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite",
"timeout": 120000
},
"tool_use_id": "toolu_01ABC123..."
}
Example: Full PostToolUse input after a file write:
{
"session_id": "abc123",
"transcript_path": "/Users/you/.claude/projects/.../transcript.jsonl",
"cwd": "/Users/you/my-project",
"permission_mode": "default",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/Users/you/my-project/src/auth.ts",
"content": "export function login() { ... }"
},
"tool_response": {
"filePath": "/Users/you/my-project/src/auth.ts",
"success": true
},
"tool_use_id": "toolu_01DEF456..."
}
Hook Output (JSON via stdout)
For finer-grained control beyond exit codes, exit 0 and print a JSON object to stdout. Claude Code reads specific fields from that JSON.
Important: You must choose one approach per hook, not both. Either use exit codes alone (exit 2 to block), or exit 0 and print JSON for structured control. Claude Code only processes JSON on exit 0.
Universal JSON output fields (all events):
| Field | Default | Description |
|---|---|---|
continue |
true |
If false, Claude stops processing entirely. Takes precedence over event-specific decisions. |
stopReason |
None | Message shown to user when continue is false. Not shown to Claude. |
suppressOutput |
false |
If true, hides stdout from verbose mode output. |
systemMessage |
None | Warning message shown to the user. |
Event-specific decision patterns:
Different events use different JSON structures to control behavior:
Top-level decision (used by UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop, SubagentStop, ConfigChange):
{
"decision": "block",
"reason": "Tests must pass before proceeding"
}
hookSpecificOutput with permissionDecision (PreToolUse only):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database writes are not allowed",
"updatedInput": {
"command": "echo 'blocked'"
},
"additionalContext": "Running in read-only mode."
}
}
PreToolUse permissionDecision options:
"allow"-- Bypass the permission system, tool call proceeds"deny"-- Prevent the tool call, reason is shown to Claude"ask"-- Show the permission prompt to the user
hookSpecificOutput with decision.behavior (PermissionRequest only):
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": {
"command": "npm run lint"
}
}
}
}
Async Hooks
By default, hooks block Claude's execution until they complete. For long-running tasks, set "async": true to run the hook in the background.
{
"type": "command",
"command": "/path/to/run-tests.sh",
"async": true,
"timeout": 300
}
How async hooks work:
- Claude Code starts the hook process and immediately continues working
- The hook receives the same JSON input via stdin as a synchronous hook
- When the process finishes, if it produced
systemMessageoradditionalContextin its JSON output, that content is delivered to Claude on the next conversation turn - Async hooks CANNOT block or control behavior -- the triggering action has already completed
Limitations:
- Only
type: "command"hooks supportasync(not prompt or agent) - Async hooks cannot return decisions (no blocking, no permission control)
- Output is delivered on the next conversation turn, not immediately
- Each firing creates a separate background process (no deduplication)
/hooks Interactive Menu
Type /hooks in Claude Code to open the interactive hooks manager.
What you can do in the /hooks menu:
- View all configured hooks organized by event
- Add new hooks without editing JSON files
- Delete existing hooks
- Choose storage location (User, Project, Local)
- Toggle "disable all hooks" on/off
Each hook in the menu is labeled with its source:
[User]-- from~/.claude/settings.json[Project]-- from.claude/settings.json[Local]-- from.claude/settings.local.json[Plugin]-- from a plugin'shooks/hooks.json(read-only)
Step-by-step: Creating a hook with /hooks:
- Type
/hooksin the CLI - Select the event (e.g.,
Notification) - Set the matcher (e.g.,
*for all, orpermission_promptfor specific) - Select
+ Add new hook... - Enter the shell command
- Choose storage location (User settings, Project settings, etc.)
- Press
Escto return to the CLI
Complete Hook Examples
Example 1: Auto-Lint After File Edits
Run ESLint on every file Claude edits or writes. This is the most common hook pattern.
Settings JSON (.claude/settings.json):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx eslint --fix",
"timeout": 30,
"statusMessage": "Running ESLint..."
}
]
}
]
}
}
How it works:
- Claude edits or writes a file (PostToolUse fires with
EditorWrite) - The matcher
Edit|Writematches jqextracts the file path from the hook's JSON input- ESLint runs with
--fixto auto-correct what it can - Exit 0: file was linted. Any output is logged in verbose mode.
Example 2: Block Dangerous Commands
Prevent Claude from running destructive shell commands like rm -rf, DROP TABLE, or git push --force.
Script (.claude/hooks/block-dangerous.sh):
#!/bin/bash
# block-dangerous.sh -- PreToolUse hook for Bash commands
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Define blocked patterns
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"rm -rf \."
"DROP TABLE"
"DROP DATABASE"
"git push --force"
"git push -f"
"git reset --hard"
"mkfs"
"> /dev/sda"
"chmod -R 777"
":(){ :|:& };:"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "BLOCKED: Command matches dangerous pattern '$pattern'" >&2
echo "If you need to run this command, do it manually in your terminal." >&2
exit 2
fi
done
exit 0
Settings JSON:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}
Do not forget to make the script executable:
chmod +x .claude/hooks/block-dangerous.sh
Example 3: Auto-Format Code with Prettier
Format every file Claude touches using Prettier. Similar to lint, but focused purely on formatting.
Settings JSON:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true",
"timeout": 15,
"statusMessage": "Formatting with Prettier..."
}
]
}
]
}
}
The 2>/dev/null || true ensures the hook never fails if Prettier does not support the file type (e.g., binary files or unsupported extensions).
Example 4: Desktop Notification on Completion
Get a macOS notification whenever Claude finishes a task, so you can work on other things.
Settings JSON (~/.claude/settings.json -- user-level for all projects):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
Linux equivalent (using notify-send):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
}
]
}
]
}
}
Example 5: Validate SQL Queries (Read-Only Enforcement)
Prevent Claude from running any SQL command that modifies data. Allow SELECT only.
Script (.claude/hooks/validate-sql.sh):
#!/bin/bash
# validate-sql.sh -- Block non-SELECT SQL queries
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Check if this looks like a SQL command
if echo "$COMMAND" | grep -qiE '(psql|mysql|sqlite3|pg_|mongosh)'; then
# Extract the SQL part (after the connection command)
SQL_PART=$(echo "$COMMAND" | grep -oiE '(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE)')
if [ -n "$SQL_PART" ]; then
echo "BLOCKED: Only SELECT queries are allowed. Found: $SQL_PART" >&2
echo "This project enforces read-only database access for AI agents." >&2
exit 2
fi
fi
exit 0
Settings JSON:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/validate-sql.sh"
}
]
}
]
}
}
Example 6: Pre-Commit Quality Gate (Stop Hook)
Prevent Claude from stopping until all tests and lint checks pass. Uses exit code 2 to force continuation.
Script (.claude/hooks/verify-before-stop.sh):
#!/bin/bash
# verify-before-stop.sh -- Stop hook to enforce quality
INPUT=$(cat)
# Prevent infinite loops: if this hook already forced a continuation, allow the stop
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi
# Run tests
echo "Running test suite..." >&2
if ! npm test 2>&1 >/dev/null; then
echo "Tests are failing. Fix them before finishing." >&2
exit 2
fi
# Run linter
echo "Running linter..." >&2
if ! npm run lint 2>&1 >/dev/null; then
echo "Lint errors detected. Fix them before finishing." >&2
exit 2
fi
echo "All checks passed." >&2
exit 0
Settings JSON:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify-before-stop.sh",
"timeout": 120,
"statusMessage": "Running quality checks..."
}
]
}
]
}
}
Critical: Preventing infinite loops. Stop hooks fire when Claude tries to stop. If your hook blocks the stop (exit 2), Claude continues working and eventually tries to stop again, firing the hook again. You MUST check the stop_hook_active field and allow the stop if it is true. Otherwise Claude runs indefinitely.
Example 7: Block Edits to Protected Files
Prevent Claude from modifying sensitive files like .env, lock files, or .git/ contents.
Script (.claude/hooks/protect-files.sh):
#!/bin/bash
# protect-files.sh -- PreToolUse hook for Edit and Write
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(
".env"
".env.local"
".env.production"
"package-lock.json"
"yarn.lock"
"pnpm-lock.yaml"
".git/"
"*.key"
"*.pem"
"*.cert"
)
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "BLOCKED: '$FILE_PATH' is a protected file (matches pattern '$pattern')." >&2
echo "Protected files must be edited manually." >&2
exit 2
fi
done
exit 0
Settings JSON:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
Example 8: Re-Inject Context After Compaction
When Claude's context fills up and compacts, important details can be lost. This hook re-injects critical context on every compaction and session resume.
Settings JSON:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor. Branch naming: feature/TICKET-description.'"
}
]
},
{
"matcher": "resume",
"hooks": [
{
"type": "command",
"command": "echo \"Recent changes since last session:\" && git log --oneline -10"
}
]
}
]
}
}
For SessionStart hooks, anything written to stdout is added to Claude's context. You can also use CLAUDE_ENV_FILE to persist environment variables:
#!/bin/bash
# setup-env.sh -- SessionStart hook
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
fi
echo "Environment configured for development."
exit 0
Combining Hooks with Skills and Commands
The real power of Level 3 comes from combining all three mechanisms. Here is a complete workflow example:
Scenario: Boris Cherny's coding workflow -- write code, simplify it, format it, verify it, and ship a PR.
1. Skill: git-conventions (auto-loaded knowledge)
# .claude/skills/git-conventions/SKILL.md
---
name: git-conventions
description: Git commit and branching conventions for this project. Use when making commits, creating branches, or writing PR descriptions.
user-invocable: false
---
## Commit Messages
- Format: type(scope): description
- Types: feat, fix, refactor, test, docs, chore, perf
- Scope: the module or area affected
- Description: imperative mood, no period, under 72 chars
- Body: explain WHY, not WHAT
## Branch Naming
- feature/TICKET-short-description
- fix/TICKET-short-description
- chore/description
## PR Descriptions
- Summary: 1-3 bullet points of what changed
- Why: motivation for the change
- Testing: how it was tested
2. Command: /commit-push-pr (manual trigger)
# .claude/commands/commit-push-pr.md
---
name: commit-push-pr
description: Commit, push, and create a PR.
disable-model-invocation: true
---
## Context
Branch: !`git branch --show-current`
Changes: !`git diff --stat`
Recent commits: !`git log --oneline -5`
## Instructions
1. Stage all relevant changes
2. Write a commit message following our git conventions
3. Push to origin
4. Create a PR with a clear title and description
3. Hooks: Auto-format + quality gate
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true",
"timeout": 15
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/verify-before-stop.sh",
"timeout": 120
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
How it flows together:
- You write code with Claude (normal conversation)
- The
git-conventionsskill is auto-loaded because Claude detects it is relevant - Every file Claude writes or edits is auto-formatted by the PostToolUse hook (zero tokens)
- When Claude tries to stop, the Stop hook verifies tests and lint pass (zero tokens)
- If checks fail, Claude is forced to continue and fix the issues
- When everything passes, you invoke
/commit-push-pr - The command precomputes git status with
!command`` syntax - Claude writes a commit message following the auto-loaded git conventions
- The Notification hook alerts you when Claude needs input
Skills handle knowledge. Hooks handle automation. Commands handle user-initiated workflows. Together, they create a seamless pipeline.
5. How to Remember the Differences
+------------------+-------------------+-------------------+
| COMMANDS | SKILLS | HOOKS |
+------------------+-------------------+-------------------+
| What YOU trigger | How Claude THINKS | What HAPPENS |
| manually | (auto-loaded | AUTOMATICALLY |
| | knowledge) | |
+------------------+-------------------+-------------------+
| /commit-push-pr | api-conventions | PostToolUse: |
| /code-review | git-conventions | auto-format |
| /generate-tests | explain-code | PreToolUse: |
| /deploy | legacy-system | block rm -rf |
| | | Stop: |
| | | verify tests |
+------------------+-------------------+-------------------+
| Trigger: /name | Trigger: auto OR | Trigger: event |
| Tokens: full | /name | Tokens: zero |
| Can block: no | Tokens: full | (command type) |
| | Can block: no | Can block: YES |
+------------------+-------------------+-------------------+
Mnemonic: "Skills Think, Hooks Act, Commands Respond"
- Skills = Thinking (knowledge in Claude's brain)
- Hooks = Acting (deterministic automation)
- Commands = Responding (to your manual trigger)
6. Exercises
Exercise 1: Create Your First Command
Create a slash command for a task you do at least three times per week. Save it in .claude/commands/ and test it.
Acceptance criteria:
- The command file uses
$ARGUMENTSfor dynamic input - The command includes verification steps
- You can invoke it with
/your-command some-argumentand get useful output
Exercise 2: Create an Auto-Loaded Skill
Create a skill that teaches Claude your project's coding conventions. It should auto-load when Claude is writing code.
Acceptance criteria:
SKILL.mdhas a cleardescriptionthat matches common coding prompts- The skill includes at least one supporting file with detailed reference material
- The skill is NOT set to
disable-model-invocation(you want auto-loading) - Test by asking Claude to write some code and verify the conventions are followed
Exercise 3: Install a Community Skill
Browse SkillsMP and install a skill that matches your workflow.
Acceptance criteria:
- The skill appears in your
/autocomplete menu - You can invoke it and get useful output
- You understand what it does by reading its
SKILL.md
Exercise 4: Build an Auto-Format Hook
Create a PostToolUse hook that auto-formats code after every file edit using your project's formatter (Prettier, Black, gofmt, etc.).
Acceptance criteria:
- The hook fires only on
Edit|Writeevents (not every tool call) - The hook extracts the file path from stdin JSON
- The formatter runs successfully and the file is formatted
- The hook does not fail on unsupported file types
Exercise 5: Block Dangerous Commands
Create a PreToolUse hook that blocks at least five dangerous command patterns.
Acceptance criteria:
- The hook script reads JSON from stdin with
jq - It checks against a list of blocked patterns
- It uses exit code 2 to block with a helpful error message
- It exits 0 for safe commands
- Test by asking Claude to run a blocked command and verify it is prevented
Exercise 6: Notification Pipeline
Set up a complete notification system: desktop notification when Claude needs input, sound when Claude finishes, and a log file of all commands Claude runs.
Acceptance criteria:
Notificationhook sends a desktop notificationStophook plays a soundPostToolUsehook withBashmatcher appends commands to a log file- All hooks are in
~/.claude/settings.json(user-level, all projects)
Exercise 7: Build a Complete Workflow
Combine a command, a skill, and a hook into a single workflow. For example:
- Skill: your project's testing conventions (auto-loaded)
- Command:
/full-testthat runs the test suite with analysis - Hook: Stop hook that prevents Claude from finishing if tests fail
Acceptance criteria:
- The skill auto-loads when Claude is writing tests
- The command can be invoked manually for on-demand testing
- The hook prevents premature completion
- All three work together seamlessly
Exercise 8: Context Fork Skill
Create a skill with context: fork that runs research in an isolated subagent.
Acceptance criteria:
- The skill has
context: forkandagent: Explorein frontmatter - When invoked, it spawns a subagent that researches the codebase
- The results are summarized and returned to your main conversation
- Your main conversation context is not consumed by the research
Exercise 9: Dynamic Context Command
Create a command that uses !command`` syntax to precompute context before the prompt is sent to Claude.
Acceptance criteria:
- At least three
!command`` substitutions (e.g., git status, recent commits, current branch) - The command produces useful, grounded output because it has real data
- Compare the result to the same command without
!command`` and note the difference in quality
Exercise 10: Production Hook Suite
Build a complete .claude/hooks/ directory with scripts for your real project:
protect-files.sh-- Block edits to sensitive filesvalidate-bash.sh-- Block dangerous shell commandsauto-format.sh-- Format files after editsverify-quality.sh-- Run tests before Claude stops
Wire all four into .claude/settings.json and verify they work together.
7. Pro Tips from Boris Cherny
Boris Cherny is an engineer at Anthropic who works on Claude Code. His publicly documented workflow is one of the most productive Claude Code setups known.
On Commands
- He uses
/commit-push-pr"dozens of times every day." It is his single most-used command. - The command uses inline bash (
!command``) to precompute git status, branch name, and recent commits so Claude has grounded facts instead of guessing. - He does NOT amend commits. He prefers clean sequential commits for easier review.
On Skills and Subagents
- He uses few subagents regularly, focused on automating common workflows.
code-simplifierruns after implementation to remove dead code, simplify conditionals, and improve naming.verify-appruns the full verification pipeline (lint, type check, tests, build) before committing.- He preloads relevant skills into subagent context to give them domain knowledge.
On Hooks
- A
PostToolUsehook automatically formats any code Claude generates. This means every file is always formatted, with zero effort from Boris or Claude. - He uses notification hooks to know when Claude needs attention while running multiple instances.
On Workflow Integration
- He runs 5 parallel Claude Code terminal sessions plus 5-10 web Claude.ai sessions simultaneously.
- Each terminal gets a color-coded tab in Ghostty so he can identify instances at a glance.
- He uses
/permissionsto pre-authorize safe commands instead of--dangerously-skip-permissions. - Plan Mode is always the first step: "I will use plan mode. I'll go back and forth with Claude until I like its plan."
- Uses macOS dictation (Fn Fn) for faster prompting.
8. Anti-Patterns
Anti-Pattern 1: Monster Commands
Problem: A single command file that tries to do everything -- commit, test, deploy, notify, update docs.
Why it fails: If any step fails midway, you cannot retry just that step. The command is fragile and hard to debug.
Solution: Break it into focused commands (/commit, /test, /deploy) and use hooks for the automatic parts (formatting, notifications). Chain them manually or let hooks handle the automation.
Anti-Pattern 2: Skills with No Description
Problem: A skill with an empty or vague description field.
Why it fails: Claude cannot auto-load the skill because it does not know when it is relevant. The skill sits unused unless you manually invoke it.
Solution: Write descriptions that include keywords users would naturally say. Instead of "Helpful utility," write "API design patterns and RESTful conventions for this codebase. Use when writing, reviewing, or modifying API endpoints."
Anti-Pattern 3: Stop Hook Without Loop Guard
Problem: A Stop hook that blocks Claude's stop but does not check stop_hook_active.
Why it fails: Claude is blocked from stopping, continues working, tries to stop again, is blocked again -- infinite loop. Tokens burn until timeout.
Solution: Always check stop_hook_active at the top of your Stop hook script:
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
Anti-Pattern 4: Using Skills Where Hooks Should Be
Problem: Creating a skill that says "Always format code with Prettier after writing a file."
Why it fails: Skills are LLM knowledge -- Claude has to decide to follow the instruction. It might forget, skip it, or do it inconsistently. Formatting is deterministic and should always happen.
Solution: Use a hook. PostToolUse with Edit|Write matcher runs Prettier every time, guaranteed, with zero tokens.
Anti-Pattern 5: Using Hooks Where Skills Should Be
Problem: Trying to encode complex coding standards as shell scripts in hooks.
Why it fails: Hooks are for deterministic actions (run a linter, block a command, send a notification). They cannot make nuanced judgments about code quality.
Solution: Use a skill for coding standards and conventions. Use hooks for the mechanical enforcement (running the linter, checking for banned patterns).
Anti-Pattern 6: Noisy Hooks
Problem: A hook that fires on every PostToolUse event (no matcher) and does heavy work.
Why it fails: It runs after every single tool call -- reads, greps, globs, everything. Most runs are wasted, and the hook slows down Claude significantly.
Solution: Use specific matchers. Format only after Edit|Write. Log only after Bash. Validate only on PreToolUse for Bash.
Anti-Pattern 7: Hardcoded Paths in Shared Hooks
Problem: A hook script with paths like /Users/boris/scripts/lint.sh committed to the team repo.
Why it fails: Other team members have different usernames and directory structures.
Solution: Use $CLAUDE_PROJECT_DIR for project-relative paths:
{
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh"
}
Anti-Pattern 8: Skipping the Executable Bit
Problem: Creating a hook script but forgetting chmod +x.
Why it fails: Claude Code cannot run the script. The hook silently fails or errors out.
Solution: Always run chmod +x on your hook scripts:
chmod +x .claude/hooks/block-dangerous.sh
chmod +x .claude/hooks/auto-format.sh
chmod +x .claude/hooks/verify-quality.sh
9. Official Documentation Links
Primary References
- Skills -- Full skills and commands reference (SKILL.md format, frontmatter, invocation control, dynamic context, subagent execution, marketplace)
- Hooks Reference -- Complete hook events, JSON schemas, exit codes, async hooks, prompt/agent hooks, MCP tool hooks
- Hooks Guide -- Quickstart with practical examples (notifications, formatting, blocking, compaction context)
Related Documentation
- Sub-Agents -- Creating specialized agents that skills can delegate to
- Plugins -- Packaging skills, hooks, and agents for distribution
- Memory -- CLAUDE.md files for persistent context
- Interactive Mode -- Built-in commands and shortcuts
- Permissions -- Controlling tool and skill access
- Settings -- Settings file locations and precedence
Community Resources
- Agent Skills Standard -- The open standard that Claude Code skills follow
- SkillsMP Marketplace -- 200K+ community skills organized by category
- SkillsMP Categories -- Browse skills by workflow type
- How Boris Uses Claude Code -- Boris Cherny's full workflow documentation
- Bash Command Validator Example -- Official reference implementation for Bash validation hooks
Debugging
- Run
claude --debugto see hook execution details (which hooks matched, exit codes, output) - Toggle verbose mode with
Ctrl+Oto see hook progress in the transcript - Use
/hooksto view, add, and delete hooks interactively - Use
/contextto check if skill descriptions are being excluded due to budget limits
Last Updated: 2026-02-20 Compiled from official Anthropic documentation, Boris Cherny's public workflow, and community best practices