Build a Reliable AI Coding Environment with Claude Code Hooks – A Complete Developer Guide

This guide explains how Claude Code’s probabilistic nature can lead to missed actions and shows how deterministic Claude Code Hooks—command, HTTP, prompt, and agent types—provide reliable pre‑, post‑, and permission‑stage controls, with detailed configuration examples, troubleshooting tips, and CI/CD integration.

Tech Minimalism
Tech Minimalism
Tech Minimalism
Build a Reliable AI Coding Environment with Claude Code Hooks – A Complete Developer Guide

What are Claude Code Hooks?

Claude Code hooks are user‑defined actions (shell commands, HTTP calls, prompts, or sub‑agents) that run automatically at specific lifecycle stages of Claude Code, such as before a tool runs ( PreToolUse), after it finishes ( PostToolUse), when a notification is sent, or when the session ends. Unlike rules written in prompts, hooks are guaranteed to fire, providing deterministic control over formatting, security checks, notifications, and other automation steps.

Why Hooks Are Needed

Claude Code is a probability‑based language model. You can ask it to run npx prettier --write after editing a file, but the model may forget the instruction because it only treats CLAUDE.md rules as suggestions, not enforced constraints. This uncertainty is a common problem for all AI coding assistants.

How Hooks Solve the Problem

Hooks insert concrete, deterministic logic at key points, turning “maybe” into “always”. The execution flow is simple:

1. Event triggers (e.g., PreToolUse(Write))
   |
2. Matcher checks if the event matches a rule
   |
3. Hook script runs, receiving a JSON payload via <code>stdin</code>
   |
4. Exit code determines the next step:
   - <code>0</code> → continue original operation
   - <code>2</code> → block the operation
   - other → error

The same four‑step flow applies to all hook types.

Configuration Scopes

Hooks can be placed in three configuration files:

User scope : ~/.claude/settings.json – personal defaults, not committed to Git.

Project scope : .claude/settings.json – shared across the team, committed to the repository.

Local scope : .claude/settings.local.json – personal overrides, usually git‑ignored.

In practice, the Project scope provides the most value because it enforces the same rules for every contributor without manual setup.

Four Hook Types

Command – fast, low‑complexity shell commands (e.g., run Prettier).

HTTP – medium speed, send JSON payloads to external services (e.g., Slack, PagerDuty).

Prompt – slower, let Claude decide by asking a simple question.

Agent – slowest, spawns a sub‑agent with its own tools for deep analysis.

Command Hooks (the workhorse)

A command hook runs a shell script and decides the outcome based on its exit code. Example that blocks dangerous commands:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "if": "tool_input.command matches 'rm -rf /'",
        "hooks": [{
          "type": "command",
          "command": "jq -r '.tool_input.command' | grep -q 'rm -rf /' && exit 2 || exit 0"
        }]
      }
    ]
  }
}

Most real‑world use cases rely on command hooks for formatting, file protection, and logging because they are fast and easy to understand.

HTTP Hooks (external integration)

HTTP hooks POST the event JSON to a URL and act based on the HTTP status code. They are ideal for sending notifications to Slack, Discord, or custom monitoring services.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [{
          "type": "http",
          "url": "https://your-api.com/claude-webhook"
        }]
      }
    ]
  }
}

Success (2xx) behaves like exit 0. Non‑2xx codes are logged but do not block the operation; to block, return a JSON payload with permissionDecision": "deny" and a 2xx status.

Prompt Hooks (AI‑driven decisions)

Prompt hooks feed the event back to Claude for a simple yes/no judgment. Example that asks whether a Bash command is safe:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "prompt",
          "prompt": "Is this bash command safe for production? Does it modify system files, delete data, or access credentials?"
        }]
      }
    ]
  }
}

Prompt hooks incur extra latency and cost, so they should be used only when a human‑like judgment is required (e.g., security review of a migration script).

Agent Hooks (tool‑assisted verification)

Agent hooks launch a sub‑agent that can read files, grep, or run other tools before deciding. Example that checks naming conventions before a write:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [{
          "type": "agent",
          "prompt": "Check that the file being written follows project naming conventions as described in .claude/CONVENTIONS.md."
        }]
      }
    ]
  }
}

Agent hooks are the most powerful but also the slowest; reserve them for high‑risk checks.

Seven Production‑Ready Hook Examples

Automatic Formatting – runs Prettier, Black, gofmt, etc., after a file is written or edited.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "FILE=$(jq -r '.tool_input.file_path // .tool_input.file' /dev/stdin); case \"$FILE\" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write \"$FILE\";; *.py) black \"$FILE\";; *.go) gofmt -w \"$FILE\";; esac; exit 0"
      }]
    }]
  }
}

Protect Sensitive Files – blocks edits to .env, lock files, etc.

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "if": "tool_input.file_path matches '(\\.env|\\.env\\.local|package-lock\\.json|yarn\\.lock|pnpm-lock\\.yaml)'",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"BLOCKED: This file is protected. Edit manually.\"}' && exit 2"
      }]
    }]
  }
}

Completion Notification – sends a desktop notification on macOS or a Linux notify‑send.

{
  "hooks": {
    "Notification": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "MSG=$(jq -r '.message // \"Claude Code task completed\"' /dev/stdin); if [ \"$(uname)\" = 'Darwin' ]; then osascript -e \"display notification \"$MSG\" with title \"Claude Code\"\"; else notify-send 'Claude Code' \"$MSG\"; fi; exit 0"
      }]
    }]
  }
}

Session Context Injection – adds project name, Git branch, and latest commit to every Claude session.

{
  "hooks": {
    "SessionStart": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"Project: $(basename $(pwd)) | Branch: $(git branch --show-current 2>/dev/null || echo none) | Last commit: $(git log --oneline -1 2>/dev/null || echo none)\"}'; exit 0"
      }]
    }]
  }
}

Automatic Test Execution – runs the corresponding test file after a source file changes.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "if": "tool_input.file_path matches '\\.(ts|tsx|js|jsx|py)$'",
      "hooks": [{
        "type": "command",
        "command": "FILE=$(jq -r '.tool_input.file_path' /dev/stdin); TEST_FILE=$(echo \"$FILE\" | sed 's/\\.[^.]*$/\\.test/'); if [ -f \"$TEST_FILE\" ]; then npx jest \"$TEST_FILE\" --no-coverage 2>&1 | tail -5; fi; exit 0",
        "timeout": 30000
      }]
    }]
  }
}

Branch Protection – blocks direct pushes to main/master/production.

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "if": "tool_input.command matches 'git push.*(main|master|production)'",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"BLOCKED: Direct push to protected branch. Use a feature branch and open a PR.\"}' && exit 2"
      }]
    }]
  }
}

Security Audit Log – records every Bash command with a UTC timestamp.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "INPUT=$(cat /dev/stdin); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command'); echo \"[$(date -u +%Y-%m-%dT%H:%M:%SZ)] BASH: $CMD\" >> .claude/audit.log; exit 0"
      }]
    }]
  }
}

Getting Started Configuration

A minimal project‑level .claude/settings.json that includes the most common hooks looks like this (comments omitted for brevity):

{
  "hooks": {
    "SessionStart": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"Project: $(basename $(pwd)) | Branch: $(git branch --show-current 2>/dev/null)\"}' ; exit 0"
      }]
    }],
    "PreToolUse": [{
      "matcher": "Write|Edit",
      "if": "tool_input.file_path matches '(\\.env|\\.env\\..+|.*lock\\.json|.*lock\\.yaml)'",
      "hooks": [{
        "type": "command",
        "command": "echo '{\"message\": \"Protected file. Edit manually.\"}' && exit 2"
      }]
    }],
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "FILE=$(jq -r '.tool_input.file_path // .tool_input.file' /dev/stdin); case \"$FILE\" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write \"$FILE\";; *.py) black \"$FILE\";; *.go) gofmt -w \"$FILE\";; esac; exit 0"
      }]
    }],
    "Notification": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "MSG=$(jq -r '.message // \"Done\"' /dev/stdin); osascript -e \"display notification \"$MSG\" with title \"Claude Code\"\" 2>/dev/null || notify-send 'Claude Code' \"$MSG\" 2>/dev/null; exit 0"
      }]
    }],
    "Stop": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "echo '[STOP] $(date +%H:%M:%S)' >> .claude/session.log; exit 0"
      }]
    }]
  }
}

Customising for Your Tech Stack

Replace the formatting and test commands with those appropriate for your language (e.g., black for Python, cargo test for Rust, gofmt for Go). The matcher patterns should reflect the file extensions you care about.

Validating Hooks

Run /hooks inside Claude Code to list all active hooks and their matchers.

Inspect the configuration files ( ~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json) for syntax errors (use jq . settings.json).

Add "disableAllHooks": true to temporarily turn everything off.

CI/CD Integration

Hooks work in headless mode ( claude -p) as well. In CI, replace desktop notifications with log writes or Slack posts. Use PreToolUse with exit code 2 to pause the pipeline and require manual approval via --resume. Example GitHub Action using the official Claude Code Action:

- name: Run Claude Code
  uses: anthropics/claude-code-action@v1
  with:
    prompt: "Review this PR and suggest improvements"
    allowed_tools: "Read,Grep,Glob"
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

Make sure any OS‑specific commands (e.g., osascript) have fallbacks for Linux runners.

Team Hook Management

Three‑level hierarchy keeps rules clear:

Project level ( .claude/settings.json ) – shared team policies (file protection, formatting, branch guards).

Local level ( .claude/settings.local.json ) – personal tweaks (custom notifications, experimental hooks), usually git‑ignored.

User level ( ~/.claude/settings.json ) – global defaults (preferred formatter, UI style).

Common Issues & Troubleshooting

Hook Not Triggering

Matcher typo – matchers are case‑sensitive; use /hooks to see the exact tool name.

Invalid JSON – a missing comma or brace disables all hooks; validate with jq . settings.json.

Global disable – check for "disableAllHooks": true in any scope.

Hook Runs but Does Not Block

Use exit code 2 (not 1) to block an operation.

Return a JSON message so Claude can display the reason.

Infinite Loops

A Stop or PostToolUse hook that modifies files can re‑trigger the same hook. Keep such hooks passive (logging, notifications) or add an if condition to prevent recursion.

Performance Problems

Heavy SessionStart hooks slow startup; keep them under one second.

Frequent PreToolUse / PostToolUse hooks should have a timeout or be offloaded to an HTTP service.

Cache expensive results (e.g., Git branch lookup) in temporary files.

Reference Links

awesome‑claude‑code: https://github.com/hesreallyhim/awesome-claude-code

automationDevOpscodingClaude CodeAI hooks
Tech Minimalism
Written by

Tech Minimalism

Simplicity is the most beautiful expression of technology.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.