Anatomy of Hook Lifecycles
Pre/Post/Stop hooks, matchers, lifecycles, and automation.

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.jsonunder thehookskey. - Core events:
SessionStart,PreToolUse,PostToolUse,PreCommit,Notification,SessionEnd,Error. - Use
PreToolUseto prevent problems andPostToolUseto 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:
| Order | Event | Phase |
|---|---|---|
| 1οΈβ£ | SessionStart | Session opens |
| 2οΈβ£ | PreToolUse | Before any tool call |
| 3οΈβ£ | Tool execution | (the actual action) |
| 4οΈβ£ | PostToolUse | After tool completes |
| 5οΈβ£ | PreCommit | Before a git commit |
| 6οΈβ£ | Notification | On important events |
| 7οΈβ£ | SessionEnd | Session 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β
| Event | When it fires | Typical uses |
|---|---|---|
SessionStart | When Claude Code session opens | Load context, env summary, welcome message |
PreToolUse | Before any tool call is executed | Block unsafe actions, secret scan, policy checks |
PostToolUse | After any tool call completes | Auto-lint, tests, format, notify, audit |
PreCommit | Before a git commit is created | Secret scan, licence check, policy enforcement |
Notification | Important events (errors, approvals, etc.) | Slack/webhook alerts, emails, dashboards |
SessionEnd | When session is closing | Persist summary, cleanup, audit log flush |
Error | When a hook or tool fails | Alert, 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β
| Field | Description |
|---|---|
matcher | Regex-like pattern that filters which tool / event triggers this hook |
hooks | Array of hook entries to run |
type | command Β· webhook Β· script Β· builtin Β· custom |
command | Shell command to execute (type: command) |
script | Path to script file (type: script) |
url | Webhook URL (type: webhook) |
timeoutMs | Max time (ms) before the hook times out |
concurrency | sequential (default) or parallel |
Hook Typesβ
| Type | What it does | Best for |
|---|---|---|
command π» | Runs a shell command | Scripts, linters, scans, formatters |
script π | Runs a script file (bash, node, python) | Complex logic |
webhook π | Sends an HTTP request | APIs, Slack, Datadog |
builtin βοΈ | Executes a built-in Claude action | Lint, format, compact |
custom π§© | Calls a registered custom integration | Org-specific tools |
Matchers β What Can They Match?β
Matchers are regex-like patterns matched against the tool or event name:
| Matcher | Matches | Examples |
|---|---|---|
* | Everything | All tools |
Read | Read operations | Read, Glob, Grep (read) |
Write | Write operations | Write, CreateFile, DeleteFile |
Edit | Edits to existing files | Edit, Patch, ApplyDiff |
Write|Edit | Write OR Edit | Either Write or Edit |
Bash | Any Bash command | Bash, Bash(git *) |
Bash(git *) | Git commands | git status, git commit |
Bash(npm *) | NPM commands | npm test, npm run build |
Bash(rm -rf *) | Dangerous deletes | rm -rf /tmp/* |
WebSearch | Web search | WebSearch(...) |
WebFetch | HTTP fetch | WebFetch(url) |
MCP:* | Any MCP tool call | MCP:github, MCP:postgres |
MCP:github | GitHub MCP only | Issues, PRs, repos |
Tool:browser | Browser automation | navigate, 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.jsonchanges.
"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.shto 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
PreToolUsehook blocks the tool call (exit 1to deny). - Timeouts protect you β always set
timeoutMsfor webhooks (1β5 s). - Order of hooks matters β first non-zero exit stops execution.
- Avoid network calls in
PreCommitif offline work is possible. - Logs may contain paths & args β review before sharing.
concurrency: parallelis 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
PreCommitfor offline workflows.
Troubleshootingβ
| Symptom | Likely cause | Fix |
|---|---|---|
| Hook not running | Wrong matcher regex | Test with "matcher": "*" first |
| Command failed | Script error | Check exit codes & local run |
| Too slow | Heavy script in hot path | Move work off the hot path / async |
| Infinite loop | Hook triggers itself | Add guard condition / matcher exclude |
| No logs | Not logging | Write to .claude/logs/ |
| Webhook hangs session | Missing timeoutMs | Set timeoutMs: 3000 |
| Hook runs twice | Multiple matchers overlap | Tighten matcher patterns |
| Works locally, not in CI | Missing env var | Pass via env: in entry |
Getting Started (5 steps)β
- Identify what should be automated (lint, scan, notifyβ¦).
- Write or choose a hook script.
- Add to
.claude/settings.jsonunder the right event. - Test with a small task.
- 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.