Claude Code Hooks Practical Guide — Building Your Own Automation Pipeline with PreToolUse·PostToolUse
If you've used AI coding tools for any length of time, you've probably had moments like these: "I clearly said don't touch .env…" or "I asked it to run formatting, and this time it forgot." No matter how carefully you craft your prompts, LLMs don't remember. They're a new entity with every turn.
Claude Code Hooks is the feature that addresses exactly that gap. The system enforces that your chosen scripts run before and after Claude executes any tool. Unlike prompt instructions, hooks operate deterministically. Even if Claude forgets something, hooks never do.
By the end of this guide, you'll be able to set up a pipeline yourself: blocking dangerous operations in advance with PreToolUse, and automating linting, type checking, and secret scanning immediately on edit with PostToolUse. The core scripts are fewer than 20 lines of code.
Core Concepts
What Are Hooks, and Why Do You Need Them?
Claude Code internally runs an "agent loop." It receives a user request → decides which tool to use → executes the tool → reviews the result → decides the next action — and repeats. Hooks are event handlers that can intercept this loop at specific points.
| Event | When It Runs | What You Can Do |
|---|---|---|
| PreToolUse | After tool parameters are generated, before actual execution | Control approve / block via the decision field |
| PostToolUse | Immediately after a tool completes successfully | Post-process results, trigger chained automation |
| Stop | Just before the agent loop terminates | Verify completion, send desktop notifications |
At first I thought, "Can't I just write rules in CLAUDE.md?" But ultimately, CLAUDE.md is guidance that Claude reads and tries to follow, while hooks are rules enforced by the system. That difference is quite significant in practice.
Things to Know Before You Start
Before jumping into real-world examples, here are some important points to understand when using hooks for the first time.
- There is no sandbox. Hooks run with your user permissions. A poorly written hook can delete files or expose secrets, so keep scripts small and explicit.
- Hooks are one layer of defense. Blocking one tool doesn't prevent the model from achieving the same result through a different path. Use them together with Git hooks and CI/CD validation for safety.
- Be careful with untrusted repos. Project-level hooks (
.claude/settings.json) may run automatically when you open the repo. It's good practice to inspect hook contents before running external repos you've cloned.
3 Handler Types
You can choose from three approaches when implementing hooks.
Command — Executes a shell command directly. Receives JSON via stdin and returns results via exit code and stdout JSON. The most versatile and flexible option — overwhelmingly the most common in practice.
Prompt — Requests a single-turn evaluation from a Claude model. Useful when you need semantic judgment that's hard to catch with pattern matching.
Agent — Spawns a sub-agent. Has access to tools like Read, Grep, and Glob, making it useful for complex validation logic.
Use Python or bash for simple path checks or file scanning, bash for external tool integrations, and the Prompt type for semantic judgment (e.g., detecting architecture rule violations).
Communication Protocol — stdin/stdout JSON
The operation of a hook script can be summarized in one line: receive tool execution info as JSON via stdin, and return control signals as JSON via stdout.
[Claude] → stdin(JSON) → [Hook Script] → stdout(JSON) → [Claude]// Received via stdin (PreToolUse - Write tool example)
{
"session_id": "abc123",
"tool_name": "Write",
"tool_input": {
"file_path": "/src/app.ts",
"content": "..."
}
}// Returned via stdout (PreToolUse block example)
{
"hookSpecificOutput": {
"decision": "block",
"reason": ".env files require manual approval"
}
}There's one important distinction: PreToolUse and PostToolUse use different control mechanisms.
- PreToolUse: Controls whether the tool runs via the
decisionfield ("block"/"approve"). Return JSON to stdout and exit with code 0. - PostToolUse: The tool has already run, so it can't be cancelled. Signal errors via a non-zero exit code, and use stdout to pass feedback to Claude.
Mixing these two approaches can cause unexpected behavior, so be careful.
Reading stdin in bash scripts is also worth covering. Since $STDIN is not an environment variable but a stdin stream, you must read it with cat.
# Correct approach
STDIN_DATA=$(cat)
FILE_PATH=$(echo "$STDIN_DATA" | jq -r '.tool_input.file_path // empty')Basic settings.json Structure
Hooks are registered in Claude Code's settings.json. Both global (~/.claude/settings.json) and project-level (.claude/settings.json) configurations are supported.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/check_write.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/auto_format.sh"
}
]
}
]
}
}matcher supports regular expressions. You can conveniently match multiple tools at once using a pipe like Edit|Write. The examples below use external tools like jq, gitleaks, and osascript — make sure they're installed beforehand.
Practical Applications
Now let's look at how to actually use this.
Example 1: Protecting .env Files — Blocking Sensitive File Edits with PreToolUse
Honestly, this was what got me to try hooks in the first place. I broke into a cold sweat watching Claude diligently write code and then touch .env. A single Python script on PreToolUse is all it takes to fix this.
# ~/.claude/hooks/check_sensitive.py
import json
import sys
data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")
sensitive_patterns = [".env", "secrets", ".pem", ".key", "credentials"]
if any(pattern in path for pattern in sensitive_patterns):
print(json.dumps({
"hookSpecificOutput": {
"decision": "block",
"reason": f"Modifying sensitive file ({path}) requires manual approval"
}
}))
sys.exit(0)// ~/.claude/settings.json (global application)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/check_sensitive.py"
}
]
}
]
}
}| Code Point | Description |
|---|---|
json.load(sys.stdin) |
Parses tool execution info from the stdin stream |
sensitive_patterns list |
Manages blockable path patterns in an extensible way |
decision: "block" |
Signals Claude to abort this tool execution |
reason field |
The block reason message that Claude shows to the user |
Example 2: Auto-Formatting — Running Prettier Immediately on Edit with PostToolUse
Manually running formatting every time a file is modified is genuinely tedious. After adding this hook, style-related comments in our team's code reviews dropped noticeably. Using PostToolUse, Prettier runs automatically the moment Claude edits a file.
#!/bin/bash
# ~/.claude/hooks/auto_format.sh
# Read data from stdin stream
STDIN_DATA=$(cat)
FILE_PATH=$(echo "$STDIN_DATA" | jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Apply Prettier only to TypeScript/JavaScript/JSON files
if echo "$FILE_PATH" | grep -qE '\.(ts|tsx|js|jsx|json|css|md)$'; then
npx prettier --write "$FILE_PATH" 2>/dev/null
fi
# If it's a TypeScript file, run a type check too — pass errors to Claude via stdout
if echo "$FILE_PATH" | grep -qE '\.tsx?$'; then
TSC_OUTPUT=$(npx tsc --noEmit 2>&1 | head -20)
if [ -n "$TSC_OUTPUT" ]; then
echo "$TSC_OUTPUT" # Claude uses this output to decide its next action
fi
fi
exit 0// .claude/settings.json (project-level application)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/auto_format.sh"
}
]
}
]
}
}Example 3: Secret Scanning — Preventing API Key Leaks with PostToolUse
When an API key accidentally ends up in a file during development, catching it before a commit is ideal — but you can catch it even earlier, the moment the file is saved. If gitleaks is installed, you can use it right away.
#!/bin/bash
# ~/.claude/hooks/secret_scan.sh
STDIN_DATA=$(cat)
FILE_PATH=$(echo "$STDIN_DATA" | jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
exit 0
fi
# Scan the just-written file with gitleaks
if command -v gitleaks &> /dev/null; then
RESULT=$(gitleaks detect --source "$(dirname "$FILE_PATH")" \
--no-git --quiet 2>&1)
if [ $? -ne 0 ]; then
# In PostToolUse, error signals are sent via non-zero exit code
echo "⚠️ Secret detected: a potential secret was found in $FILE_PATH"
echo "$RESULT"
exit 1
fi
fi
exit 0Example 4: Architecture Rule Validation — Semantic Judgment with the Prompt Type
When using NestJS, business logic has a way of quietly slipping into Controllers. This is hard to catch with simple pattern matching, but with a Prompt-type hook, the Claude model can make the judgment directly.
// .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "The following code is about to be written to a NestJS Controller file (*.controller.ts). Determine whether business logic (data transformation, calculations, direct DB access) is being implemented directly in the Controller. If so, return JSON with hookSpecificOutput.decision set to 'block' and a reason explaining why. If there's no issue, return nothing. Code: $ARGUMENTS"
}
]
}
]
}
}Prompt-type hook caveat: An additional LLM call is made on every tool execution, which slows response time. It's best to narrow the
matcherto something like.*controller\.ts$and apply it selectively to only the key files.
Example 5: Task Completion Notification — Desktop Alerts with the Stop Event
When you hand off a long refactoring task to Claude and go work on something else, it's easy to miss when it finishes. A single Stop event hook gives you a macOS notification.
#!/bin/bash
# ~/.claude/hooks/notify_done.sh
# macOS
if command -v osascript &> /dev/null; then
osascript -e 'display notification "Task complete" with title "Claude Code" sound name "Glass"'
fi
# Linux (notify-send)
if command -v notify-send &> /dev/null; then
notify-send "Claude Code" "Task complete"
fi
exit 0// ~/.claude/settings.json
{
"hooks": {
"Stop": [
{
// Stop events fire at loop termination, not tied to a specific tool, so no matcher needed
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/notify_done.sh"
}
]
}
]
}
}Pros and Cons Analysis
Advantages
| Item | Description |
|---|---|
| Deterministic execution | Hooks always run, even if the LLM forgets the instruction |
| Immediate feedback | Lint and type checks run instantly on edit, enabling early error detection |
| Fine-grained control | Precise targeting via per-tool and per-file-pattern matching |
| Language-agnostic | Write in any familiar language — Python, Bash, Node.js, etc. |
| MCP integration | MCP server tools can be matched with hooks in the same way |
Disadvantages and Caveats
| Item | Description | Mitigation |
|---|---|---|
| No sandbox | Hooks run with user permissions — poorly written scripts can delete files or expose secrets | Keep scripts small and explicit; code review recommended |
| Bypassable | Blocking a specific tool doesn't prevent the model from achieving the same result another way | Treat as one layer in a defense-in-depth strategy only |
| No GUI | Managed entirely through JSON and code — no visual editor or sandbox testing | Validate scripts by running them directly in the terminal first |
| Sub-agents not covered | Hooks do not apply to sub-agents or pipe mode | Apply critical rules redundantly with Git hooks |
| Malicious project risk | Project-level hooks can run automatically when loading an untrusted repo | Inspect project hook contents before running |
| Maintenance burden | Hook scripts require ongoing upkeep as the project evolves | Version-control hooks like production code |
The most painful part in practice was debugging with nothing but JSON and no GUI. When a hook isn't working as expected, the fastest approach is to run the script directly in the terminal.
⚠️ Common Mistakes and Troubleshooting
- Using the
$STDINenvironment variable in bash — Hooks deliver data via the stdin stream, not an environment variable. You must read it first withSTDIN_DATA=$(cat). This mistake causesFILE_PATHto always be empty, so the hook does nothing. - Mixing exit codes and JSON stdout — Using
exit 1to block in PreToolUse while simultaneously outputtingdecision: "block"JSON causes unexpected behavior. Stick to one approach. - Attaching heavy hooks to all tools — Putting a Prompt-type or expensive scan on
matcher: ".*"for every tool will significantly degrade response speed. Apply selectively to only the key tools and files.
Closing Thoughts
Claude Code Hooks are the most practical way to work around the fundamental limitation that "LLMs forget" — by encoding those constraints in code.
3 steps you can take right now:
- Register your first hook. Copy the
.envprotection example above into~/.claude/settings.json, create the file~/.claude/hooks/check_sensitive.py, and save it. It takes effect starting with your next Claude Code session. - Add a PostToolUse formatting hook to your project. Register
auto_format.shin your project's.claude/settings.jsonand immediately experience Prettier running automatically every time Claude edits a file. - Explore community recipes and extend them into your own pipeline. There are already 20+ validated hook examples shared in
disler/claude-code-hooks-masteryandhesreallyhim/awesome-claude-code— pick ones that fit your team's conventions and put them to use.
Next post: Building a full-stack agent pipeline that integrates Claude Code MCP servers with Hooks to automate external DB and API calls
References
- Hooks reference — Official Claude Code documentation
- Automate workflows with hooks — Official guide
- Claude Code Hooks: A Practical Guide to Workflow Automation — DataCamp
- Automate Your AI Workflows with Claude Code Hooks — GitButler Blog
- Claude Code hooks: A practical guide with examples (2026) — eesel AI
- Claude Code Hooks: Complete Guide with 20+ Ready-to-Use Examples — DEV Community
- Claude Code Hooks Guide 2026: Automate Your AI Coding Workflow — DEV Community
- claude-code-hooks-mastery — GitHub (disler)
- awesome-claude-code — GitHub (hesreallyhim)
- Claude Code Hooks: Guardrails That Actually Work — paddo.dev
- Inside Claude Code: Architecture Behind Tools, Memory, Hooks, and MCP — Penligent
- Claude Code Hooks Master Guide — 18 Events — Claude Lab