The definitive guide to extending Claude Code with reusable prompts, auto-loaded knowledge, and deterministic automation -- from basic slash
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.
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.
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:
.claude/commands/ directory/ autocomplete menuImportant 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.
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
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
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.
Naming rules:
fix-issue.md creates /fix-issue)commands/frontend/component.md creates /frontend:component)File format:
@path/to/file syntax!shell-command`` for dynamic context injectionThe $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>.
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.
| 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).
/ in the Claude Code CLI/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.
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"
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
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
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
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:
Save the analysis to ./output/youtube-analysis-<sanitized-title>.md
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
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.
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-hint frontmatter field shows users what input the command expects during autocomplete. This prevents misuse.
Reference project conventions. Use @CLAUDE.md or @.claude/rules/git-conventions.md to pull in project rules rather than duplicating them in the command.
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/.
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.
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:
/name entry in the slash menuEach 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.
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. |
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:
description: "Explains code with visual diagrams and analogies" loads when a user says "how does this work?" or "explain this code"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 |
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:
SKILL.md under 500 linesSKILL.md so 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 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:
.claude/skills/ to version controlskills/ directory in your Claude Code pluginAdd 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:
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.).claude/agents/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:
!command`` executes immediately (before Claude processes anything)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`
| 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
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": []
}
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).
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.
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).
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.
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:
**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.
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) |
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.
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 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 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 |
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..."
}
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 userhookSpecificOutput with decision.behavior (PermissionRequest only):
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": {
"command": "npm run lint"
}
}
}
}
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:
systemMessage or additionalContext in its JSON output, that content is delivered to Claude on the next conversation turnLimitations:
type: "command" hooks support async (not prompt or agent)Type /hooks in Claude Code to open the interactive hooks manager.
What you can do in the /hooks menu:
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's hooks/hooks.json (read-only)Step-by-step: Creating a hook with /hooks:
/hooks in the CLINotification)* for all, or permission_prompt for specific)+ Add new hook...Esc to return to the CLIRun 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:
Edit or Write)Edit|Write matchesjq extracts the file path from the hook's JSON input--fix to auto-correct what it canPrevent 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
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).
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'"
}
]
}
]
}
}
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"
}
]
}
]
}
}
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.
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"
}
]
}
]
}
}
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
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:
git-conventions skill is auto-loaded because Claude detects it is relevant/commit-push-pr!command`` syntaxSkills handle knowledge. Hooks handle automation. Commands handle user-initiated workflows. Together, they create a seamless pipeline.
+------------------+-------------------+-------------------+
| 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"
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:
$ARGUMENTS for dynamic input/your-command some-argument and get useful outputCreate a skill that teaches Claude your project's coding conventions. It should auto-load when Claude is writing code.
Acceptance criteria:
SKILL.md has a clear description that matches common coding promptsdisable-model-invocation (you want auto-loading)Browse SkillsMP and install a skill that matches your workflow.
Acceptance criteria:
/ autocomplete menuSKILL.mdCreate a PostToolUse hook that auto-formats code after every file edit using your project's formatter (Prettier, Black, gofmt, etc.).
Acceptance criteria:
Edit|Write events (not every tool call)Create a PreToolUse hook that blocks at least five dangerous command patterns.
Acceptance criteria:
jqSet 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:
Notification hook sends a desktop notificationStop hook plays a soundPostToolUse hook with Bash matcher appends commands to a log file~/.claude/settings.json (user-level, all projects)Combine a command, a skill, and a hook into a single workflow. For example:
/full-test that runs the test suite with analysisAcceptance criteria:
Create a skill with context: fork that runs research in an isolated subagent.
Acceptance criteria:
context: fork and agent: Explore in frontmatterCreate a command that uses !command`` syntax to precompute context before the prompt is sent to Claude.
Acceptance criteria:
!command`` substitutions (e.g., git status, recent commits, current branch)!command`` and note the difference in qualityBuild 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 stopsWire all four into .claude/settings.json and verify they work together.
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.
/commit-push-pr "dozens of times every day." It is his single most-used command.!command``) to precompute git status, branch name, and recent commits so Claude has grounded facts instead of guessing.code-simplifier runs after implementation to remove dead code, simplify conditionals, and improve naming.verify-app runs the full verification pipeline (lint, type check, tests, build) before committing.PostToolUse hook automatically formats any code Claude generates. This means every file is always formatted, with zero effort from Boris or Claude./permissions to pre-authorize safe commands instead of --dangerously-skip-permissions.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.
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."
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
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.
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).
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.
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"
}
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
claude --debug to see hook execution details (which hooks matched, exit codes, output)Ctrl+O to see hook progress in the transcript/hooks to view, add, and delete hooks interactively/context to check if skill descriptions are being excluded due to budget limitsLast Updated: 2026-02-20 Compiled from official Anthropic documentation, Boris Cherny's public workflow, and community best practices
New techniques, real-world patterns, and Claude updates โ delivered as the guides evolve.