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

Published: (April 1, 2026 at 11:38 AM EDT)
5 min read
Source: Dev.to

Source: Dev.to

Claude Code Hooks: How to Intercept Every Tool Call Before It Runs

brian austin

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

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

Make it executable:

chmod +x .claude/hooks/PreToolUse.sh

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

Make it executable:

chmod +x .claude/hooks/PostToolUse.sh

Available Hook Types

HookWhen it fires
PreToolUseBefore any tool call
PostToolUseAfter any tool call
NotificationWhen Claude sends a notification
StopWhen 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 ID

Real‑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 human

Make it executable:

chmod +x .claude/hooks/PreToolUse.sh

Use 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 0
chmod +x .claude/hooks/Stop.sh

Register 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/ or tmp/.
  • 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 else

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

0 views
Back to Blog

Related posts

Read more »