You know the drill. Claude writes some Python code, it works eventually, but the formatting is all over the place. Or it creates a file but doesn’t run your linter. Or it makes changes without updating your pre-commit hooks. These aren’t deal-breakers, but they’re the kind of friction that adds up over a day of development work.

Anthropic recently released the ability to define hooks. Claude Code hooks are essentially shell commands that run automatically at specific points in Claude’s workflow. Think of them as your personal automation layer that ensures certain things always happen, rather than hoping Claude remembers to do them.

How many times have you committed instructions to Claude’s memory, but it wholesale ignores them?!

What Are Hooks, Really?

In practical terms, hooks are shell commands that execute when specific events occur in Claude Code:

  • PreToolUse: Before Claude runs a tool (perfect for validation or permission checks)
  • PostToolUse: After Claude completes a tool (ideal for formatting, linting, or cleanup)
  • Notification: When Claude sends you a notification
  • Stop: When Claude finishes responding

The best part is that these aren’t mere suggestions for Claude to consider, they’re guaranteed to run. If you want every Python file formatted with Black, it will happen. Every time.

A Real-World Example: Automatic Python Formatting

Let me walk you through setting up a hook that automatically formats Python code with Black. This is the kind of thing that seems trivial until you’re working on a project for hours and realising half your files are inconsistently formatted.

The Setup

First, you’ll need a formatting script. In this case, I’m keeping mine in .claude/scripts/ within the project, so it’s version-controlled and shared across the various machines on which I maintain this particular project:

#!/bin/bash
# .claude/scripts/format-python.sh

# Read JSON input from stdin
input_json=$(cat)

# Extract file path from the JSON input
file_path=$(echo "$input_json" | jq -r '.tool_input.file_path // empty')

# Check if we got a valid file path
if [[ -z "$file_path" ]]; then
    exit 0
fi

# Check if the file is a Python file
if [[ "$file_path" =~ \.py$ ]]; then
    # Check if black is installed
    if ! command -v black &> /dev/null; then
        echo "Warning: black is not installed. Install with: pip install black" >&2
        exit 1
    fi
    
    # Check if file exists
    if [[ ! -f "$file_path" ]]; then
        echo "Warning: File $file_path does not exist" >&2
        exit 1
    fi
    
    # Run black on the Python file
    if black "$file_path" --quiet; then
        echo "Formatted Python file: $file_path"
        exit 0
    else
        echo "Error: Failed to format $file_path with black" >&2
        exit 1
    fi
else
    exit 0
fi

Then you configure the hook in .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "./.claude/scripts/format-python.sh"
          }
        ]
      }
    ]
  }
}

Why This Approach Works

The hook receives JSON data about what Claude just did, including the exact file path. Using jq to parse this gives you surgical precision - you’re only formatting the specific file that was just modified, not scanning the entire filesystem or running formatters unnecessarily.

When I tested this setup, I asked Claude to create a deliberately poorly formatted Python file:

def test(x,y):
    if x>y:return x
    else:return y

The hook then automatically transformed it into proper Black-formatted code:

def test(x, y):
    if x > y:
        return x
    else:
        return y

No manual intervention required. No remembering to run Black afterwards. It just works.

The Bigger Picture

This example scratches the surface of what’s possible. You could set up hooks for:

  • Code quality: Running linters, type checkers, or security scanners
  • Notifications: Custom alerts when Claude modifies sensitive files
  • Logging: Tracking all commands for compliance or debugging
  • Permissions: Blocking modifications to production code or critical directories
  • Integration: Triggering CI/CD pipelines or updating documentation

The key insight is that hooks turn suggestions into guarantees. Instead of prompting Claude to “remember to format the code,” you encode that requirement at the application level.

Security Considerations

Hooks execute with your full user permissions, so they come with the usual security considerations. Review any hook commands carefully - you’re essentially giving them automatic execution rights.

Also, hooks use a 60-second timeout and run in parallel, so keep them focused and efficient. The formatting hook above typically completes in milliseconds, which is exactly what you want.

Getting Started

If you’re already using Claude Code, try the /hooks slash command to see the configuration interface. Start simple - maybe just logging the commands Claude runs - and build up from there.

In my experience, the most valuable hooks are the ones that eliminate small, repetitive tasks that you’d otherwise forget about until code review time. Automatic formatting is a perfect example, but there’s probably something specific to your workflow that would benefit from this approach.

The documentation is thorough, and the JSON input structure is well-designed for extracting exactly the information you need. It’s worth spending some time with hooks - they’re one of those features that becomes indispensable once you start using them properly.