Claude Code Essential Commands Created: 13 Apr 2026 Updated: 13 Apr 2026

Claude Code Hooks — Automate Workflows with Deterministic Control

When you work with Claude Code, you sometimes need guaranteed behavior — formatting every edited file, blocking dangerous commands, or sending a notification whenever Claude waits for input. Relying on the LLM to remember these tasks is unreliable. Hooks solve this by letting you attach your own shell commands, HTTP calls, LLM prompts, or verification agents to specific lifecycle events inside Claude Code.

Think of hooks like event listeners in a web application: you register a handler for an event (e.g., PostToolUse), and Claude Code calls your handler every time that event fires — no prompting required.

What You Will Learn

  1. What hooks are and why they matter.
  2. The hook lifecycle — all 26 events you can listen to.
  3. Where to configure hooks (settings files, scopes).
  4. The four hook types: command, HTTP, prompt, agent.
  5. How hooks communicate: stdin, stdout, exit codes, and JSON.
  6. Practical, copy-paste examples for the most common use-cases.
  7. Filtering with matchers and the if field.
  8. Troubleshooting and limitations.

What Are Hooks?

Definition

Hooks are user-defined actions that execute automatically at specific points in Claude Code's lifecycle. They provide deterministic control: certain actions always happen rather than relying on the LLM to choose to run them.

Why Hooks Matter

Without hooks, enforcing project rules or automating repetitive tasks depends on Claude remembering instructions in its context window. As context grows and compactions occur, instructions can get lost. Hooks guarantee execution:

  1. Consistency — auto-format every file edit, every time.
  2. Safety — block dangerous commands before they execute.
  3. Productivity — get notified when Claude needs input instead of watching the terminal.
  4. Compliance — audit every configuration change or tool call.
  5. Integration — connect Claude Code with external services, linters, and CI pipelines.

Four Hook Types

Claude Code supports four hook types, each suited to different scenarios:

  1. command — runs a shell command. Input arrives on stdin as JSON; output goes to stdout/stderr.
  2. http — POSTs event data to a URL. The response body uses the same JSON format as command hooks.
  3. prompt — sends a single-turn prompt to a Claude model for a yes/no judgment call.
  4. agent — spawns a subagent that can read files, run commands, and make multi-turn decisions.

Hook Lifecycle — The 26 Events

Overview

Every hook is attached to an event. When that event fires, all matching hooks run in parallel. Below is the complete list grouped by category:

Session-Level Events

  1. SessionStart — when a session begins, resumes, or compacts. Supports command hooks only.
  2. SessionEnd — when a session terminates (clear, resume, logout, etc.).
  3. InstructionsLoaded — when a CLAUDE.md or .claude/rules/*.md file is loaded into context.
  4. ConfigChange — when a configuration file changes during a session.

Turn-Level Events

  1. UserPromptSubmit — when you submit a prompt, before Claude processes it.
  2. Stop — when Claude finishes responding.
  3. StopFailure — when a turn ends due to an API error.
  4. Notification — when Claude Code sends a notification (permission prompt, idle, etc.).

Tool-Level Events

  1. PreToolUse — before a tool call executes. Can block it.
  2. PostToolUse — after a tool call succeeds.
  3. PostToolUseFailure — after a tool call fails.
  4. PermissionRequest — when a permission dialog would appear.
  5. PermissionDenied — when a tool call is denied.

Subagent & Task Events

  1. SubagentStart / SubagentStop — when a subagent is spawned or finishes.
  2. TaskCreated / TaskCompleted — when a task is created or completed.
  3. TeammateIdle — when an agent team teammate is about to go idle.

Environment & File Events

  1. CwdChanged — when Claude changes the working directory.
  2. FileChanged — when a watched file changes on disk.
  3. WorktreeCreate / WorktreeRemove — when a git worktree is created or removed.

Compaction & Elicitation Events

  1. PreCompact / PostCompact — before and after context compaction.
  2. Elicitation / ElicitationResult — when an MCP server requests user input or receives a response.

Where to Configure Hooks

Settings File Locations

Where you add a hook determines its scope. Claude Code checks multiple settings files, each serving a different audience:

FileScopeShared with Team?
~/.claude/settings.jsonAll your projects (user-level)No — local to your machine
.claude/settings.jsonSingle projectYes — commit to the repo
.claude/settings.local.jsonSingle projectNo — gitignored
Managed policy settingsOrganization-wideYes — admin-controlled
Plugin hooks/hooks.jsonWhen plugin is enabledYes — bundled with the plugin
Skill or agent frontmatterWhile skill/agent is activeYes — defined in the component file

Configuration Structure — Three Nesting Levels

A hook configuration has three levels: event → matcher group → hook handler(s). Here is the JSON skeleton:

{
"hooks": {
"EventName": [
{
"matcher": "pattern",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
]
}
}
  1. Event name — the top-level key inside "hooks" (e.g., PreToolUse, Notification).
  2. Matcher group — an array entry with a "matcher" that filters when the group fires.
  3. Hook handlers — the "hooks" array inside each group, containing one or more actions to execute.

Multiple events live as sibling keys inside the single "hooks" object. If your settings file already has a "hooks" key, add new event names next to the existing ones rather than replacing the whole object.

Filtering with Matchers

How Matchers Work

Without a matcher, a hook fires on every occurrence of its event. A matcher narrows the trigger. Each event type matches on a specific field:

Event(s)Matches onExample
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDeniedTool nameBash, Edit|Write, mcp__.*
SessionStartHow the session startedstartup, resume, compact
SessionEndWhy the session endedclear, logout
NotificationNotification typepermission_prompt, idle_prompt
ConfigChangeConfiguration sourceuser_settings, project_settings
FileChangedLiteral filenames to watch.envrc|.env
UserPromptSubmit, Stop, CwdChanged, etc.No matcher supportAlways fires on every occurrence

Matcher Pattern Rules

  1. "*" or empty string — matches everything.
  2. Contains only letters, digits, underscores, and | — treated as an exact name or pipe-separated list (e.g., Edit|Write).
  3. Contains other characters (dots, brackets, etc.) — treated as a JavaScript regular expression (e.g., mcp__.*).

The if Field — Filter by Tool Arguments

The if field (Claude Code v2.1.85+) goes beyond the group-level matcher. It uses permission rule syntax to filter by both tool name and arguments, so the hook process only spawns when the tool call matches.

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}

This hooks fires only when the Bash tool runs a command starting with git. Other Bash commands skip it entirely. The if field only works on tool events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied.

How Hooks Communicate — Input, Output, and Exit Codes

Hook Input (stdin / POST body)

When an event fires, Claude Code sends event-specific JSON to your hook. Command hooks receive it on stdin; HTTP hooks receive it as a POST body. Every event includes common fields like session_id, cwd, and hook_event_name, plus event-specific data.

For example, a PreToolUse hook for a Bash command receives:

{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}

Exit Codes

The exit code tells Claude Code what to do next:

  1. Exit 0 — the action proceeds. For UserPromptSubmit and SessionStart hooks, stdout text is added to Claude's context.
  2. Exit 2 — the action is blocked. Write a reason to stderr; Claude receives it as feedback.
  3. Any other exit code — the action proceeds but a hook-error notice appears in the transcript.

Structured JSON Output

For more control than allow/block, exit 0 and print a JSON object to stdout. Different events use different JSON patterns:

PreToolUse — Permission Decisions

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}

Available permissionDecision values for PreToolUse:

  1. "allow" — skip the interactive permission prompt (deny/ask rules still apply).
  2. "deny" — cancel the tool call and send the reason to Claude.
  3. "ask" — show the permission prompt to the user as normal.
  4. "defer" — available in non-interactive mode (-p), preserves the call for an SDK wrapper.

PermissionRequest — Auto-Approve Decisions

{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow"
}
}
}

PostToolUse / Stop — Block Decisions

These events use a top-level decision: "block" field. For Stop hooks, blocking tells Claude to keep working.

Practical Examples

Example 1: Get Notified When Claude Needs Input

Receive a desktop notification whenever Claude finishes working and waits for you, so you can switch to other tasks. This uses the Notification event.

macOS

{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}

Windows (PowerShell)

{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention','Claude Code')",
"shell": "powershell"
}
]
}
]
}
}

Add this to ~/.claude/settings.json. Verify with /hooks in the CLI.

Example 2: Auto-Format Code After Edits

Run Prettier on every file Claude edits. The PostToolUse event with an Edit|Write matcher ensures it runs only after file-editing tools.

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}

Add this to .claude/settings.json in your project root. The command uses jq to extract the edited file path from the hook input JSON, then pipes it to Prettier.

Example 3: Block Edits to Protected Files

Prevent Claude from modifying sensitive files like .env, package-lock.json, or anything inside .git/. This uses a separate script file and a PreToolUse hook.

Step 1 — Create the Hook Script

Save this to .claude/hooks/protect-files.sh:

#!/bin/bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done

exit 0

Step 2 — Make the Script Executable (macOS/Linux)

chmod +x .claude/hooks/protect-files.sh

Step 3 — Register the Hook

Add a PreToolUse hook to .claude/settings.json:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}

When Claude tries to edit a protected file, the hook exits with code 2 and sends the reason to stderr. Claude receives this feedback and adjusts its approach.

Example 4: Block Dangerous Shell Commands

Prevent DROP TABLE statements from ever running:

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2
exit 2
fi

exit 0

Attach this to PreToolUse with "matcher": "Bash".

Example 5: Re-Inject Context After Compaction

When Claude's context window fills up, compaction summarizes the conversation, which can lose important details. A SessionStart hook with a compact matcher re-injects critical context. Any text your command writes to stdout is added to Claude's context.

{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}

You can replace the echo with any command that produces dynamic output, like git log --oneline -5 to show recent commits.

Example 6: Log Every Bash Command

Record each command Claude runs to an audit log:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
}
]
}
]
}
}

Example 7: Audit Configuration Changes

Track when settings or skills files change during a session by appending each change to an audit log:

{
"hooks": {
"ConfigChange": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
}
]
}
]
}
}

Example 8: Reload Environment on Directory Change

If you use direnv, a CwdChanged hook reloads environment variables whenever Claude changes directories:

{
"hooks": {
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash >> \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}

Example 9: Auto-Approve a Specific Permission Prompt

Skip the approval dialog for ExitPlanMode so you aren't prompted every time a plan is ready. This uses a PermissionRequest hook with structured JSON output:

{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}

Warning: keep the matcher as narrow as possible. Matching on .* or leaving it empty would auto-approve every permission prompt, including file writes and shell commands.

Prompt-Based Hooks

When to Use Prompt Hooks

For decisions that require judgment rather than deterministic rules, use type: "prompt". Instead of running a shell command, Claude Code sends your prompt and the hook's input data to a Claude model (Haiku by default) to make the decision.

How It Works

The model returns a yes/no decision as JSON:

  1. "ok": true — the action proceeds.
  2. "ok": false + "reason" — the action is blocked, and the reason is fed back to Claude.

Example: Check Task Completeness Before Stopping

{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}

If the model returns "ok": false, Claude keeps working and uses the reason as its next instruction. You can specify a different model with the "model" field.

Agent-Based Hooks

When to Use Agent Hooks

When verification requires inspecting files or running commands, use type: "agent". Unlike prompt hooks (single LLM call), agent hooks spawn a subagent that can read files, search code, and use other tools to verify conditions.

Example: Verify Tests Pass Before Stopping

{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}

Agent hooks use the same "ok" / "reason" response format as prompt hooks, but with a longer default timeout (60 seconds) and up to 50 tool-use turns.

HTTP Hooks

When to Use HTTP Hooks

Use type: "http" when you want a web server, cloud function, or external service to handle hook logic — for example, a shared audit service that logs tool use events across a team.

Example: Post Tool Use to a Logging Service

{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}

Header values support environment variable interpolation ($VAR_NAME). Only variables listed in allowedEnvVars are resolved. The endpoint returns JSON in the same format as command hooks.

Building an HTTP Hook Endpoint in C# / ASP.NET Core

If you want to build your own hook receiver as a web API, here is a minimal ASP.NET Core endpoint that receives PostToolUse events and logs them:

// Program.cs — Minimal API for receiving Claude Code HTTP hooks
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/hooks/tool-use", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var json = await reader.ReadToEndAsync();

// Log the tool use event
Console.WriteLine($"[Hook] Tool use event: {json}");

// Return success — action proceeds
return Results.Ok(new { });
});

app.Run();

For a blocking response (e.g., denying a PreToolUse call), return the appropriate JSON structure:

app.MapPost("/hooks/pre-tool", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var json = await reader.ReadToEndAsync();

// Parse and check the tool call
var doc = System.Text.Json.JsonDocument.Parse(json);
var toolName = doc.RootElement.GetProperty("tool_name").GetString();

if (toolName == "Bash")
{
var command = doc.RootElement
.GetProperty("tool_input")
.GetProperty("command")
.GetString();

if (command?.Contains("rm -rf", StringComparison.OrdinalIgnoreCase) == true)
{
return Results.Ok(new
{
hookSpecificOutput = new
{
hookEventName = "PreToolUse",
permissionDecision = "deny",
permissionDecisionReason = "rm -rf is not allowed by policy"
}
});
}
}

return Results.Ok(new { });
});

Combining Multiple Hooks

Multiple Events in One Settings File

Each event name is a key inside the single "hooks" object. Here is a settings file with several hooks working together:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}

Conflict Resolution

When multiple hooks match, each returns its own result. Claude Code picks the most restrictive answer. A PreToolUse hook returning "deny" cancels the tool call no matter what others return. One hook returning "ask" forces the permission prompt even if the rest return "allow".

Stop Hook Infinite Loop Prevention

The Problem

A Stop hook that always blocks will keep Claude working forever in an infinite loop.

The Fix

Check the stop_hook_active field from the JSON input and exit early if it's true:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
# ... rest of your hook logic

Hooks and Permission Modes

PreToolUse hooks fire before any permission-mode check. A hook returning permissionDecision: "deny" blocks the tool even in bypassPermissions mode or with --dangerously-skip-permissions.

The reverse is not true: a hook returning "allow" does not bypass deny rules from settings. Hooks can tighten restrictions but not loosen them past what permission rules allow.

Debugging Hooks

The /hooks Menu

Type /hooks in Claude Code to browse all configured hooks grouped by event. A count appears next to each event that has hooks configured. Select an event to see hook details: event, matcher, type, source file, and command. The menu is read-only — to edit, modify the settings JSON directly.

Debug Logging

Start Claude Code with a debug file to capture full execution details:

claude --debug-file /tmp/claude.log

Then in another terminal:

tail -f /tmp/claude.log

If you started without that flag, run /debug mid-session to enable logging.

Manual Hook Testing

Test your hook script by piping sample JSON:

echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $? # Check the exit code

Common Troubleshooting

  1. Hook not firing: run /hooks to confirm it appears under the correct event. Check matcher case-sensitivity.
  2. "command not found": use absolute paths or $CLAUDE_PROJECT_DIR.
  3. "jq: command not found": install jq or use Python/Node.js for JSON parsing.
  4. Script not running: make it executable with chmod +x.
  5. JSON validation failed: your shell profile may have unconditional echo statements that pollute stdout. Wrap them in an interactive-shell check:
# In ~/.zshrc or ~/.bashrc
if [[ $- == *i* ]]; then
echo "Shell ready"
fi

Limitations

  1. Command hooks communicate through stdout, stderr, and exit codes only. They cannot trigger / commands or tool calls.
  2. Hook timeout is 10 minutes by default, configurable per hook with the timeout field (in seconds).
  3. PostToolUse hooks cannot undo actions — the tool has already executed.
  4. PermissionRequest hooks do not fire in non-interactive mode (-p). Use PreToolUse hooks for automated permission decisions.
  5. Stop hooks fire whenever Claude finishes responding, not only at task completion. They do not fire on user interrupts. API errors fire StopFailure instead.
  6. When multiple PreToolUse hooks return updatedInput to rewrite a tool's arguments, the last one to finish wins (hooks run in parallel, order is non-deterministic).
  7. SessionStart supports only command hooks.
  8. On Windows, use "shell": "powershell" for command hooks.

Quick Reference Card

Hook TypeBest ForKey Fields
commandShell scripts, CLI tools, deterministic checkscommand, shell, async
httpExternal services, team-wide loggingurl, headers, allowedEnvVars
promptJudgment-based decisions, single-turn evaluationprompt, model
agentMulti-turn verification, file inspection, running testsprompt, model, timeout
Exit CodeMeaning
0Allow / success. Optionally return JSON on stdout for structured control.
2Block the action. Write a reason to stderr.
OtherAction proceeds, but a hook-error notice appears in the transcript.

Further Reading

  1. Hooks Reference — full event schemas, JSON output format, async hooks, MCP tool hooks.
  2. Hooks Guide — official quickstart and use-case walkthrough.
  3. Security Considerations — review before deploying hooks in shared or production environments.
  4. Bash Command Validator Example — complete reference implementation on GitHub.


Share this lesson: