DEV Community

~K¹yle Million
~K¹yle Million

Posted on

Claude Code Hooks: Automate What Happens Before and After Every Tool Call

Claude Code Hooks: Automate What Happens Before and After Every Tool Call

Most Claude Code deployments are missing a critical layer.

You've configured your agent. You've written your CLAUDE.md. Your tools are permitted. The agent runs — and it does exactly what you told it to. But then things happen around the tool calls that you never asked for and can't control: files get written without being linted, git commits happen without tests running, shell commands execute without audit logs.

Hooks fix this. They're the event system underneath Claude Code that most people haven't touched yet.


What Hooks Actually Are

Hooks are shell commands that Claude Code executes automatically in response to specific events — before a tool runs, after it completes, when you submit a prompt, when the agent stops. They run outside Claude's reasoning loop. The agent can't override them. They just fire.

This distinction matters. A prompt instruction like "always run tests before committing" depends on Claude following it. A hook that runs tests before every Bash call doesn't depend on Claude at all. It's guaranteed execution.

The four hook events:

Event When It Fires
PreToolUse Before any tool executes — can block the tool
PostToolUse After any tool completes
UserPromptSubmit When you hit enter on a prompt
Stop When Claude finishes responding

Where Hooks Live

Hooks are configured in .claude/settings.json — the same file that controls tool permissions. Structure:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo '[AUDIT] Bash about to run' >> ~/audit.log"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The matcher field accepts a tool name (Bash, Write, Edit, Read) or * to match everything.


Environment Variables Available to Hooks

Claude Code injects context into every hook execution:

CLAUDE_TOOL_NAME          # Which tool is about to run / just ran
CLAUDE_TOOL_INPUT_*       # Tool input fields (flattened)
CLAUDE_TOOL_OUTPUT        # Tool output (PostToolUse only)
CLAUDE_SESSION_ID         # Current session identifier
CLAUDE_CWD                # Working directory
Enter fullscreen mode Exit fullscreen mode

For a Write tool call, you'd get CLAUDE_TOOL_INPUT_FILE_PATH and CLAUDE_TOOL_INPUT_CONTENT. For Bash, you'd get CLAUDE_TOOL_INPUT_COMMAND. These are the variables that make hooks genuinely useful — you're not just running a static script, you're reacting to exactly what Claude is about to do.


Blocking Tool Execution

PreToolUse hooks can block a tool from running. If the hook command exits with a non-zero status code, Claude Code will not execute the tool. Claude sees the hook's output as an error and has to respond to it.

This is how you build hard constraints:

#!/bin/bash
# block_rm.sh — prevent any rm -rf from running
if echo "$CLAUDE_TOOL_INPUT_COMMAND" | grep -q "rm -rf"; then
  echo "BLOCKED: rm -rf is not permitted by hook policy"
  exit 1
fi
exit 0
Enter fullscreen mode Exit fullscreen mode

Hook in settings.json:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "bash ~/.claude/hooks/block_rm.sh"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now rm -rf is structurally impossible, regardless of what Claude reasons. The model can't talk its way past a hook.


Five Hooks Worth Deploying Today

1. Auto-format on every Write

{
  "PostToolUse": [
    {
      "matcher": "Write",
      "hooks": [
        {
          "type": "command",
          "command": "ext=\"${CLAUDE_TOOL_INPUT_FILE_PATH##*.}\"; case $ext in py) black \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null;; js|ts|tsx) prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null;; esac; exit 0"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Every file Claude writes gets formatted immediately. No separate cleanup step.

2. Audit log for all Bash commands

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) | $CLAUDE_SESSION_ID | CMD: $CLAUDE_TOOL_INPUT_COMMAND\" >> ~/intuitek/logs/bash_audit.log"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Every command Claude runs is timestamped and logged. You can replay exactly what happened in any session.

3. Run tests before every git commit

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "if echo \"$CLAUDE_TOOL_INPUT_COMMAND\" | grep -q \"git commit\"; then cd \"$CLAUDE_CWD\" && npm test --silent 2>&1 | tail -5; fi; exit 0"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Tests run before every commit. If they fail, Claude sees the output and can fix the issue before the commit lands.

4. Session start notification

{
  "UserPromptSubmit": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "command",
          "command": "bash ~/intuitek/notify.sh \"🟢 Claude Code session prompt submitted\" 2>/dev/null; exit 0"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Get a Telegram ping every time a prompt is submitted. Useful for async monitoring when you're away from the machine.

5. Write session summary on stop

{
  "Stop": [
    {
      "matcher": "*",
      "hooks": [
        {
          "type": "command",
          "command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) | session ended | $CLAUDE_SESSION_ID\" >> ~/intuitek/logs/sessions.log"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Every session end is logged with a timestamp and session ID. Useful for debugging when cron-triggered headless runs complete.


Hooks in Headless Mode

Hooks fire in headless (-p) mode too. This is where they're most valuable for autonomous agents.

When Claude Code runs headless via cron:

cd ~/project && claude -p "process inbox files" --allowedTools "Bash(*),Read(*),Write(*)"
Enter fullscreen mode Exit fullscreen mode

Every tool call still triggers configured hooks. Your audit log gets written. Your formatter runs. Your notification fires. The agent is fully constrained and observable even without a human watching.


The Pattern That Makes Hooks Powerful

Hooks + CLAUDE.md + tool permissions form three independent enforcement layers:

  • CLAUDE.md — tells Claude what to do (reasoning-level, can be argued with)
  • Tool permissions — controls which tools are available (binary allow/deny)
  • Hooks — automates behavior around every tool call (guaranteed execution)

An agent relying only on CLAUDE.md instructions for safety has a reasoning dependency. Hooks eliminate that dependency for specific behaviors. Use CLAUDE.md to shape intent. Use hooks to enforce the behaviors that can't be left to intent.


Common Mistakes

Hooks that always exit 1 — PreToolUse hooks that unconditionally fail will block every tool call. Always exit 0 at the end unless you specifically want to block.

Slow hooks — Every tool call waits for the hook to complete. A hook that makes an API call or runs a slow script adds latency to every action. Keep hooks fast or run slow work in the background (your_slow_script.sh &).

Forgetting PostToolUse for Edit — If you auto-format Write but not Edit, Claude can bypass formatting by editing instead of writing from scratch. Apply formatting hooks to both.

Not testing hooks before deployment — Run claude -p "echo test" --allowedTools "Bash(*)" with your hooks active and verify the audit log or notification appears before committing the config.


Where to Start

If you're deploying Claude Code for anything serious, add the audit hook first. Before anything else. You want a timestamped log of every Bash command Claude runs — not because you expect problems, but because when something unexpected happens you'll want to know exactly what Claude did.

After that: auto-formatting, then blocking.

The rest follows naturally once you see how the event system works.


W. Kyle Million (K¹) — IntuiTek¹

Building autonomous AI infrastructure in Poplar Bluff, Missouri.

Tags: claudecode, devtools, aiagents, webdev

Top comments (0)