← Back to learning path
Level 3

Level 3: Slash Commands, Skills & Hooks

The definitive guide to extending Claude Code with reusable prompts, auto-loaded knowledge, and deterministic automation -- from basic…

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

  1. Overview: Three Ways to Extend Claude Code
  2. Part A: Slash Commands (Manual Triggers)
  3. Part B: Skills (Auto-Invoked Knowledge)
  4. Part C: Hooks (Automatic Triggers)
  5. How to Remember the Differences
  6. Exercises
  7. Pro Tips from Boris Cherny
  8. Anti-Patterns
  9. 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:

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:

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:

File format:

$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

  1. Type / in the Claude Code CLI
  2. Start typing the command name -- autocomplete kicks in
  3. Select the command
  4. Type any arguments after the command name
  5. 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"
  1. Read the downloaded .vtt or .srt file

  2. 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?)
  3. Save the analysis to ./output/youtube-analysis-<sanitized-title>.md

  4. Clean 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

  1. Use disable-model-invocation: true for any command with side effects (committing, deploying, posting). You do not want Claude deciding on its own to run your deploy command.

  2. 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.

  3. 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.

  4. 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.

  5. Add argument hints. The argument-hint frontmatter field shows users what input the command expects during autocomplete. This prevents misuse.

  6. Reference project conventions. Use @CLAUDE.md or @.claude/rules/git-conventions.md to pull in project rules rather than duplicating them in the command.

  7. Put personal commands at user level. Commands like /linkedin-post or /meeting-notes that have nothing to do with a specific project belong in ~/.claude/commands/.

  8. Put team commands at project level. Commands like /deploy or /code-review that 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:

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:

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:

## 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:

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:

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:

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:

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:

  1. Each !command`` executes immediately (before Claude processes anything)
  2. The output replaces the placeholder in the skill content
  3. 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

Naming Conventions

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


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:
  1. Hook event (e.g., "PreToolUse")
    1. Matcher group (e.g., match only "Bash" tools)
      1. Hook handler(s) (the shell command or prompt to run)

**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:

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:

  1. Claude Code starts the hook process and immediately continues working
  2. The hook receives the same JSON input via stdin as a synchronous hook
  3. When the process finishes, if it produced systemMessage or additionalContext in its JSON output, that content is delivered to Claude on the next conversation turn
  4. Async hooks CANNOT block or control behavior -- the triggering action has already completed

Limitations:

/hooks Interactive Menu

Type /hooks in Claude Code to open the interactive hooks manager.

What you can do in the /hooks menu:

  1. View all configured hooks organized by event
  2. Add new hooks without editing JSON files
  3. Delete existing hooks
  4. Choose storage location (User, Project, Local)
  5. Toggle "disable all hooks" on/off

Each hook in the menu is labeled with its source:

Step-by-step: Creating a hook with /hooks:

  1. Type /hooks in the CLI
  2. Select the event (e.g., Notification)
  3. Set the matcher (e.g., * for all, or permission_prompt for specific)
  4. Select + Add new hook...
  5. Enter the shell command
  6. Choose storage location (User settings, Project settings, etc.)
  7. Press Esc to 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:

  1. Claude edits or writes a file (PostToolUse fires with Edit or Write)
  2. The matcher Edit|Write matches
  3. jq extracts the file path from the hook's JSON input
  4. ESLint runs with --fix to auto-correct what it can
  5. 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:

  1. You write code with Claude (normal conversation)
  2. The git-conventions skill is auto-loaded because Claude detects it is relevant
  3. Every file Claude writes or edits is auto-formatted by the PostToolUse hook (zero tokens)
  4. When Claude tries to stop, the Stop hook verifies tests and lint pass (zero tokens)
  5. If checks fail, Claude is forced to continue and fix the issues
  6. When everything passes, you invoke /commit-push-pr
  7. The command precomputes git status with !command`` syntax
  8. Claude writes a commit message following the auto-loaded git conventions
  9. 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"


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:

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:

Exercise 3: Install a Community Skill

Browse SkillsMP and install a skill that matches your workflow.

Acceptance criteria:

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:

Exercise 5: Block Dangerous Commands

Create a PreToolUse hook that blocks at least five dangerous command patterns.

Acceptance criteria:

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:

Exercise 7: Build a Complete Workflow

Combine a command, a skill, and a hook into a single workflow. For example:

Acceptance criteria:

Exercise 8: Context Fork Skill

Create a skill with context: fork that runs research in an isolated subagent.

Acceptance criteria:

Exercise 9: Dynamic Context Command

Create a command that uses !command`` syntax to precompute context before the prompt is sent to Claude.

Acceptance criteria:

Exercise 10: Production Hook Suite

Build a complete .claude/hooks/ directory with scripts for your real project:

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

On Skills and Subagents

On Hooks

On Workflow Integration


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

Related Documentation

Community Resources

Debugging


Last Updated: 2026-02-20 Compiled from official Anthropic documentation, Boris Cherny's public workflow, and community best practices