Claude Code hooks: how to intercept every tool call before it runs
Source: Dev.to
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
Below is a quick guide to getting started.
The hooks Directory
Create a .claude/hooks/ directory in your project:
mkdir -p .claude/hooksHooks are shell scripts that Claude executes at specific lifecycle points.
PreToolUse Hook
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 0Make it executable:
chmod +x .claude/hooks/PreToolUse.shPostToolUse Hook
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 0Make it executable:
chmod +x .claude/hooks/PostToolUse.shAvailable Hook Types
| 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 |
Environment Variables Passed to Hooks
All hooks receive the following variables:
$TOOL_NAME # e.g., "Read", "Write", "Bash", "Edit", …
$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 IDReal‑World Use Case: Auto‑Approve Safe Operations
# .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 read‑only git commands
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 humanMake it executable:
chmod +x .claude/hooks/PreToolUse.shUse Case: Stop Hook for Task Completion
# .claude/hooks/Stop.sh
#!/bin/bash
# Desktop notification 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 0chmod +x .claude/hooks/Stop.shRegister Hooks in settings.json
Instead of placing scripts in the hooks directory, you can register them in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/PreToolUse.sh"
}
]
}
]
}
}The matcher field filters by tool name, allowing different hooks for different tools.
Combine with CLAUDE.md for Full Control
Hooks handle the mechanical side of things, while CLAUDE.md defines the behavioral side. Using both together gives you complete control over Claude Code’s actions.
# CLAUDE.md
...(Add your CLAUDE.md content here.)
Tool behavior
- You do not need to ask permission for file reads.
- You do not need to ask permission for writes to
tests/ortmp/. - Always run the test suite after making changes.
- Never run
rm -rf(it is blocked at the hook level).
Starter hooks setup
Copy the following script 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 elseWhy use hooks?
The hooks system turns Claude Code from a supervised assistant into a trusted autonomous agent. You define the safety boundaries once, and Claude operates within them without interruption.
If you’re using 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, and hooks handling the safety layer.
Drop your hook configs in the comments — curious what permission rules others are running.
