Claude Code hooks: how to intercept every tool call before it runs
One of the most powerful — and least documented — features revealed in the Claude Code source is the hooks system. You can intercept every single tool call Claude makes, before it executes.
This means you can:
- Auto-approve certain commands (no more hitting Enter 40 times)
- Block dangerous operations entirely
- Log every file Claude touches
- Inject context before tool execution
Here's how it works.
The hooks directory
Create a .claude/hooks/ directory in your project:
mkdir -p .claude/hooks
Hooks are shell scripts that Claude executes at specific lifecycle points.
PreToolUse hook
This runs before any tool call. The tool name and arguments are passed as environment variables:
# .claude/hooks/PreToolUse.sh
#!/bin/bash
# Auto-approve file reads — stop asking me every time
if [ "$TOOL_NAME" = "Read" ]; then
exit 0 # 0 = approve
fi
# Block rm -rf entirely
if [ "$TOOL_NAME" = "Bash" ] && echo "$TOOL_INPUT" | grep -q 'rm -rf'; then
echo "Blocked: rm -rf is not allowed" >&2
exit 1 # 1 = reject
fi
# Everything else: default behavior
exit 0
Make it executable:
chmod +x .claude/hooks/PreToolUse.sh
PostToolUse hook
This runs after a tool executes. Use it for logging or side effects:
# .claude/hooks/PostToolUse.sh
#!/bin/bash
# Log every file write to a change log
if [ "$TOOL_NAME" = "Write" ]; then
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) WROTE: $TOOL_INPUT_PATH" >> .claude/changes.log
fi
# Log every bash command Claude runs
if [ "$TOOL_NAME" = "Bash" ]; then
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) BASH: $TOOL_INPUT" >> .claude/commands.log
fi
exit 0
Available hook types
From the source, there are four hook points:
| Hook | When it fires |
|---|---|
PreToolUse |
Before any tool call |
PostToolUse |
After any tool call |
Notification |
When Claude sends a notification |
Stop |
When Claude finishes a task |
Available environment variables
All hooks receive these env vars:
$TOOL_NAME # "Read", "Write", "Bash", "Edit", etc.
$TOOL_INPUT # Full JSON of the tool arguments
$TOOL_INPUT_PATH # For file tools: the file path
$TOOL_OUTPUT # PostToolUse only: what the tool returned
$SESSION_ID # Current Claude Code session
Real-world use case: auto-approve safe operations
The most common use case is eliminating repetitive permission prompts:
# .claude/hooks/PreToolUse.sh
#!/bin/bash
# Auto-approve all reads
if [ "$TOOL_NAME" = "Read" ]; then exit 0; fi
# Auto-approve writes to test/temp directories
if [ "$TOOL_NAME" = "Write" ]; then
if echo "$TOOL_INPUT_PATH" | grep -qE '^(tests?/|tmp/|/tmp/)'; then
exit 0
fi
fi
# Auto-approve git status/log/diff (read-only git)
if [ "$TOOL_NAME" = "Bash" ]; then
if echo "$TOOL_INPUT" | grep -qE '^git (status|log|diff|show)'; then
exit 0
fi
fi
# Default: require manual approval
exit 2 # 2 = ask human
Use case: Stop hook for task completion
# .claude/hooks/Stop.sh
#!/bin/bash
# Notify when Claude finishes
echo "✅ Claude Code task complete" | notify-send "Claude Code" 2>/dev/null || true
# Run your test suite automatically after every Claude session
if [ -f package.json ]; then
npm test --silent 2>&1 | tail -5
fi
exit 0
Register hooks in settings.json
You can also register hooks in .claude/settings.json instead of the hooks directory:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/PreToolUse.sh"
}
]
}
]
}
}
The matcher field filters by tool name — you can have different hooks for different tools.
Combine with CLAUDE.md for full control
Hooks handle the mechanical side. CLAUDE.md handles the behavioral side. Together:
# CLAUDE.md
## Tool behavior
- You do not need to ask permission for file reads
- You do not need to ask permission for writes to tests/ or tmp/
- Always run the test suite after making changes
- Never run rm -rf (it is blocked at the hook level)
Now Claude knows it can proceed on reads/writes, and the hook enforces the rm -rf block even if Claude forgets.
Complete starter hooks setup
Copy this into .claude/hooks/PreToolUse.sh:
#!/bin/bash
# Claude Code PreToolUse hook — safe auto-approvals
TOOL=$TOOL_NAME
INPUT=$TOOL_INPUT
PATH_=$TOOL_INPUT_PATH
# === ALWAYS APPROVE ===
[[ "$TOOL" == "Read" ]] && exit 0
[[ "$TOOL" == "LS" ]] && exit 0
[[ "$TOOL" == "Glob" ]] && exit 0
[[ "$TOOL" == "Grep" ]] && exit 0
# === ALWAYS BLOCK ===
if [[ "$TOOL" == "Bash" ]]; then
echo "$INPUT" | grep -qE 'rm -rf|DROP TABLE|DELETE FROM' && {
echo "Blocked dangerous command" >&2
exit 1
}
fi
# === DEFAULT ===
exit 0 # approve everything else
The hooks system turns Claude Code from a supervised assistant into a trusted autonomous agent — you define the safety boundaries once, and it runs within them without interruption.
If you're running Claude on a flat-rate API (not pay-per-token), this becomes even more valuable — you can let it run long autonomous sessions without worrying about runaway costs.
I use SimplyLouie as my ANTHROPIC_BASE_URL for exactly this: $2/month flat rate, no token anxiety, hooks handling the safety layer.
Drop your hook configs in the comments — curious what permission rules others are running.
Top comments (0)