Claude Code hooks: how to intercept every tool call before it runs
<h1> Claude Code hooks: how to intercept every tool call before it runs </h1> <p>One of the most powerful — and least documented — features revealed in the Claude Code source is the <strong>hooks system</strong>. You can intercept every single tool call Claude makes, before it executes.</p> <p>This means you can:</p> <ul> <li>Auto-approve certain commands (no more hitting Enter 40 times)</li> <li>Block dangerous operations entirely</li> <li>Log every file Claude touches</li> <li>Inject context before tool execution</li> </ul> <p>Here's how it works.</p> <h2> The hooks directory </h2> <p>Create a <code>.claude/hooks/</code> directory in your project:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">mkdir</span> <span class="nt">-p</span>
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
Enter fullscreen mode
Exit fullscreen mode
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# .claude/hooks/PreToolUse.sh #!/bin/bashAuto-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`
Enter fullscreen mode
Exit fullscreen mode
Make it executable:
chmod +x .claude/hooks/PreToolUse.sh
Enter fullscreen mode
Exit fullscreen mode
PostToolUse hook
This runs after a tool executes. Use it for logging or side effects:
# .claude/hooks/PostToolUse.sh #!/bin/bash# .claude/hooks/PostToolUse.sh #!/bin/bashLog 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`
Enter fullscreen mode
Exit fullscreen mode
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$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 sessionEnter fullscreen mode
Exit fullscreen mode
Real-world use case: auto-approve safe operations
The most common use case is eliminating repetitive permission prompts:
# .claude/hooks/PreToolUse.sh #!/bin/bash# .claude/hooks/PreToolUse.sh #!/bin/bashAuto-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`
Enter fullscreen mode
Exit fullscreen mode
Use case: Stop hook for task completion
# .claude/hooks/Stop.sh #!/bin/bash# .claude/hooks/Stop.sh #!/bin/bashNotify 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`
Enter fullscreen mode
Exit fullscreen mode
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" } ] } ] } }{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash .claude/hooks/PreToolUse.sh" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
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)`
Enter fullscreen mode
Exit fullscreen mode
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`
Enter fullscreen mode
Exit fullscreen mode
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.
DEV Community
https://dev.to/subprime2010/claude-code-hooks-how-to-intercept-every-tool-call-before-it-runs-1ff8Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
claudeavailablefeature
How a Solana feature designed for convenience let attackers drain more than $270 million from Drift
The exploit did not involve a bug in Drift's code. It used "durable nonces," a legitimate Solana transaction feature, to pre-sign administrative transfers weeks before executing them, bypassing the protocol's multisig security in minutes.
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.






Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!