AY Automate
Services
Case Studies
Industries
Contact
n8n logo
Claude logo
Cursor logo
Make logo
OpenAI logo
AUTOMATION GATEWAY

DEPLOYAUTOMATION

> System status: READY_FOR_DEPLOYMENT
Transform your business operations today.

Company
AY Automate
Connect with us
LinkedInXXYouTube
Explore AI Summary
ChatGPTClaude wrapperPerplexityGoogle AIGrokCopilot
Free Tools
  • ROI Calculator
  • AI Readiness Assessment
  • AI Budget Planner
  • Workflow Audit
  • AI Maturity Quiz
  • AI Use Case Generator
  • AI Tool Selector
  • Digital Transformation Scorecard
  • AI Job Description Generator
+ 5 more free tools
Our Builds
  • Ayn8nn8n Library
  • AyclaudeClaude Library
  • AyDesignMake your vibecoded app look like a $10M company
  • AyRankBe the solution cited by AI
  • LiwalaOpen Source
  • AY SkillsOur best skills
  • n8n × Claude CodeWorkflow builder
  • AY FrameworkOpen Source
Services
  • All Services
  • AI Strategy Consulting
  • AI Agent Development
  • Workflow Automation
  • Custom Automation
  • RAG Pipeline Development
  • SaaS MVP Development
  • AI Workshops
  • Engineer Placement
  • Custom Training
  • Maintenance & Support
  • OpenClaw & NemoClaw Setup
Industries
  • All Industries
  • Marketing Agencies
  • Ecommerce
  • Consulting Firms
  • Revenue Operations
  • Law Firms
  • SaaS Startups
  • Logistics
  • Finance
  • Professional Services
Resources
  • Blog
  • Case Studies
  • Playbooks
  • Courses
  • FAQ
  • Contact Us
  • Careers
Stay Updated

Stay tuned

Get the latest automation insights, playbooks, and case studies delivered to your inbox. No spam, ever.

Join 4,500+ operators · Weekly · Unsubscribe anytime

Featured
Claude

30 Days of Claude Code

Daily challenges + agents

n8n

AI Automation Playbook

Free guide · 1,000+ hours saved

Golden Offer

Scale your company without hiring more staff

Get in touch
Walid Boulanouar
Walid BoulanouarCo-Founder · CEO
Adel Dahani
Adel DahaniCo-Founder · CTO
contact@ayautomate.com

Operating Globally

Serving clients worldwide - across North America, Europe, MENA, Asia & beyond.

© 2026 AY Automate. All rights reserved.
Terms of UsePrivacy Policy
Blog
20 June 2026/16 min read

How to Write Claude Code Hooks (With 7 Production Examples)

Claude Code hooks are the difference between a coding assistant that does what you want and one that does what you say. By 2026, hooks are how serious teams enforce guardrails, automate formatting, track cost, and ship safely. This tutorial walks through the hook system end to end with seven production examples you can copy today.

Boulanouar Walid
Author:Boulanouar Walid,Founder & CEO
How to Write Claude Code Hooks (With 7 Production Examples)

Book a Free Strategy Call

Skip the read — talk to Walid in 30 min.

Free strategy call. We map your AI engineering team, you keep the notes.

Or send us a brief →

Claude Code hooks changed in 2025. By 2026, the question is no longer "do I need hooks" but "which lifecycle events do I intercept, and what JSON do I return to keep the agent on rails." Hooks are the deterministic layer underneath an otherwise probabilistic agent. They are how you stop a destructive rm -rf, auto-run Prettier after every edit, scan for secrets before a file lands on disk, and pipe token cost into your status line in real time.

The hard part is not registering a hook. It is knowing which event fires when, which JSON contract Claude Code expects back, and how to write commands that fail closed instead of silently swallowing errors. Most "hooks broken" tickets are matcher typos, wrong exit codes, or a shell script that returns 0 when it should have returned 2.

This tutorial shows you exactly how to write Claude Code hooks in 2026. You will get the full event lifecycle, the JSON shape Claude Code expects, the settings.json registration format, and seven copy-paste production examples covering bash guardrails, auto-format, auto-test, secret scanning, cost tracking, Slack notifications, and a custom status line. If you want a curated list of community hooks instead of building your own, see our best Claude Code hooks and best Claude Code hooks examples roundups.

The hook system in 60 seconds

A Claude Code hook is a shell command that Claude Code runs at a specific point in the agent loop. The agent pauses, runs your command, reads stdout and the exit code, and then decides whether to continue, block, or modify what it was about to do.

Three things to understand:

  1. Where hooks live. Hooks are registered in settings.json. You can put them in three places: global at ~/.claude/settings.json, project-shared at <repo>/.claude/settings.json (checked into git), and project-local at <repo>/.claude/settings.local.json (gitignored, for personal overrides). All three merge at runtime.
  2. The event lifecycle. Every agent turn fires events in order: UserPromptSubmit → SessionStart (first turn) → PreToolUse (per tool call) → tool runs → PostToolUse → loop repeats → Stop when the turn ends. Notification, SubagentStop, and statusLine fire on their own triggers.
  3. The JSON contract. Claude Code pipes a JSON payload to your hook on stdin. Your hook can respond in two ways: exit code (0 = continue, 2 = block with stderr shown to the model, anything else = non-blocking error), or by writing a structured JSON object to stdout ({"decision": "block", "reason": "..."} for richer control).

That is the whole model. Everything below is detail.

Available hook events

EventFires whenCommon use
PreToolUseBefore any tool call executesBlock dangerous commands, validate inputs, require approval
PostToolUseAfter a tool call succeedsAuto-format, run tests, lint, push to remote
UserPromptSubmitWhen the user submits a promptInject context, redact secrets, log prompts
SessionStartFirst turn of a sessionLoad project briefing, set environment
StopAgent finishes its turnCost reporting, Slack notifications, cleanup
SubagentStopA spawned sub-agent finishesAggregate sub-agent results, log handoffs
NotificationClaude Code emits a system notificationPipe to OS notifications, desktop alerts
statusLineStatus line refreshes (every few seconds)Show tokens, cost, git branch, current model

Each event ships a different payload on stdin. PreToolUse and PostToolUse include the tool_name and tool_input (or tool_response). Stop includes the full transcript path. statusLine includes session metadata. Always cat stdin first when prototyping so you can see what you are working with.

How to register a hook

Hooks live under a top-level hooks key in settings.json. Each event maps to an array of matcher groups. Each group has a matcher (regex or string) and one or more hooks (commands to run).

Minimum viable structure:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/Users/you/.claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

The matcher field matches against tool_name for tool events, or is omitted for events that do not target a specific tool (Stop, UserPromptSubmit, etc.). You can use regex (Edit|Write|MultiEdit) or exact match (Bash). An empty string "" or omitted matcher matches everything.

The command field is run via the shell. Use absolute paths — Claude Code does not guarantee a working directory. Make scripts executable (chmod +x) and shebang them properly. Exit code 2 blocks the action and surfaces stderr to the model; exit code 0 lets it proceed.

7 production examples

These are real hooks we run in production at AY Automate and inside client engagements via our Claude Code agency. Copy them, adjust paths, and ship.

1) Block dangerous bash commands (PreToolUse)

Stops rm -rf /, :(){:|:&};:, dd of=/dev/sda, and friends before they execute. The hook reads the bash command from tool_input.command, regex-matches a denylist, and exits 2 with a reason if any pattern hits.

settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/block-dangerous-bash.sh"
          }
        ]
      }
    ]
  }
}

block-dangerous-bash.sh:

#!/usr/bin/env bash
set -euo pipefail

payload=$(cat)
cmd=$(echo "$payload" | jq -r '.tool_input.command // ""')

deny_patterns=(
  'rm[[:space:]]+-rf[[:space:]]+/($|[[:space:]])'
  'rm[[:space:]]+-rf[[:space:]]+~'
  'dd[[:space:]]+.*of=/dev/'
  ':\(\)\s*\{\s*:\|:&\s*\};:'
  'mkfs\.'
  '>\s*/dev/sd[a-z]'
  'chmod[[:space:]]+-R[[:space:]]+777[[:space:]]+/'
)

for pat in "${deny_patterns[@]}"; do
  if [[ "$cmd" =~ $pat ]]; then
    echo "Blocked: command matches dangerous pattern: $pat" >&2
    exit 2
  fi
done

exit 0

The model sees the stderr message and self-corrects on its next turn. This single hook eliminates an entire category of incidents.

2) Auto-format on Edit (PostToolUse)

After Claude edits or writes a file, run the right formatter for that file type. Keep your diffs clean without reminding the agent every prompt.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/auto-format.sh"
          }
        ]
      }
    ]
  }
}

auto-format.sh:

#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)
file=$(echo "$payload" | jq -r '.tool_input.file_path // .tool_input.path // ""')

[[ -z "$file" || ! -f "$file" ]] && exit 0

case "$file" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.md|*.css)
    npx --no-install prettier --write "$file" 2>/dev/null || true ;;
  *.py)
    ruff format "$file" 2>/dev/null || true
    ruff check --fix "$file" 2>/dev/null || true ;;
  *.go)
    gofmt -w "$file" 2>/dev/null || true ;;
  *.rs)
    rustfmt "$file" 2>/dev/null || true ;;
esac

exit 0

Always exit 0 here. A formatter failure should not block the agent — it should silently no-op.

3) Auto-test after Edit (PostToolUse)

When Claude touches source code, run the nearest test file. Catches regressions inside the agent loop instead of waiting for CI.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/auto-test.sh"
          }
        ]
      }
    ]
  }
}

auto-test.sh:

#!/usr/bin/env bash
set -uo pipefail
payload=$(cat)
file=$(echo "$payload" | jq -r '.tool_input.file_path // ""')

[[ -z "$file" ]] && exit 0

# Find a sibling test file
base="${file%.*}"
for candidate in "${base}.test.ts" "${base}.test.tsx" "${base}.spec.ts" "${base}_test.go" "test_$(basename "$base").py"; do
  if [[ -f "$(dirname "$file")/$(basename "$candidate")" ]]; then
    case "$candidate" in
      *.ts|*.tsx) result=$(npx --no-install vitest run "$(dirname "$file")/$(basename "$candidate")" 2>&1) ;;
      *_test.go)  result=$(go test "./$(dirname "$file")" 2>&1) ;;
      test_*.py)  result=$(pytest "$(dirname "$file")/$(basename "$candidate")" 2>&1) ;;
    esac
    if [[ $? -ne 0 ]]; then
      echo "Test failed for $candidate:" >&2
      echo "$result" >&2
      exit 2
    fi
    break
  fi
done

exit 0

Exit 2 here surfaces the failing test output to the model. It will read the failure and attempt a fix on the next turn — a tight feedback loop without you typing anything.

4) Secret scanning before file writes (PreToolUse)

Catch API keys, JWTs, and private keys before they touch the filesystem. This is the most underrated hook on the list.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/scan-secrets.sh"
          }
        ]
      }
    ]
  }
}

scan-secrets.sh:

#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)
content=$(echo "$payload" | jq -r '.tool_input.content // .tool_input.new_string // ""')

patterns=(
  'sk-[a-zA-Z0-9]{32,}'                  # OpenAI / Anthropic style
  'AKIA[0-9A-Z]{16}'                     # AWS access key
  'ghp_[a-zA-Z0-9]{36,}'                 # GitHub PAT
  'eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}'  # JWT
  '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'
  'xox[baprs]-[a-zA-Z0-9-]{10,}'         # Slack tokens
)

for pat in "${patterns[@]}"; do
  if echo "$content" | grep -qE "$pat"; then
    echo "Blocked: file content matches secret pattern. Use env vars or .env (gitignored)." >&2
    exit 2
  fi
done

exit 0

Pair this with a .env workflow and you get a meaningful security floor with zero ongoing effort.

5) Cost tracker on Stop

When the agent finishes a turn, parse the transcript and append the cost to a log. Helps you understand which prompts burn budget.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/track-cost.sh"
          }
        ]
      }
    ]
  }
}

track-cost.sh:

#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)
transcript=$(echo "$payload" | jq -r '.transcript_path // ""')
[[ -z "$transcript" || ! -f "$transcript" ]] && exit 0

# Transcript is JSONL; sum cost_usd from each assistant turn
total=$(jq -s '[.[] | select(.cost_usd) | .cost_usd] | add // 0' "$transcript")
session=$(echo "$payload" | jq -r '.session_id // "unknown"')
log="$HOME/.claude/logs/cost.jsonl"
mkdir -p "$(dirname "$log")"
echo "{\"ts\":\"$(date -u +%FT%TZ)\",\"session\":\"$session\",\"cost_usd\":$total}" >> "$log"
exit 0

Roll this up weekly with jq and you have an instant cost report — no observability vendor required.

6) Slack notification on Stop

Long-running agent? Get pinged in Slack when it finishes so you can context-switch without watching the terminal.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/slack-notify.sh"
          }
        ]
      }
    ]
  }
}

slack-notify.sh:

#!/usr/bin/env bash
set -euo pipefail
[[ -z "${SLACK_WEBHOOK_URL:-}" ]] && exit 0

payload=$(cat)
cwd=$(echo "$payload" | jq -r '.cwd // "unknown"')
project=$(basename "$cwd")

curl -s -X POST -H 'Content-type: application/json' \
  --data "{\"text\":\":white_check_mark: Claude finished a turn in \`${project}\`\"}" \
  "$SLACK_WEBHOOK_URL" > /dev/null || true

exit 0

Export SLACK_WEBHOOK_URL in your shell profile and you are done. Add a 30-second threshold check if you only want pings for long turns.

7) Custom status line showing tokens and cost

Replace the default status line with one that shows tokens used, cost, model, and git branch. Updates in real time.

{
  "statusLine": {
    "type": "command",
    "command": "$HOME/.claude/hooks/statusline.sh"
  }
}

statusline.sh:

#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)

model=$(echo "$payload" | jq -r '.model.display_name // "claude"')
cost=$(echo "$payload" | jq -r '.cost.total_cost_usd // 0')
input_tokens=$(echo "$payload" | jq -r '.tokens.input // 0')
output_tokens=$(echo "$payload" | jq -r '.tokens.output // 0')
branch=$(git -C "$(echo "$payload" | jq -r '.cwd')" branch --show-current 2>/dev/null || echo "-")

printf "\033[36m%s\033[0m | \033[33min:%s out:%s\033[0m | \033[32m\$%.4f\033[0m | \033[35m%s\033[0m" \
  "$model" "$input_tokens" "$output_tokens" "$cost" "$branch"

The statusLine event is the only one that uses stdout as the rendered output. Whatever you print is what shows in the status bar.

Debugging hooks

When a hook misbehaves, work through this checklist in order:

  • Check it is registered. Run claude --debug and inspect the merged config. Typos in event names (PreToolUse vs PreToolUSe) silently fail.
  • Verify the script runs at all. Add echo "fired" >> /tmp/hook.log at the top. If the log never grows, the matcher is wrong or the path is wrong.
  • Inspect the payload. Replace your command with cat > /tmp/payload.json temporarily. Look at the actual JSON shape — field names changed between versions.
  • Check exit codes. bash -x your-hook.sh < /tmp/payload.json; echo "exit: $?" to replay the exact invocation outside Claude Code.
  • Watch for shell quoting. A command with shell variables needs the right escaping in JSON. Prefer wrapping logic in a separate .sh file and call it directly.
  • Confirm jq is installed. Many examples depend on jq. On fresh machines, install it first.

If you are still stuck, the best Claude Code hooks examples post has working repos you can diff against your config.

Security considerations

Hooks run with your full shell privileges. Treat them like cron jobs you actually care about.

  • Never eval payload content. Always parse with jq and treat the result as untrusted input.
  • Pin commands to absolute paths. $PATH can be manipulated; /usr/local/bin/prettier cannot.
  • Fail closed for security hooks, open for cosmetic hooks. Secret scanning should exit 2 on error. Auto-format should exit 0.
  • Audit shared hooks before checking in .claude/settings.json. Anyone who pulls your repo and opens Claude Code will run those commands.
  • Do not write secrets into hook scripts. Read them from env vars or a keychain.
  • Log destructive actions. If a hook auto-pushes, deletes, or mutates remote state, write an audit log.

A hook is a tiny trust boundary between a probabilistic model and your real system. The whole point is determinism, so write them deterministically.

When to use hooks vs sub-agents

Hooks and sub-agents solve different problems. Use the right one.

Use hooks when:

  • You need a deterministic action on a specific event (format, scan, log).
  • The action is short, idempotent, and does not require reasoning.
  • You want to enforce a guardrail the model cannot disable.
  • You need to integrate with external systems on lifecycle (Slack, observability, billing).

Use sub-agents when:

  • The task requires reasoning, multi-step planning, or tool use.
  • You want a scoped persona (reviewer, tester, planner) that can run autonomously.
  • The work benefits from a separate context window.
  • You want to parallelize independent work streams.

In practice you compose them. A PostToolUse hook can spawn a reviewer sub-agent. A Stop hook can hand the transcript to a retro sub-agent. The hook is the trigger; the sub-agent is the brain.

If you want a team to design this end-to-end — hooks, sub-agents, MCP servers, guardrails, and CI integration — that is exactly what our Claude Code agency ships for clients. We bring the playbook. You bring the codebase. Book a free 30-minute consultation and we will scope it on the call.

FAQ

What language do Claude Code hooks have to be written in? Any language. The command field is run by your shell, so bash, zsh, Python, Node, Go binaries, anything that reads stdin and writes stdout works. Bash is most common because the payloads are small JSON blobs.

Where do I put my settings.json file? Three locations: ~/.claude/settings.json (global, applies to every project), <repo>/.claude/settings.json (project-shared, checked into git), and <repo>/.claude/settings.local.json (gitignored, personal overrides). All three merge — arrays concatenate.

What does exit code 2 do in a hook? Exit code 2 blocks the action that triggered the hook and surfaces stderr to the model. The model sees the error message on its next turn and adjusts. Any non-zero, non-2 exit code is treated as a non-blocking error (logged but ignored).

How do I share hooks with my team without leaking personal config? Commit the team hooks in <repo>/.claude/settings.json. Put personal preferences in <repo>/.claude/settings.local.json and gitignore it. The two merge at runtime.

Can a hook modify the prompt or tool input? Yes. For UserPromptSubmit, return JSON like {"decision": "modify", "prompt": "new prompt"} on stdout. For PreToolUse you can return {"decision": "block", "reason": "..."} to block, or let it pass through. Modification semantics vary by event — always test with --debug.

Do hooks run during claude --headless mode? Yes. Hooks are part of the agent loop, not the UI. They fire identically in headless, in CI, and in interactive mode. This is why secret scanning and bash guardrails matter even more in CI.

Will hooks slow down my Claude Code sessions? A well-written hook adds 50–200ms per fire. A poorly written hook that runs a full test suite on every edit can add 30 seconds. Profile with time and move expensive work to Stop instead of PostToolUse.

Where can I find more example hooks to copy? Start with the best Claude Code hooks roundup and the best Claude Code hooks examples gallery. Both link to working repos you can clone and modify.

Book a Free Strategy Call

Building this in production?

Walid runs a 30-min call to map your AI engineering team. Free, no slides.

Or send us a brief →
Share this article
About the Author
Boulanouar Walid
Boulanouar Walid
Founder & CEO

Walid founded AY Automate to help businesses ship AI workflows that actually move revenue. He leads strategy and oversees every client engagement end-to-end.

Full Bio →