DEV Community

brian austin
brian austin

Posted on

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
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

# 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
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

# 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
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
Enter 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

# 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
Enter fullscreen mode Exit fullscreen mode

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
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"
          }
        ]
      }
    ]
  }
}
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.

Top comments (0)