Skip to main content

Anatomy of Hook Lifecycles

Pre/Post/Stop hooks, matchers, lifecycles, and automation.

Hook Lifecycles

Automate guardrails, quality, and workflows with lifecycle hooks in Claude Code β€” the silent guardians that turn AI suggestions into AI-with-policy.


TL;DR​

  • A hook is a command, script, webhook, or built-in action that Claude Code runs automatically at lifecycle events.
  • Defined in .claude/settings.json under the hooks key.
  • Core events: SessionStart, PreToolUse, PostToolUse, PreCommit, Notification, SessionEnd, Error.
  • Use PreToolUse to prevent problems and PostToolUse to improve quality.
  • Fail closed for safety, fail open for convenience.

Why this matters​

Hooks are seat belts for your AI teammate. You don't think about them every drive, but the one time you need them, they save the trip. They turn AI suggestions into AI-with-policy.

Bottom line: Hooks make AI agents enterprise-ready β€” by making guardrails invisible.


The Big Picture​

Bottom line: Events flow; hooks intercept. Any hook can block the action by returning non-zero.


Hook Lifecycle Overview​

Hooks fire at key points during a Claude Code session and workflow:

OrderEventPhase
1️⃣SessionStartSession opens
2️⃣PreToolUseBefore any tool call
3️⃣Tool execution(the actual action)
4️⃣PostToolUseAfter tool completes
5️⃣PreCommitBefore a git commit
6️⃣NotificationOn important events
7️⃣SessionEndSession closes

Plus the cross-cutting Error event whenever a hook or tool fails.

πŸ’‘ Tip: Any hook can block the action if it exits with a non-zero status.


Built-in Lifecycle Events​

EventWhen it firesTypical uses
SessionStartWhen Claude Code session opensLoad context, env summary, welcome message
PreToolUseBefore any tool call is executedBlock unsafe actions, secret scan, policy checks
PostToolUseAfter any tool call completesAuto-lint, tests, format, notify, audit
PreCommitBefore a git commit is createdSecret scan, licence check, policy enforcement
NotificationImportant events (errors, approvals, etc.)Slack/webhook alerts, emails, dashboards
SessionEndWhen session is closingPersist summary, cleanup, audit log flush
ErrorWhen a hook or tool failsAlert, rollback, capture diagnostics

πŸ’‘ Tip: Use PreToolUse to prevent problems. Use PostToolUse to improve quality.


Hooks Configuration (.claude/settings.json)​

{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "./scripts/pre-tool.sh", "timeoutMs": 5000 }
],
"concurrency": "sequential"
},
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./scripts/policy-check.sh" },
{ "type": "webhook", "url": "https://hooks.slack.com/...", "timeoutMs": 3000 }
],
"concurrency": "parallel"
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "npm run lint" },
{ "type": "command", "command": "npm test --silent", "timeoutMs": 60000 }
]
}
]
}
}

πŸ’‘ Tip: Hooks run in the order listed. The first failure (non-zero exit) blocks the event.


Hook Entry Format​

FieldDescription
matcherRegex-like pattern that filters which tool / event triggers this hook
hooksArray of hook entries to run
typecommand Β· webhook Β· script Β· builtin Β· custom
commandShell command to execute (type: command)
scriptPath to script file (type: script)
urlWebhook URL (type: webhook)
timeoutMsMax time (ms) before the hook times out
concurrencysequential (default) or parallel

Hook Types​

TypeWhat it doesBest for
command πŸ’»Runs a shell commandScripts, linters, scans, formatters
script πŸ“„Runs a script file (bash, node, python)Complex logic
webhook 🌐Sends an HTTP requestAPIs, Slack, Datadog
builtin βš™οΈExecutes a built-in Claude actionLint, format, compact
custom 🧩Calls a registered custom integrationOrg-specific tools

Matchers β€” What Can They Match?​

Matchers are regex-like patterns matched against the tool or event name:

MatcherMatchesExamples
*EverythingAll tools
ReadRead operationsRead, Glob, Grep (read)
WriteWrite operationsWrite, CreateFile, DeleteFile
EditEdits to existing filesEdit, Patch, ApplyDiff
Write|EditWrite OR EditEither Write or Edit
BashAny Bash commandBash, Bash(git *)
Bash(git *)Git commandsgit status, git commit
Bash(npm *)NPM commandsnpm test, npm run build
Bash(rm -rf *)Dangerous deletesrm -rf /tmp/*
WebSearchWeb searchWebSearch(...)
WebFetchHTTP fetchWebFetch(url)
MCP:*Any MCP tool callMCP:github, MCP:postgres
MCP:githubGitHub MCP onlyIssues, PRs, repos
Tool:browserBrowser automationnavigate, click, screenshot

πŸ’‘ Tip: Specific patterns override wildcards. Order matters.


Concurrency & Flow Control​

Control how multiple hooks under the same matcher run.

sequential (default)​

Run hooks one after another. Use when order matters or hooks share state.

1 β†’ 2 β†’ 3

parallel​

Run hooks at the same time. Faster, but be careful with shared resources.

β”Œβ”€ 2
1 ──
└─ 3
"PostToolUse": [
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "npm run lint" },
{ "type": "command", "command": "npx prettier --write ." }
],
"concurrency": "parallel" }
]

Example Hook Scripts​

scripts/pre-tool.sh β€” block unsafe commands​

#!/usr/bin/env bash
case "$CLAUDE_TOOL_NAME" in
Bash)
if echo "$CLAUDE_TOOL_INPUT" | grep -Eq "rm -rf|sudo|curl .*\|.*sh"; then
echo "❌ Blocked: $CLAUDE_TOOL_INPUT" >&2
exit 1
fi ;;
esac
exit 0

scripts/secret-scan.sh β€” pre-commit​

#!/usr/bin/env bash
if git diff --cached | grep -Eiq "(api[_-]?key|secret|password|token)\s*=\s*['\"][^'\"]+['\"]"; then
echo "❌ Possible secret detected." >&2
exit 1
fi
exit 0

scripts/notify-slack.sh β€” webhook​

#!/usr/bin/env bash
curl -s -X POST -H "Content-Type: application/json" \
--data "{\"text\":\"πŸ€– $CLAUDE_EVENT at $PWD\"}" \
"$SLACK_WEBHOOK_URL"

scripts/save-summary.sh β€” session end​

#!/usr/bin/env bash
DATE=$(date +%Y-%m-%d_%H-%M)
mkdir -p docs/sessions
echo "# Session $DATE" > "docs/sessions/$DATE.md"
echo "_Auto-saved summary placeholder._" >> "docs/sessions/$DATE.md"

Step-by-Step Setup​

1. Create the scripts folder​

mkdir -p scripts && chmod 755 scripts

2. Drop in a starter pre-tool.sh (see above)​

chmod +x scripts/pre-tool.sh

3. Wire it into .claude/settings.json​

{
"hooks": {
"PreToolUse": [
{ "matcher": "*",
"hooks": [{ "type": "command", "command": "./scripts/pre-tool.sh", "timeoutMs": 5000 }] }
],
"PostToolUse": [
{ "matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "npm run lint" }] }
]
}
}

4. Restart Claude Code​

/exit
claude

5. Test​

Ask Claude to edit a file β†’ npm run lint should fire automatically.

6. Iterate​

Add more matchers as the team's pain points show up.


Variants for Different Team Settings​

πŸ‘€ Solo developer (lightweight)​

Lint-on-write only. Skip strict blocking.

"PostToolUse": [{ "matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "npm run lint -- --fix" }] }]

πŸ‘₯ Small team (2–10) (shared standards)​

Add secret scan + tests on PR.

"PreCommit": [{ "matcher": "*",
"hooks": [{ "type": "command", "command": "./scripts/secret-scan.sh" }] }],
"PostToolUse": [{ "matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "npm test --silent" }] }]

🏒 Enterprise / regulated (fail closed + audit)​

  • Central audit log, SIEM webhook, code-review required for settings.json changes.
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./scripts/policy-check.sh" },
{ "type": "webhook", "url": "https://siem.corp/audit", "timeoutMs": 3000 }
]
}
],
"SessionEnd": [
{ "matcher": "*",
"hooks": [{ "type": "command", "command": "./scripts/audit-flush.sh" }] }
]

🌍 Open-source / community​

  • Document every hook in CONTRIBUTING.md.
  • Make hooks opt-in for contributors (avoid surprise blocks on first clone).
  • Provide a bootstrap.sh to set them up explicitly.

Bottom line: Same hook engine, escalating strictness.


Anatomy of a Single Hook β€” Zoomed In​

{
"matcher": "Write|Edit", // when it fires
"hooks": [
{
"type": "command", // hook type
"command": "./scripts/post-write.sh", // what runs
"timeoutMs": 10000, // safety cap
"env": { "STRICT": "1" } // env vars passed in
}
],
"concurrency": "sequential" // order matters
}

Available env vars inside the script:

  • $CLAUDE_TOOL_NAME β€” tool that triggered the hook
  • $CLAUDE_TOOL_INPUT β€” the input passed to that tool
  • $CLAUDE_EVENT β€” lifecycle event name
  • $CLAUDE_SESSION_ID β€” current session id
  • $PWD β€” workspace root

Common Patterns & Recipes​

Pattern 1 β€” Auto-format after writes​

"PostToolUse": [{ "matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "npx prettier --write ." }] }]

Pattern 2 β€” Slack alert on errors​

"Error": [{ "matcher": "*",
"hooks": [{ "type": "webhook", "url": "https://hooks.slack.com/services/..." }] }]

Pattern 3 β€” Session summary on close​

"SessionEnd": [{ "matcher": "*",
"hooks": [{ "type": "command", "command": "./scripts/save-summary.sh" }] }]

Pattern 4 β€” Matcher scoping​

"matcher": "Write|Edit" // only file mutations
"matcher": "Bash" // only shell calls
"matcher": "MCP:*" // only MCP server calls
"matcher": "*" // everything (sparingly)

Pattern 5 β€” Conditional fail-open​

#!/usr/bin/env bash
./scripts/policy-check.sh
RC=$?
if [ $RC -ne 0 ] && [ "$STRICT" = "1" ]; then
exit $RC # fail closed in strict mode
fi
exit 0 # fail open otherwise

Pattern 6 β€” Webhook with timeout guard​

{ "type": "webhook", "url": "https://hooks.slack.com/...",
"timeoutMs": 2000 } // never hang the session

Pattern 7 β€” Idempotent post-write​

#!/usr/bin/env bash
# Safe to run repeatedly with the same effect
npx prettier --write . >/dev/null
exit 0

Pattern 8 β€” Compose with slash commands​

"PostToolUse": [
{ "matcher": "manual:command",
"hooks": [{ "type": "command", "command": "./scripts/log-command.sh" }] }
]

Gotchas βš οΈβ€‹

  • Hooks run with your user's permissions β€” audit script contents.
  • A failed PreToolUse hook blocks the tool call (exit 1 to deny).
  • Timeouts protect you β€” always set timeoutMs for webhooks (1–5 s).
  • Order of hooks matters β€” first non-zero exit stops execution.
  • Avoid network calls in PreCommit if offline work is possible.
  • Logs may contain paths & args β€” review before sharing.
  • concurrency: parallel is faster but unsafe if hooks share state.
  • Matchers are case-sensitive.
  • A * matcher fires on every event β€” use sparingly.

Best Practices βœ…β€‹

  • Keep hooks fast (< 5 seconds).
  • Fail closed for security checks; fail open for cosmetics.
  • Be idempotent β€” safe to run repeatedly.
  • Log to files for an audit trail.
  • Use matchers to limit scope and reduce noise.
  • Test locally before committing.
  • Version-control and review hook changes.
  • Never hard-code secrets in the command.

Anti-Patterns βŒβ€‹

  • Long-running hooks (60-second test suites in PostToolUse).
  • Hooks that modify repo files blindly.
  • Over-broad matchers (* everywhere).
  • Hard-coded secrets or tokens.
  • Swallowing errors silently (|| true).
  • No timeouts on webhooks.
  • Relying on network in PreCommit for offline workflows.

Troubleshooting​

SymptomLikely causeFix
Hook not runningWrong matcher regexTest with "matcher": "*" first
Command failedScript errorCheck exit codes & local run
Too slowHeavy script in hot pathMove work off the hot path / async
Infinite loopHook triggers itselfAdd guard condition / matcher exclude
No logsNot loggingWrite to .claude/logs/
Webhook hangs sessionMissing timeoutMsSet timeoutMs: 3000
Hook runs twiceMultiple matchers overlapTighten matcher patterns
Works locally, not in CIMissing env varPass via env: in entry

Getting Started (5 steps)​

  1. Identify what should be automated (lint, scan, notify…).
  2. Write or choose a hook script.
  3. Add to .claude/settings.json under the right event.
  4. Test with a small task.
  5. Monitor, log, and iterate.

Starter Template (copy-paste)​

{
"hooks": {
"PreToolUse": [
{ "matcher": "*",
"hooks": [
{ "type": "command", "command": "./scripts/pre-tool.sh", "timeoutMs": 5000 }
] }
],
"PostToolUse": [
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "npm run lint" }
] }
],
"PreCommit": [
{ "matcher": "*",
"hooks": [
{ "type": "command", "command": "./scripts/secret-scan.sh" }
] }
]
}
}

πŸ’‘ Tip: Start with PreToolUse + PostToolUse. Add more events as the team grows.


Glossary​

  • Hook β€” A shell command, script, webhook, or built-in action bound to a Claude lifecycle event.
  • Event β€” A point in the session lifecycle (PreToolUse, etc.).
  • Matcher β€” Regex-like pattern that scopes hook execution to specific tools/events.
  • Fail closed β€” Block on error (safe default for security).
  • Fail open β€” Continue on error (safe default for cosmetics).
  • Sequential vs parallel β€” Run hooks one-by-one or all at once.
  • Idempotent β€” Safe to run repeatedly with the same effect.

Further Reading​

  • Search: "Claude Code hooks documentation"
  • Search: "git pre-commit hooks examples"
  • Search: "shell hook patterns idempotent"
  • Search: "webhook timeout best practices"

πŸͺ™ Golden Rules​

  • 🎯 Curate ruthlessly β€” keep context small & relevant.
  • βš™οΈ Automate wisely β€” hook the right events.
  • πŸ›‘οΈ Be safe by default β€” least privilege.
  • πŸ‘οΈ Observe everything β€” logs, alerts, audits.
  • πŸ” Iterate & improve β€” feedback makes it better.

Remember: Hooks are your silent guardians. They prevent problems and enforce quality β€” automatically.