Claude Code Hooks: Automate Dev Team Workflows in 2026
Learn how Claude Code hooks work and 5 practical automation patterns for SME dev teams: linting, testing, Slack alerts, audit logs, and more.
TL;DR: Learn how Claude Code hooks work and 5 practical automation patterns for SME dev teams: linting, testing, Slack alerts, audit logs, and more.
Claude Code is already useful as an AI coding assistant, but most SME teams use it reactively. You type a prompt, Claude edits a file, you check the result. That is a fine start, but it leaves your team re-running the same manual steps after every AI-assisted change: running the linter, triggering tests, pasting a Slack update to the team. Claude Code hooks change this equation. They let you attach shell commands to specific lifecycle events so the routine work runs automatically, every time, without your team having to remember. For a 10-person dev team shipping fast, that difference adds up across hundreds of daily interactions.
This guide explains what hooks are, how to configure them in settings.json, and covers five concrete automation patterns you can adopt this week.
What Are Claude Code Hooks
Hooks are user-defined shell commands that Claude Code executes at defined points in its lifecycle. They are not AI features. They are deterministic scripts. Claude Code fires them at the right moment; your script does the work.
Claude Code documents a broad set of lifecycle events. The ones you will reach for most often are:
- PreToolUse: runs before Claude uses any tool (file write, bash command, etc.)
- PostToolUse: runs after a tool call completes
- UserPromptSubmit: fires when the user submits a new prompt
- Stop: runs when Claude finishes responding to a turn
- SessionStart / SessionEnd: run when a session opens or closes
The full set also covers subagent events, compaction events, and notifications. Verify the current event list at the official hooks reference (code.claude.com/docs/en/hooks.md) before relying on a specific event.
Each hook receives context about what just happened as a JSON payload over stdin. Parse it with jq (or any JSON tool) to get values like tool_name and tool_input.file_path. Claude Code also exposes a small set of documented environment variables, most usefully CLAUDE_PROJECT_DIR (the project root path). Avoid relying on undocumented env vars that may not exist in current versions.
Exit codes carry meaning: exit code 2 blocks the action and feeds your stderr back to Claude as the reason. Other non-zero exit codes show a warning but do not block. Exit code 0 is success.
How to Configure Hooks in settings.json
Hooks live in your Claude Code settings.json file. There are three documented locations, each with a different scope:
.claude/settings.json: project-level, team-shared, commit this file.claude/settings.local.json: project-level, personal, gitignored by default~/.claude/settings.json: user-level, applies to every project on your machine
Most team-wide hooks belong in the first file so every developer who clones the repo gets the same configuration.
A minimal hook configuration looks like this:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npm run lint"
}
]
}
]
}
}
The matcher field filters which tool calls trigger the hook. You can match on tool names like Write, Edit, Bash, or use "*" to catch everything.
That is the full configuration model. No build step, no plugin registry. You edit JSON, save the file, and the next Claude Code session picks it up.
Pattern 1: Auto-Lint Before Every File Write
The most common source of noise in AI-assisted coding is style drift. Claude writes valid code that fails your linter because it does not know your project's exact ESLint or Flake8 configuration. A PreToolUse hook solves this by running the linter on the target file before Claude commits the change.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -n \"$FILE\" ] && eslint --fix \"$FILE\" 2>&1 || true"
}
]
}
]
}
}
The hook reads the affected file path from the JSON payload via jq, runs ESLint with --fix, and lets non-fatal warnings through with || true. For blocking enforcement (where you want Claude to stop and re-think on a lint failure), remove the || true and emit a non-zero exit (specifically exit code 2 if you want to block the write rather than warn).
This pattern eliminates the round-trip where a developer reviews an AI change, runs lint, finds five style issues, and has to prompt Claude again to fix them.
Pattern 2: Run Tests After Code Changes
Tests should run after every substantive edit. Most teams skip this in practice because manually triggering test suites mid-session breaks flow. A PostToolUse hook on Edit or Write events keeps tests running continuously without developer intervention.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "python -m pytest tests/ -x -q --tb=short 2>&1 | tail -20"
}
]
}
]
}
}
The -x flag stops pytest at the first failure so you get fast feedback. The tail -20 keeps the output readable inside Claude Code's interface.
For a TypeScript project, swap in npx jest --passWithNoTests --bail 2>&1 | tail -20. The pattern is identical; only the test runner changes.
Pattern 3: Slack Notifications When Claude Completes a Task
Claude Code hooks at the Stop event give you a clean signal that Claude has finished responding. For SME teams where developers work across time zones or where one developer kicks off a long AI-assisted refactor and steps away, a Slack notification on completion is genuinely useful.
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "PROJECT=$(basename \"$CLAUDE_PROJECT_DIR\"); curl -s -X POST -H 'Content-type: application/json' --data \"{\\\"text\\\":\\\"Claude Code task complete in project: $PROJECT\\\"}\" \"$SLACK_WEBHOOK_URL\""
}
]
}
]
}
}
This uses the documented CLAUDE_PROJECT_DIR env var to derive the project name. Store SLACK_WEBHOOK_URL in your environment via Doppler or your existing secrets manager. Never hardcode it in settings.json.
You can make this smarter by reading the Claude session summary from the stdin payload and including the task description in the Slack message. That turns a simple ping into a lightweight async standup: the team sees what Claude worked on even if the developer is offline.
Pattern 4: Audit Logging for Every Tool Call
This pattern has practical relevance beyond developer productivity. Under the EU AI Act's transparency requirements and general GDPR accountability principles, teams using AI tools in software development may need to demonstrate what actions the AI system took and when. A PostToolUse hook that appends a structured JSON log line to a file gives you that trail without any external service dependency.
{
"hooks": {
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "PAYLOAD=$(cat); TOOL=$(echo \"$PAYLOAD\" | jq -r '.tool_name // \"unknown\"'); TS=$(date -u +%Y-%m-%dT%H:%M:%SZ); PROJECT=$(basename \"$CLAUDE_PROJECT_DIR\"); echo \"{\\\"ts\\\":\\\"$TS\\\",\\\"tool\\\":\\\"$TOOL\\\",\\\"project\\\":\\\"$PROJECT\\\",\\\"user\\\":\\\"$USER\\\"}\" >> ~/.claude/audit.log"
}
]
}
]
}
}
The hook reads the JSON payload from stdin, extracts tool_name with jq, derives the project from the documented CLAUDE_PROJECT_DIR env var, and appends one JSON line per tool call. The log captures the timestamp, tool name, project, and local user. For a team of five developers, this produces a searchable record of every file Claude wrote, every bash command it ran, and every read operation it performed.
For stricter audit requirements, replace the local file write with a POST to an internal logging endpoint or a write to an append-only S3 bucket with object lock enabled.
Pattern 5: Auto-Format After Every Edit
Code formatting is the task developers are most likely to skip under time pressure and most likely to argue about in code review. A PostToolUse hook on Write that runs Prettier, Black, or gofmt after every edit removes the decision entirely.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -n \"$FILE\" ] && { prettier --write \"$FILE\" 2>/dev/null; black \"$FILE\" 2>/dev/null; }; true"
}
]
}
]
}
}
Running both Prettier and Black in the same command is safe: Prettier handles JS/TS/CSS/JSON; Black handles Python. Non-matching files are silently skipped. The trailing ; true ensures the hook never blocks Claude regardless of formatter exit codes.
This pattern pairs well with Pattern 1. Lint catches logical issues; auto-format handles style. Both run without the developer doing anything.
Combining Patterns: A Practical settings.json for SME Teams
In practice, you will combine several of these patterns in one file. A production-ready settings.json for a Python and TypeScript monorepo might include:
- PreToolUse lint on Write events
- PostToolUse test run on Edit events
- PostToolUse audit log on all events
- Stop notification on all events
The order matters when multiple hooks fire on the same event. Claude Code executes them in the order they appear in the array. Put your fastest, most critical hooks first so failures surface quickly.
Operator Takeaway: What to Do This Week
The practical path for a 10-to-50 person dev team is to start with two hooks: audit logging (always on, zero friction) and auto-format (saves the most time per developer per day). Commit the project-level .claude/settings.json to your repo so every developer gets the same hooks automatically when they clone the project.
For hooks that require secrets (Slack webhooks, internal API endpoints), use environment variable references rather than hardcoded values and inject them through your existing secrets manager. This keeps settings.json safe to commit.
Review your audit log after two weeks. The data will show you which tools Claude uses most frequently, which projects generate the most activity, and where manual follow-up steps still persist despite the hooks. That data is the input for the next round of automation.
Three concrete actions for a CTO or engineering lead:
- Drop the audit-log hook into
.claude/settings.jsontoday. Zero friction, valuable evidence. - Add the auto-format hook for whatever language your team writes most.
- Plan a 30-minute review of the audit log in two weeks to inform what to automate next.
Need a structured rollout? Talk to First AI Movers about AI Consulting, or evaluate your team's posture first with the AI Readiness Assessment.
Frequently Asked Questions
Do hooks run in every Claude Code session, including CI environments?
Yes, hooks defined in .claude/settings.json at the project level run in any Claude Code session opened in that project directory, including automated or headless sessions. If you run Claude Code in a CI pipeline, the same hooks fire. Make sure your hook commands are available in the CI environment and that any required environment variables (such as Slack webhook URLs) are injected through your CI secrets manager.
Can hooks slow down Claude Code if the commands take too long?
Yes. A hook that runs a full test suite on every file write will noticeably slow down interactive sessions. Use scoped test runs rather than full suites. Pass the modified file path to the test runner and run only the tests covering that file. Claude Code does not currently impose a timeout on hook execution, so a long-running command will block until it completes.
Is there a way to test hooks without affecting my main project?
The cleanest approach is to create a separate directory with a minimal .claude/settings.json and run Claude Code there first. You can also temporarily add echo statements at the start of hook commands to confirm they are firing and receiving the expected environment variables before wiring in the real logic.

