When I first discovered Claude Code hooks, I thought I’d found a cheat code for developer productivity. No more manual validation steps. No more forgetting security checks before deployment. No more scrambling to keep my team updated on task completion.
Hooks are the automation layer that transforms Claude Code from a powerful tool into an autonomous system that works while you’re focused on the actual problem-solving.
What Are Hooks?
Hooks are event-driven shell commands that execute automatically when specific actions happen during a Claude Code session. Think of them as Git hooks or webhook handlers, but for your AI-assisted development workflow.
Instead of manually running checks or notifications, you define once what should happen when Claude edits a file, uses a tool, or completes a task. Then it just… happens. Automatically. Every time.
For an indie developer managing multiple projects, this eliminates entire categories of human error. You can’t forget to lint if the linting happens automatically after every file change.
The Four Hook Types
Claude Code hooks are organized into four categories based on what they respond to:
Tool Hooks — Trigger when Claude uses specific tools like file editors, bash commands, or API calls. You can hook into PreToolUse (before the tool runs) and PostToolUse (after completion).
Session Hooks — Respond to lifecycle events in your Claude Code session like startup and prompt submission. SessionStart runs when you begin a session; UserPromptSubmit fires when you send a new prompt.
File Hooks — Listen for file system changes. FileChanged triggers whenever Claude edits a tracked file, useful for running formatters or validators.
Task Hooks — Monitor long-running operations. TaskCompleted fires when an autonomous task finishes, perfect for notifications and reporting.
Key Events Explained
Let me break down the most useful events you’ll actually hook into:
PreToolUse: Runs before Claude uses a tool. Use this for validation, security checks, or preventing certain operations. If your hook exits with a non-zero status, it can block the tool execution.
PostToolUse: Runs after a tool completes. Great for side effects like logging, formatting, or triggering dependent operations. This is where you’d auto-format code or run tests.
SessionStart: Fires when Claude starts working. Perfect for initializing environments, checking prerequisites, or logging session startup.
UserPromptSubmit: Triggers when you send a new prompt to Claude. Use this to validate the prompt content, check for accidental sensitive data, or log what you’re asking Claude to do.
FileChanged: Runs when Claude modifies a file. I use this constantly for auto-formatting and linting, so the code matches my style without extra effort.
TaskCompleted: Fires when an autonomous task finishes. This is my hook for Slack notifications — my team knows immediately when a background task wrapped up.
Setting Up Hooks
Hooks live in ~/.claude/hooks/ directory on your system. If the directory doesn’t exist, create it:
mkdir -p ~/.claude/hooks
Each hook is a simple shell script. Name them descriptively so you remember what they do:
~/.claude/hooks/post-file-edit.sh
~/.claude/hooks/pre-commit-check.sh
~/.claude/hooks/task-completed-notify.sh
Then configure which hooks run on which events in your ~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": { "toolName": "EditFile" },
"script": "post-file-edit.sh"
}
],
"PreToolUse": [
{
"matcher": { "toolName": "Bash" },
"script": "pre-commit-check.sh"
}
],
"TaskCompleted": [
{
"script": "task-completed-notify.sh"
}
]
}
}
Practical Examples
Example 1: Auto-Format Code After Every Edit
I hate forgetting to format code before committing. With a PostToolUse hook on file edits, it formats automatically:
#!/bin/bash
# ~/.claude/hooks/post-file-edit.sh
FILE="$1"
# Only format TypeScript and JavaScript
if [[ ! "$FILE" =~ \.(ts|js|tsx|jsx)$ ]]; then
exit 0
fi
# Run Prettier if available
if command -v prettier &> /dev/null; then
prettier --write "$FILE" 2>/dev/null
echo "Formatted $FILE"
fi
Configure it in settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": { "toolName": "EditFile" },
"script": "post-file-edit.sh"
}
]
}
}
Now every time Claude edits a TypeScript file, Prettier runs automatically. No intervention needed.
Example 2: Security Scan Before Commits
Before pushing code, I want a security check. Using a PreToolUse hook on bash commands:
#!/bin/bash
# ~/.claude/hooks/pre-commit-check.sh
COMMAND="$1"
# Only run on git commit commands
if [[ ! "$COMMAND" =~ ^git\ commit ]]; then
exit 0
fi
# Run security scanner
if command -v snyk &> /dev/null; then
snyk test --severity-threshold=high
if [ $? -ne 0 ]; then
echo "Security scan failed. Commit blocked."
exit 1 # Block the commit
fi
fi
exit 0 # Allow the commit
Since this is a PreToolUse hook, returning non-zero status blocks the tool execution entirely. Claude can’t commit until the security scan passes.
Example 3: Slack Notification on Task Completion
When an autonomous task finishes, notify your team immediately:
#!/bin/bash
# ~/.claude/hooks/task-completed-notify.sh
TASK_NAME="$1"
TASK_STATUS="$2"
DURATION="$3"
WEBHOOK_URL="${SLACK_WEBHOOK_URL}"
if [ -z "$WEBHOOK_URL" ]; then
exit 0 # Silently skip if no webhook configured
fi
curl -X POST "$WEBHOOK_URL" \
-H 'Content-type: application/json' \
--data "{
\"text\": \"Task Completed\",
\"blocks\": [
{
\"type\": \"section\",
\"text\": {
\"type\": \"mrkdwn\",
\"text\": \"*$TASK_NAME* completed in $DURATION\n Status: $TASK_STATUS\"
}
}
]
}"
In settings.json:
{
"hooks": {
"TaskCompleted": [
{
"script": "task-completed-notify.sh"
}
]
}
}
Now your entire team knows when long-running tasks finish, without you having to manually check.
Example 4: Validate Prompts for Sensitive Data
Before processing any user prompt, scan it for accidentally-included secrets:
#!/bin/bash
# ~/.claude/hooks/validate-prompt.sh
PROMPT="$1"
# Check for common secret patterns
if echo "$PROMPT" | grep -qiE "(password|api[_-]?key|secret|token)" ; then
echo "WARNING: Prompt may contain sensitive data. Review before continuing."
# In a blocking PreToolUse context, this would prevent execution
exit 1
fi
exit 0
This runs on every prompt submission, adding a safety layer without slowing you down.
Hook Matchers: Filtering Your Automation
Hooks support matchers so you can be selective about which hooks run. You can filter by:
toolName: Specific tool name like “EditFile”, “Bash”, “ExecuteSQL”filePattern: Glob patterns like “*.test.ts” or “src/**”eventName: The specific event (though this is usually implicit)
{
"hooks": {
"PostToolUse": [
{
"matcher": {
"toolName": "EditFile",
"filePattern": "src/**/*.ts"
},
"script": "typescript-formatter.sh"
},
{
"matcher": {
"toolName": "EditFile",
"filePattern": "*.md"
},
"script": "markdown-formatter.sh"
}
]
}
}
Matchers let you fine-tune when hooks execute, avoiding unnecessary overhead.
Best Practices for Reliable Hooks
Over several months of using hooks in production, I’ve learned what actually works:
Keep hooks fast. A hook that takes 30 seconds to run will slow down your entire workflow. If you need longer operations, queue them to run asynchronously in the background.
Use PreToolUse for validation, not blocking. While you can block operations, it’s often better to just log warnings and let the user decide. Blocking tools too aggressively frustrates the workflow.
Always log. Append hook execution to a log file. When something goes wrong (and it will), you need a trail of what hooks ran and what they did.
echo "[$(date)] Hook executed: $0" >> ~/.claude/hooks.log
Exit cleanly. Always exit with 0 for success, non-zero only when blocking is intentional. Unexpected errors will disrupt your workflow.
Test hooks in isolation first. Write your hook script and test it manually before configuring it in settings.json. A broken hook can cascade across your entire session.
Use environment variables. Pass parameters to hooks via environment variables when your hook system supports it. Makes hooks more flexible and testable.
# In hook script
FILE="${CLAUDE_FILE_PATH}"
TOOL="${CLAUDE_TOOL_NAME}"
What’s Next?
Hooks are the foundation of Claude Code automation, but they’re just the beginning. In Part 6, we’ll explore advanced features: how to create multi-step automation pipelines, integrate with external services, and build your own custom hook system that goes beyond the defaults.
For now, start with one simple hook. Auto-format your code. Run a security check before commits. Send yourself a Slack notification when long tasks complete. These small automations compound into massive time savings over a week of development.
That’s the real power of Claude Code hooks — they turn repetitive manual tasks into invisible automation that just works.