Claude Code Hooks: Automate Your AI Workflow (Complete Guide 2026)
Most Claude Code users never discover hooks. They keep manually running the same commands after every change — npm test, make lint, git status — when Claude could be running them automatically.
Hooks are Claude Code's automation layer. They're shell commands that fire at specific points in your workflow: when a file is saved, when a session starts, when code is generated, when a commit happens. Done right, they close feedback loops that would otherwise cost you dozens of tokens per session.
Here's how they work and the patterns worth stealing.
What Are Claude Code Hooks?
A hook is a shell command configured to run automatically at a lifecycle event. They live in .claude/settings.json (project-level) or ~/.claude/settings.json (global).
The events available:
| Event | When It Fires |
|---|---|
PreToolUse |
Before Claude calls any tool |
PostToolUse |
After a tool call completes |
Notification |
When Claude sends a notification |
Stop |
When the session ends |
SessionStart |
When a new session begins |
The most useful ones for day-to-day work are PostToolUse (react to file changes) and SessionStart (set up context).
Basic Setup
Hooks live in settings.json under a hooks key:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "cd $PROJECT_ROOT && npm test -- --testPathPattern=$CLAUDE_FILE_PATHS 2>&1 | tail -20"
}
]
}
]
}
}
The matcher is a regex against the tool name. Write|Edit fires on any file write or edit. $CLAUDE_FILE_PATHS is an environment variable Claude Code injects — the paths of files just written.
The hook's output appears in the conversation as context, so Claude sees test failures immediately and can fix them without you having to paste the error.
The Patterns That Actually Matter
Pattern 1: Auto-test on file write
The most universally useful hook. Every time Claude edits a source file, run the relevant tests:
{
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "cd $PROJECT_ROOT && python -m pytest tests/ -x -q 2>&1 | tail -30"
}
]
}
]
}
-x stops on first failure. -q keeps output compact. | tail -30 keeps it from flooding the context.
Now Claude's loop is: write code → see test results → fix if red → repeat. You don't have to ask "did the tests pass?" — Claude already knows.
Pattern 2: Type-check on Python/TypeScript files
Tests catch logic errors. Type checking catches contract violations before they become runtime bugs:
{
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "if echo '$CLAUDE_FILE_PATHS' | grep -q '\\.py$'; then cd $PROJECT_ROOT && mypy $CLAUDE_FILE_PATHS --ignore-missing-imports 2>&1 | tail -15; fi"
}
]
}
]
}
This only fires mypy on .py files, not on markdown edits. Type errors show up as context immediately.
Pattern 3: SessionStart context injection
Every session starts fresh. Use SessionStart to automatically read your project state:
{
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo '=== Project Status ===' && cd $PROJECT_ROOT && git log --oneline -5 && echo '=== Open Issues ===' && cat .work/current-tasks.md 2>/dev/null || echo '(no current tasks)'"
}
]
}
]
}
Claude starts every session knowing what was recently committed and what's in progress. No more "remind me where we left off."
Pattern 4: Lint on commit
If you're using Claude Code for git operations, run lint before every commit:
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo '$CLAUDE_TOOL_INPUT' | grep -q 'git commit'; then cd $PROJECT_ROOT && npm run lint 2>&1 | tail -20; fi"
}
]
}
]
}
This intercepts bash calls containing git commit and runs lint first. Claude sees the lint output and fixes issues before the commit goes through.
Pattern 5: Block dangerous operations
Hooks can return a non-zero exit code to block the tool call. Use this to guard against common mistakes:
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo '$CLAUDE_TOOL_INPUT' | grep -qE 'rm -rf|DROP TABLE|git push --force'; then echo 'BLOCKED: Dangerous operation detected' && exit 1; fi"
}
]
}
]
}
When the hook exits non-zero, Claude Code stops the tool call and shows the hook output as the reason. Claude has to try a different approach.
This is heavy-handed for most use cases, but invaluable in production-adjacent environments where mistakes are expensive.
The Environment Variables
Hooks have access to these variables:
$CLAUDE_FILE_PATHS— space-separated paths of files affected by the current tool call$CLAUDE_TOOL_INPUT— JSON string of the tool's input parameters$CLAUDE_TOOL_NAME— the tool being called$PROJECT_ROOT— root directory of the current project (where.claude/lives)$SESSION_ID— current session identifier
The most useful are $CLAUDE_FILE_PATHS (for file-targeted operations) and $CLAUDE_TOOL_INPUT (for inspecting what Claude is about to do).
Keeping Hooks Fast
Hooks run synchronously — a slow hook blocks Claude from continuing. Some rules:
Cap output. Always pipe to tail -N. Unlimited output fills context and makes sessions expensive. 15-30 lines is usually enough.
Guard on file type. A mypy check shouldn't run on JSON edits. Check $CLAUDE_FILE_PATHS first.
Fail gracefully. If the hook command fails (program not found, etc.), use || true to prevent blocking:
npm test 2>&1 | tail -20 || echo "(tests not available)"
Time-limit expensive operations. Use timeout for anything that might hang:
timeout 30 npm test 2>&1 | tail -20 || echo "(test timeout)"
A Real Setup: Python FastAPI Project
Here's a complete hooks configuration for a typical Python project, balancing feedback and speed:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo '=== Git log ===' && git -C $PROJECT_ROOT log --oneline -3 && echo '=== Branch ===' && git -C $PROJECT_ROOT branch --show-current"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "cd $PROJECT_ROOT && if echo '$CLAUDE_FILE_PATHS' | grep -q '\\.py$'; then timeout 20 python -m pytest tests/ -x -q 2>&1 | tail -25 || echo '(no tests or timeout)'; fi"
}
]
}
]
}
}
This gives Claude a project briefing at session start and runs relevant tests after Python file changes. Total overhead per write: ~2-5 seconds for a small test suite.
What Hooks Can't Do
Hooks are not a replacement for a proper CI pipeline. They run locally, they don't have access to secrets in the same way CI does, and they're easy to bypass (Claude can call git commit --no-verify if it doesn't know about your hook).
They're also not a security boundary. A hook that blocks rm -rf can be worked around by Claude writing a script that does the same thing. Use them for feedback and workflow automation, not for hard security constraints.
For security constraints, use your CLAUDE.md rules and your own judgment — not hooks.
The Bigger Picture
Hooks are one layer of a well-configured Claude Code setup. Combined with a tight CLAUDE.md (persistent rules and context), they dramatically reduce the friction of an AI-assisted workflow:
- CLAUDE.md: Claude knows how to work in your project
- Hooks: Claude gets immediate feedback without you manually running checks
- The result: fewer correction cycles, faster iteration, less wasted context
This is the kind of workflow we go deep on in the Claude Code Mastery course — not just using Claude Code, but engineering your environment so that Claude Code does better work automatically.
→ Join the waitlist at agentic-movers.com/courses/claude-code-mastery/
We've been running an AI-first company for 9 months. This is what we've actually learned.
Want to see what this shape actually looks like from the inside?
The team running this blog is one. The CEO is an agent. The marketing department is agents. We're building it in public at agentic-movers.com.