Claude Code Mastery
13 partsTL;DR
Hooks give you deterministic control over Claude Code. Auto-format on save, block dangerous commands, run tests before commits, fire desktop notifications. Here's how to set them up.
You can tell Claude Code "always run Prettier after editing files" in your CLAUDE.md. It will probably listen. Probably. But CLAUDE.md instructions are suggestions the model can choose to ignore. Hooks are not suggestions. They are shell commands that execute every single time, at exact points in Claude Code's lifecycle.
Think of hooks like git hooks, but for your AI coding agent. Before a tool runs, after a file gets edited, when the agent finishes responding, when a session starts. You define what happens at each point, and it happens deterministically. No forgetting. No deciding it's unnecessary this time.
For anyone running Claude Code on production codebases, the distinction between "probably follows the rule" and "always follows the rule" is everything.
Hooks are shell commands, LLM prompts, or sub-agents that Claude Code executes at specific lifecycle events. You configure them in JSON settings files, and they run automatically with zero manual intervention.
Every hook has three core parts:
PostToolUse, PreToolUse, Stop)Write, Edit|Write, Bash)Here's the simplest possible hook. It runs Prettier every time Claude writes a file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $(cat | jq -r '.tool_input.file_path')"
}
]
}
]
}
}
That's it. Every Write operation now auto-formats. No reminders needed.
Every hook has a type field that determines how it executes. Claude Code supports three types, which is more than any competing tool.
The most common type. Runs a shell command as a child process. The command receives JSON context on stdin with the session ID, tool name, tool input, and working directory.
{
"type": "command",
"command": "npx prettier --write $(cat | jq -r '.tool_input.file_path')"
}
Use these for: auto-formatting, logging, notifications, file operations, blocking dangerous commands.
Sends a text prompt to a fast Claude model (Haiku by default) for single-turn evaluation. The $ARGUMENTS placeholder injects the hook's input JSON. No custom scripts needed.
{
"type": "prompt",
"prompt": "Analyze this context: $ARGUMENTS. Are all tasks complete and were tests run? Respond with {\"decision\": \"approve\"} or {\"decision\": \"block\", \"reason\": \"explanation\"}."
}
Use these for: context-aware decisions, task verification, intelligent filtering. This is unique to Claude Code. No other AI coding tool lets you delegate hook decisions to an LLM without writing custom code.
Spawns a sub-agent with access to tools like Read, Grep, and Glob for multi-turn codebase verification. The heaviest handler type, but the most powerful.
Use these for: deep validation like confirming all modified files have test coverage, or checking that an API change updated all consumers.
Claude Code exposes lifecycle events that cover every stage of the agent's execution. Here are the ones you'll use most, plus the full list.
| Event | When It Fires | Use It For |
|---|---|---|
PreToolUse | Before Claude runs any tool | Block dangerous commands, protect files, validate inputs |
PostToolUse | After Claude runs any tool | Auto-format code, stage files, run linters, log actions |
Stop | When Claude finishes responding | Run tests, verify task completion, quality checks |
Notification | When Claude needs user attention | Desktop alerts, Slack messages, sound effects |
| Event | When It Fires |
|---|---|
PreToolUse | Before any tool execution |
PostToolUse | After any tool execution |
PostToolUseFailure | After a tool execution fails |
Notification | When Claude sends an alert |
PermissionRequest | When a permission dialog would appear |
Stop | When Claude finishes its response |
SubagentStop | When a sub-agent finishes |
SubagentStart | When a sub-agent spawns |
PreCompact | Before context compaction |
PostCompact | After context compaction |
SessionStart | When a new session begins |
SessionEnd | When a session ends |
UserPromptSubmit | When you submit a prompt |
TaskCompleted | When a task completes |
Setup | During initialization |
PreToolUse, PostToolUse, Notification, and Stop handle 90% of real-world use cases.
Matchers filter which tools trigger a hook. They're regex strings matched against tool names.
| Matcher | What It Matches |
|---|---|
"Bash" | Shell commands only |
"Edit" | File edits only |
"Write" | File creation only |
"Edit|Write" | Any file modification |
"Bash|Edit|Write" | Most common operations |
"mcp__.*" | All MCP server tools |
"mcp__github__.*" | GitHub MCP tools only |
| Not specified | Everything |
Tool names are case-sensitive. "Bash" works. "bash" does not. "Edit" works. "edit" does not.
For Bash tool matchers, you can also match command arguments: "Bash(npm test.*)" matches any bash command starting with npm test.
Hooks are configured in JSON settings files at four levels:
| Scope | File Path | Use Case |
|---|---|---|
| Project | .claude/settings.json | Team-shared hooks, committed to git |
| Project local | .claude/settings.local.json | Personal project overrides, gitignored |
| User | ~/.claude/settings.json | Global hooks across all projects |
| Enterprise | Managed policy | Organization-wide enforcement |
Project-level hooks are the most common. Commit them to git so your whole team gets the same automation.
One important security detail: Claude Code snapshots your hook configuration at startup and uses that snapshot for the entire session. Edits mid-session have no effect. This prevents any modification of hooks while the agent is running.
The highest-value hook for most projects. Run your formatter every time Claude edits or creates a file.
Prettier (JavaScript/TypeScript):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && npx prettier --write \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
Go:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [[ \"$FILE\" == *.go ]] && gofmt -w \"$FILE\" || true"
}
]
}
]
}
}
Python (Black):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [[ \"$FILE\" == *.py ]] && python -m black \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
The 2>/dev/null || true at the end is important. It prevents the hook from failing on files the formatter doesn't support.
Prevent Claude from running destructive shell commands, even in autonomous mode.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(cat | jq -r '.tool_input.command // empty') && if echo \"$CMD\" | grep -qEi '(rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE|mkfs\\.|:\\(\\)\\{|chmod\\s+-R\\s+777\\s+/|dd\\s+if=.*of=/dev/)'; then echo \"BLOCKED: Dangerous command detected\" >&2; exit 2; fi"
}
]
}
]
}
}
Exit code 2 is the key. It tells Claude Code to block the operation and feed the stderr message back to Claude as an error. Claude sees the message, understands why the operation was blocked, and adjusts its approach.
Blocked patterns include:
rm -rf / (recursive delete from root)DROP TABLE / DROP DATABASE (SQL destruction)mkfs. (format filesystem)chmod -R 777 / (recursive permission change on root)dd if=... of=/dev/ (raw disk writes)Block Claude from touching files that should never be AI-modified.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && if echo \"$FILE\" | grep -qE '(\\.env|\\.lock|secrets\\.yaml|credentials|id_rsa|\\.pem)'; then echo \"BLOCKED: Cannot modify protected file: $FILE\" >&2; exit 2; fi"
}
]
}
]
}
}
Customize the grep pattern for your project. Add migration files, CI configs, or anything else that shouldn't change without human review.
Get notified when Claude needs your attention or finishes a long task. Essential if you multitask while Claude works.
macOS:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
Linux:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude needs your attention'"
}
]
}
]
}
}
Put this in ~/.claude/settings.json so it works across all projects.
Force Claude to verify its own work before it finishes. This is the hook that changed how I use Claude Code.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npm test 2>&1 || (echo 'Tests are failing. Please fix before finishing.' >&2; exit 2)"
}
]
}
]
}
}
If tests fail, the Stop hook returns exit code 2, which forces Claude to continue working. Claude sees the test output and attempts to fix the failures. This creates an automatic test-fix loop.
For a smarter version, use a prompt hook that evaluates whether the task is actually complete:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Analyze this context: $ARGUMENTS. Were all requested tasks completed? Were tests run and passing? If not, respond with {\"decision\": \"block\", \"reason\": \"explanation\"}. If everything looks good, respond with {\"decision\": \"approve\"}."
}
]
}
]
}
}
Automatically stage every file Claude modifies, so changes are always ready to commit.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [ -f \"$FILE\" ] && git add \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
Pair this with a solid .gitignore. You do not want to accidentally stage build artifacts or node_modules.
Load project-specific context automatically when Claude Code starts.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"Current branch: $(git branch --show-current). Last 3 commits: $(git log --oneline -3). Open issues: $(gh issue list --limit 5 --json title -q '.[].title' 2>/dev/null || echo 'N/A')\""
}
]
}
]
}
}
The stdout from SessionStart hooks gets injected as context for Claude. Every session starts with awareness of your current branch, recent commits, and open issues. No more explaining where you left off.
Run ESLint with auto-fix on JavaScript/TypeScript files after every edit.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && [[ \"$FILE\" =~ \\.(js|ts|jsx|tsx)$ ]] && npx eslint --fix \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
The regex check prevents ESLint from running on files it can't handle.
Get the weekly deep dive
Tutorials on Claude Code, AI agents, and dev tools - delivered free every week.
Hooks receive JSON on stdin with context about the current event. The structure varies by event type.
Base fields (all events):
{
"session_id": "abc123",
"transcript_path": "/Users/you/.claude/projects/my-project/conversation.jsonl",
"cwd": "/Users/you/my-project",
"hook_event_name": "PostToolUse"
}
Tool events add:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/Users/you/my-project/src/index.ts",
"old_string": "...",
"new_string": "..."
}
}
PostToolUse also includes tool_response with the result.
Use jq to extract specific fields in your hook commands:
# Get the file path
cat | jq -r '.tool_input.file_path'
# Get the bash command
cat | jq -r '.tool_input.command'
# Get the tool name
cat | jq -r '.tool_name'
For PreToolUse hooks, exit codes control flow:
| Exit Code | Effect |
|---|---|
0 | Allow the operation |
2 | Block the operation. stderr is sent to Claude as feedback |
| Other | Hook error. Operation proceeds, error is logged |
For PostToolUse hooks, the operation already happened, so the exit code doesn't block anything. But stderr output still gets sent to Claude as context.
PreToolUse hooks can return structured JSON on stdout for fine-grained control:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow"
}
}
Valid values for permissionDecision:
"allow" - skip the permission prompt, auto-approve"deny" - block the operation (same as exit code 2)"ask" - show the normal permission prompt to the userThis is useful for auto-allowing safe operations while still prompting for anything risky.
Two ways to configure hooks.
Type /hooks in Claude Code. Choose the event, add a new hook, set your matcher, enter the command, save. Claude Code updates your settings file and reloads the configuration. This is the easiest way to get started.
Open .claude/settings.json in your project (or ~/.claude/settings.json for global hooks) and add the hooks configuration directly.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "another-command-here"
}
]
}
]
}
}
Restart Claude Code or use /hooks to reload after manual edits.
Every hook adds latency. A 200ms formatter is fine. A 30-second test suite on every file edit is not. Save heavy operations for Stop hooks, not PostToolUse.
|| true to prevent cascading failuresIf your hook command fails on certain files (like running Prettier on a binary), the error can confuse Claude. Append || true to commands that might fail on edge cases.
Auto-formatting on every PostToolUse works, but each format change triggers a system reminder to Claude about the file modification. This eats into your context window. For large projects, a better pattern is formatting on Stop or through a git pre-commit hook rather than on every individual edit.
Ask Claude to write a test file and verify your hook triggers. Check Claude Code's transcript (Ctrl+O) for error messages if a hook doesn't seem to work.
Claude Code snapshots hook configuration at startup. If you edit your settings.json while a session is running, the changes won't take effect until you start a new session.
"Bash" matches. "bash" does not. Tool names are PascalCase: Bash, Edit, Write, Read, Glob, Grep.
Only exit code 2 blocks a PreToolUse operation. Exit code 1 or any other non-zero code is treated as a hook error and logged, but the operation still proceeds.
If you have three PostToolUse hooks with the same matcher, all three run simultaneously. They don't run sequentially.
Hooks have a default timeout of 60 seconds. For commands that might take longer, set the timeout field explicitly (in milliseconds):
{
"type": "command",
"command": "npm test",
"timeout": 120000
}
Your hook command receives JSON on stdin. If your command doesn't read stdin (like a simple echo), that's fine. But if it reads stdin and then hangs waiting for more input, the hook will timeout. Always consume stdin completely or ignore it.
Here's a production-ready configuration that combines multiple hooks into a cohesive workflow:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo \"Branch: $(git branch --show-current). Last commit: $(git log --oneline -1). Node: $(node -v)\""
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(cat | jq -r '.tool_input.command // empty') && if echo \"$CMD\" | grep -qEi '(rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE)'; then echo \"BLOCKED: Dangerous command\" >&2; exit 2; fi"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && if echo \"$FILE\" | grep -qE '(\\.env|\\.lock)'; then echo \"BLOCKED: Protected file\" >&2; exit 2; fi"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$FILE\" ] && npx prettier --write \"$FILE\" 2>/dev/null || true"
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs your attention\" with title \"Claude Code\"'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npm test 2>&1 | tail -20"
}
]
}
]
}
}
This gives you: project context on start, dangerous command blocking, sensitive file protection, auto-formatting, desktop notifications, and test output on completion. Six hooks covering the full development lifecycle.
| CLAUDE.md | Hooks | |
|---|---|---|
| Enforcement | Probabilistic (model may ignore) | Deterministic (always runs) |
| Speed | Zero overhead | Adds latency per hook |
| Flexibility | Natural language, very flexible | Structured, requires JSON config |
| Blocking | Cannot block operations | Can block with exit code 2 |
| Best for | Coding style, conventions, preferences | Safety, formatting, verification |
Use both. CLAUDE.md for soft guidance ("prefer named exports"). Hooks for hard requirements ("never touch .env files").
How do I set up my first hook?
Type /hooks in Claude Code. Choose an event, set a matcher, enter a command. Or edit .claude/settings.json directly.
Can hooks modify tool inputs before execution?
Yes. PreToolUse hooks can return an updatedInput field in their JSON output to modify tool arguments before execution. Useful for path correction or secret redaction.
Do hooks work in headless mode (claude -p)?
Yes. Hooks fire in both interactive and headless mode.
What happens if a hook times out? The hook is killed and treated as a non-blocking error. The operation proceeds normally.
Can I use hooks to auto-approve permissions?
Yes. A PreToolUse hook returning {"hookSpecificOutput": {"permissionDecision": "allow"}} on stdout will skip the permission prompt. This is a safer alternative to --dangerously-skip-permissions because you control exactly which operations get auto-approved.
How do I debug hooks that aren't working? Press Ctrl+O in Claude Code to open the transcript. Hook errors and output appear there. Common issues: wrong case in matcher names, commands not found in PATH, and syntax errors in the JSON config.
Can I have multiple hooks for the same event? Yes. Multiple hook entries under the same event run in parallel. Multiple matchers for the same event each fire independently when their pattern matches.
Are there community hook collections?
Yes. The disler/claude-code-hooks-mastery repo on GitHub has configurations for all events including security validation and observability. The lasso-security/claude-hooks repo focuses on prompt injection defense.
How do hooks compare to Cursor's hook system? Cursor added hooks in v1.7 with 6 events and command-only handlers. Claude Code has 15 events and three handler types (command, prompt, agent). The prompt and agent hook types are unique to Claude Code.
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.
Anthropic's agentic coding CLI. Runs in your terminal, edits files autonomously, spawns sub-agents, and maintains memory...
View ToolHigh-performance code editor built in Rust with native AI integration. Sub-millisecond input latency. Built-in assistant...
View ToolNew tutorials, open-source projects, and deep dives on coding agents - delivered weekly.
AI-native code editor forked from VS Code. Composer mode rewrites multiple files at once. Tab autocomplete predicts your...
Configure Claude Code for maximum productivity -- CLAUDE.md, sub-agents, MCP servers, and autonomous workflows.
AI AgentsInstall Claude Code, configure your first project, and start shipping code with AI in under 5 minutes.
Getting StartedWhat MCP servers are, how they work, and how to build your own in 5 minutes.
AI Agents
Anthropic has released Channels for Claude Code, enabling external events (CI alerts, production errors, PR comments, Discord/Telegram messages, webhooks, cron jobs, logs, and monitoring signals) to b

Claude Code “Loop” Scheduling: Recurring AI Tasks in Your Session The script explains Claude Code’s new “Loop” feature (an evolution of the Ralph Wiggins technique) for running recurring prompts that

Anthropic's Big Claude Code & Cowork Update: Remote Control, Scheduled Tasks, Plugins, Auto Memory + New Simplify/Batch Skills The script recaps a consolidated update on new Anthropic releases across

Complete pricing breakdown for every major AI coding tool. Claude Code, Cursor, Copilot, Windsurf, Codex, Augment, and m...

A practical guide to using Claude Code in Next.js projects. CLAUDE.md config for App Router, common workflows, sub-agent...
Three tools, three architectures. Terminal agent, IDE agent, cloud agent. Here is how to decide which one fits your work...