Claude Code hooks: how to intercept every tool call before it runs

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.