Claude Code Hooks Guide — Automation & Workflow Setup

8 min read · Claude Code configuration

Hooks let you automate workflows in Claude Code by triggering actions at specific moments: before a tool runs, after it completes, or when Claude stops and waits for input. With hooks, you can enforce rules (block commits to main), run quality checks automatically (tests after every file change), or surface warnings before risky operations (file deletion).

Hooks live in your settings.json and execute through your machine, not Claude. They're reliable, fast, and under your control. For the full shape of that file — scopes, precedence, and the other top-level keys — see the settings.json guide.

How hooks work

A hook has three parts:

Hooks are defined in JSON inside one of six possible locations — ~/.claude/settings.json for global rules, .claude/settings.json for project rules you commit to the repo, .claude/settings.local.json for machine-specific overrides, managed-policy settings for org-wide enforcement, plugin bundles, and skill or agent frontmatter. The full locations table below covers precedence. The harness that runs Claude Code executes hooks, so they work reliably even if Claude tries to bypass them.

Key: Hooks run on your machine, not through Claude. This means they're trustworthy for enforcing rules Claude shouldn't be able to override.

Example 1: Block commits to main

Prevent accidental direct commits to production branch

This hook watches for git commits and blocks any attempt to commit directly to main or master, forcing Claude to use a feature branch instead.

When to use: On any project with multiple developers or a main-to-prod deployment pipeline.

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git commit.*)",
        "hooks": [
          {
            "type": "command",
            "command": "if git rev-parse --verify HEAD 2>/dev/null && [[ $(git rev-parse --abbrev-ref HEAD) =~ ^(main|master)$ ]]; then echo '❌ Cannot commit directly to main/master. Create a feature branch first.'; exit 1; fi"
          }
        ]
      }
    ]
  }
}

How it works: Runs before any Bash command that looks like a git commit. If you're on main or master, exits with error and prints a message. Claude sees the error and switches to creating a branch.

Example 2: Run tests after file changes

Automatically run your test suite when files change

Whenever Claude writes or edits a file, run your test suite to catch broken tests immediately before Claude moves on.

When to use: On projects with fast test suites (< 30 seconds). Skip this if tests take > 1 minute.

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "if [ -f package.json ]; then npm test -- --run 2>&1 | head -50; elif [ -f pytest.ini ] || [ -f pyproject.toml ]; then python -m pytest --tb=short -q 2>&1 | head -50; fi"
          }
        ]
      }
    ]
  }
}

How it works: After Write or Edit completes, runs npm test (or pytest) and shows the first 50 lines of output. Claude sees test failures immediately and can fix them.

Example 3: Show git status when Claude stops

Display the working tree state for context

When Claude pauses to wait for your input, automatically show git status so you see what changed without asking.

When to use: Always. This is a pure information hook with no downsides.

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "echo '\\n━━━ Git Status ━━━'; git status --short 2>/dev/null || echo 'Not a git repo'"
          }
        ]
      }
    ]
  }
}

How it works: On every Stop event (when Claude finishes and waits for input), runs git status --short. You see file changes at a glance without needing to type the command yourself.

Example 4: Warn before file deletion

Surface a confirmation prompt before any file is deleted

Prevents accidental file deletion by showing a warning and requiring confirmation before Bash runs rm, rmdir, or git rm commands.

When to use: On any project where data loss is a risk. Always safer to warn first.

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(rm|rmdir|git rm)",
        "hooks": [
          {
            "type": "command",
            "command": "echo '⚠️  WARNING: About to delete file(s). Confirm this was intentional.' && read -p 'Continue? (y/N): ' -n 1 response && [[ $response == 'y' ]] || exit 1"
          }
        ]
      }
    ]
  }
}

How it works: Before any Bash command containing rm, rmdir, or git rm, prompts you to confirm. If you don't type 'y', the command is blocked.

Example 5: Audit all tool calls to a log file

Log successful tool calls for security and debugging

Keep a timestamped record of every tool Claude runs successfully — what it did, when. Useful for security audits or debugging unexpected behavior. Add the same hook under PostToolUseFailure as well if you also want to log failed calls.

When to use: On sensitive projects, team setups, or when debugging mysterious behavior.

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "mkdir -p \"$CLAUDE_PROJECT_DIR/.claude\" && INPUT=$(cat); T=$(echo \"$INPUT\" | jq -r '.tool_name'); echo \"$(date '+%Y-%m-%d %H:%M:%S') | $T\" >> \"$CLAUDE_PROJECT_DIR/.claude/tool-audit.log\""
          }
        ]
      }
    ]
  }
}

How it works: After each successful tool call, the hook ensures .claude/ exists, reads the hook payload from stdin with jq, extracts the tool_name field, and appends a timestamped line to .claude/tool-audit.log. Claude Code passes hook data as JSON on stdin — not environment variables — so jq is the shortest path to a specific field. Requires jq installed; on macOS brew install jq. To also capture failed tool calls, duplicate the block under PostToolUseFailure.

Hook events, matchers, and handler types

Events

Claude Code ships more than two dozen hook events. The three you'll reach for 90% of the time:

Other events worth knowing: SessionStart and SessionEnd (session lifecycle), UserPromptSubmit (before Claude processes your prompt), PostToolUseFailure (only fires when a tool errored), SubagentStart / SubagentStop (for teammate agents), PreCompact / PostCompact (context compaction), FileChanged (watched file changed on disk), and CwdChanged. The authoritative list lives at the official hooks reference — Anthropic has been adding events every few releases, so check the docs before you bet a workflow on an event name you remember.

Matchers (which tools trigger the hook)

Hook handler types

Each hook entry has a type field with four options:

Best practices

Keep hook commands fast

If a hook takes > 5 seconds, Claude's workflow becomes annoying. Run quick checks, not full test suites.

Use PreToolUse to block, PostToolUse to inform

PreToolUse hooks that exit with error code 1 will block the operation. PostToolUse hooks run after success, so they can't prevent the operation but can inform you (tests failed, formatting done, etc.).

Prefer project-specific hooks for constraints

Put hooks that enforce project rules (no commits to main, no .env writes) in .claude/settings.json at the project root so they're version-controlled and shared with your team. Put personal preference hooks (show git status, format code) in ~/.claude/settings.json globally.

Where hooks can live

Hooks can be defined in six different places. The mental model is additive, not override: Claude Code merges hook arrays across scopes, so a PreToolUse hook in your user settings and a PreToolUse hook in a plugin will both run. Managed policy settings are the exception — they can restrict which scopes are allowed to load hooks in the first place, and disableAllHooks set at the managed level overrides disableAllHooks anywhere else. Run /hooks inside Claude Code to see every source currently active.

LocationScopeShareable?
~/.claude/settings.jsonAll your projectsNo — 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.jsonWhile plugin enabledYes — bundled with plugin
Skill / agent frontmatterWhile component activeYes — in the component file
How hook commands receive data: Claude Code passes hook input as JSON on stdin, not environment variables. Parse it with jq:
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
CWD=$(echo "$INPUT" | jq -r '.cwd')
Common fields on stdin: session_id, transcript_path, cwd, permission_mode, hook_event_name. Tool events add tool_name and tool_input. A few environment variables are also set: $CLAUDE_PROJECT_DIR (project root), $CLAUDE_PLUGIN_ROOT (plugin install dir, if any), and $CLAUDE_ENV_FILE (on SessionStart / CwdChanged / FileChanged — append export KEY=value lines into this file to persist env vars for later Bash commands in the session).

Debugging hooks

If a hook isn't working, check that your JSON is valid (use jq to parse .claude/settings.json), confirm the settings file location is correct, and test the matcher pattern — try a broad one first like .*.

Hooks should be reliable, but if something goes wrong, you can temporarily disable all hooks by adding "disableAllHooks": true to your settings.json, then reload Claude Code.

Frequently asked questions

Where should I put my hooks — global or project settings?

Put project-wide rules (block commits to main, enforce lint) in .claude/settings.json at the project root so your team gets them on checkout. Put personal preferences (show git status, format on save) in ~/.claude/settings.json so they apply everywhere but aren't shared. Use .claude/settings.local.json for secrets or machine-specific commands that should stay gitignored.

How does a hook command receive data about the tool being called?

Claude Code passes a JSON payload on stdin. Parse it with jq: INPUT=$(cat); TOOL=$(echo "$INPUT" | jq -r '.tool_name'). Common fields include tool_name, tool_input, session_id, cwd, and permission_mode. A handful of environment variables are also set, including $CLAUDE_PROJECT_DIR and $CLAUDE_PLUGIN_ROOT.

Can a hook block a tool call before it runs?

Yes — PreToolUse hooks can block. If your command exits with a non-zero status, Claude sees the error and cancels the tool call. PostToolUse hooks run after the tool has already completed, so they can report problems but cannot prevent the action that already happened.

What is the difference between PreToolUse and PostToolUse?

PreToolUse fires before the tool runs and can block it by exiting non-zero — use it for guardrails like "no commits to main" or confirmation prompts before rm. PostToolUse fires after the tool succeeds and cannot prevent the action — use it for reactions like running tests after Edit or Write, or formatting. There is also a PostToolUseFailure event for when the tool failed.

Do hooks fire for subagents the same way they fire in the main conversation?

Yes — and the JSON on stdin includes agent_id and agent_type when a subagent triggered the hook, so you can filter by agent. There are also dedicated SubagentStart and SubagentStop events for subagent lifecycle.

Related reading: the MCP servers guide for how hooks layer on top of MCP tool calls, and the CLAUDE.md guide for how project rules load before hooks fire.

Get all 5 hooks pre-configured

The Claude Code Starter Kit includes these 5 hooks plus 4 more production-ready examples, 5 CLAUDE.md templates, 10 slash commands, settings profiles, and 20 power prompts — free download.

Download free →

Saved you time? Tip the maker in BTC — no account, no signup, just paste.

BTC bc1qs04leape97ner4wqa98n94l9n0gv9aa84eg4ux