Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Automating Template Workflows with Core Hooks

Welcome back! In the last lesson, we built the "hands" for our reusable project template by creating a suite of custom slash commands. These commands allow us to scaffold new features like FastAPI endpoints and React components with a single instruction, all while respecting the architectural rules defined in our CLAUDE.md files.

Today, we're going to build the "reflexes" for our template. This lesson is about implementing core hooks to automate tasks like code formatting, linting, and pre-commit checks. By the end, your project will have an automated quality assurance system that runs in the background, keeping your codebase clean and consistent without any manual effort.

1. What Are Hooks? The Automated Reflex System

While slash commands are explicit instructions you give to Claude, hooks are automated scripts that trigger in response to specific events in Claude's workflow. Think of them as event listeners for your development process.

Claude Code Hooks Documentation in Terminal
As shown in the Claude Code interface, hooks are user-defined shell commands that execute at specific lifecycle events, enabling powerful automation.

Hooks are the key to enforcing project standards deterministically. Instead of just telling Claude to use a certain code style in CLAUDE.md, you can create a hook that automatically applies that style after every file edit.

To get a quick and dynamic overview of what hooks can do, let's start with a short video.

How Claude Code Hooks Save Me HOURS Daily

The video 'How Claude Code Hooks Save Me HOURS Daily' from Better Stack provides an excellent, fast-paced introduction to the power and flexibility of hooks.

Watch the first 41 seconds (00:00 - 00:41) for an overview of hook types. Then, watch the section from 01:04 to 01:50, which introduces the crucial concepts of hook inputs (JSON) and using exit codes to influence Claude's behavior.

2. The Hook Lifecycle and Configuration

As you saw, hooks are powerful because they can intercept Claude's actions. The two most important events for our purposes are:

  • PostToolUse: Fires after a tool (like writing a file) has completed. This is perfect for reactive tasks like formatting code, running tests after a change, or logging an action.
  • PreToolUse: Fires before a tool executes. This is ideal for preventative tasks like validation, security checks, or blocking disallowed actions.

To understand these concepts in more detail, let's turn to a clear, well-structured guide.

Claude Code Hooks: A Practical Guide to Workflow Automation

The DataCamp tutorial 'Claude Code Hooks: A Practical Guide to Workflow Automation' is a fantastic resource that breaks down how hooks work. We'll use it as our foundational text for this section.

Please read the following three sections: 'Getting Started With Claude Code Hooks' (focus on the event table and matchers). 'Working With Hook Inputs' (understand the JSON structure and how your scripts receive data). 'Working With Hook Outputs' (this is crucial; focus on the meaning of exit codes 0 and 2).\nThis will take about 15 minutes and will give you all the necessary theory to build our hooks.

To summarize the key takeaways from the reading:

  • Hooks are defined in .claude/settings.json.
  • They trigger on an event (e.g., PostToolUse) and are filtered by a matcher (e.g., Write|Edit).
  • Your hook script receives context as a JSON object via stdin.
  • Your script communicates back via stdout (for logs), stderr (for errors), and exit codes (to control the workflow). An exit code of 2 is particularly important as it blocks the action and sends the stderr message to Claude for correction.

3. Implementation 1: The Auto-Formatter (PostToolUse)

Let's start with a classic use case: automatically formatting code. Our goal is to create a PostToolUse hook that runs black on Python files and prettier on frontend files immediately after Claude creates or edits them.

Step 1: Create the Hook Script

First, let's create a directory to hold our hook scripts: mkdir -p .claude/hooks.

Now, create a new file named .claude/hooks/auto-format.sh. This script will be the "brain" of our hook.




#!/bin/bash




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




# Use jq to extract the file_path
# It checks tool_input.file_path (for Write) and tool_input.path (for Edit)
file_path=$(echo "$input_json" | jq -r '.tool_input.file_path // .tool_input.path // empty')




# If no file_path, exit silently
if [ -z "$file_path" ]; then
    exit 0
fi




# Determine which formatter to run based on file extension
if [[ "$file_path" == *.py ]]; then
    echo "Running black on $file_path"



    # Assuming black is installed in your project's environment
    black "$file_path"
    echo "Formatting complete."
elif [[ "$file_path" == *.ts || "$file_path" == *.tsx || "$file_path" == *.js || "$file_path" == *.css ]]; then
    echo "Running prettier on $file_path"



    # Assuming prettier is a dev dependency in your frontend/package.json
    # We run it from the root, so specify the path to prettier config
    npx prettier --write "$file_path" --config ./frontend/.prettierrc
    echo "Formatting complete."
fi




# Exit with success
exit 0

Make the script executable by running chmod +x .claude/hooks/auto-format.sh.

Step 2: Configure settings.json

Now, open or create .claude/settings.json and add the hook configuration.

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

That's it! After restarting Claude Code, every time it writes or edits a file, this hook will fire, check the file extension, and apply the correct formatter.

4. Implementation 2: The Pre-Commit Quality Gate (PreToolUse)

Now for a more advanced hook. We'll create a PreToolUse hook that simulates a git pre-commit check. Before Claude is allowed to execute a git commit command, this hook will run linters (ruff for Python, eslint for the frontend). If linting fails, the hook will block the commit and feed the errors back to Claude.

This is an incredibly powerful pattern for enforcing quality.

Step 1: Create the Hook Script

Create a new file named .claude/hooks/pre-commit-check.sh.




#!/bin/bash




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




# Extract the bash command being run
command=$(echo "$input_json" | jq -r '.tool_input.command // empty')




# Check if the command is 'git commit'
if [[ "$command" == git\ commit* ]]; then
    echo "Pre-commit check triggered for: $command"




    # --- Run Backend Linter (Ruff) ---
    echo "Running Ruff linter on backend..."



    # Assuming ruff is installed in your project's environment
    ruff_output=$(ruff check backend/app)
    ruff_exit_code=$?




    # --- Run Frontend Linter (ESLint) ---
    echo "Running ESLint on frontend..."



    # Assuming eslint is in frontend dev dependencies
    eslint_output=$(npx eslint 'frontend/src/**/*.{ts,tsx}')
    eslint_exit_code=$?

    if [ $ruff_exit_code -ne 0 ] || [ $eslint_exit_code -ne 0 ]; then
        echo "Commit blocked. Linting failed." >&2
        
        if [ $ruff_exit_code -ne 0 ]; then
            echo -e "\n--- Backend Issues (Ruff) ---\n$ruff_output" >&2
        fi

        if [ $eslint_exit_code -ne 0 ]; then
            echo -e "\n--- Frontend Issues (ESLint) ---\n$eslint_output" >&2
        fi
        
        echo -e "\nPlease fix the linting issues before committing." >&2
        



        # Exit with code 2 to block the command and send stderr to Claude
        exit 2
    else
        echo "All checks passed. Proceeding with commit."
        exit 0
    fi
fi




# If not a git commit command, exit with success and do nothing
exit 0

Again, make it executable: chmod +x .claude/hooks/pre-commit-check.sh.

Step 2: Update settings.json

Add the new PreToolUse hook to your settings.json. The file should now look like this:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/auto-format.sh"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/pre-commit-check.sh"
          }
        ]
      }
    ]
  }
}

5. Your Turn: Test Your Automated Guardian

Now it's time to see your automated reflexes in action.

  1. Restart Claude Code to load the new hook configurations.
  2. Test the Auto-Formatter (PostToolUse):
    • Ask Claude: /edit backend/app/main.py
    • In the prompt, tell it to add a new line but intentionally mess up the formatting (e.g., use weird indentation).
    • When Claude applies the edit, open the file (/open backend/app/main.py) and verify that black has automatically fixed the formatting.
  3. Test the Quality Gate (PreToolUse):
    • First, intentionally introduce a linting error. For example, ask Claude to edit a Python file and add an unused import (import os).
    • Next, ask Claude: Run a git commit with the message "add test feature"
    • Observe the result. The PreToolUse hook should fire. You will see a "Bash hook error" message. The commit will be blocked, and the error output from ruff will be displayed in the transcript and fed to Claude.
    • Now, you can simply tell Claude: "Okay, fix that linting error and try the commit again."

This workflow, where deterministic hooks catch errors and Claude intelligently fixes them, is a cornerstone of advanced AI-assisted development.

Conclusion

In this lesson, you've added a powerful automation layer to your project template. You've gone beyond merely instructing Claude and have started building a self-correcting system that enforces quality standards automatically.

Key Takeaways:

  • Hooks provide automated "reflexes": They trigger scripts based on Claude's actions, enabling deterministic control over your workflow.
  • PostToolUse for Reaction: Ideal for tasks like code formatting that should happen after an action is complete.
  • PreToolUse for Prevention: Perfect for creating quality gates, security checks, or validation that must run before an action is allowed.
  • Exit Code 2 is a Superpower: By exiting with code 2 and writing to stderr, your hooks can block actions and provide actionable feedback directly to Claude, enabling it to self-correct.
  • Version Control is Key: Your hook scripts and settings.json are part of your project's DNA. They should be committed to your repository so the entire team benefits from the same automated standards.

Next Up:

You have now established a "brain" (CLAUDE.md), "hands" (slash commands), and "reflexes" (hooks). Your project template is becoming a sophisticated development environment. In the next module, Advanced Prompting & Large-Scale Refactoring, we will leverage this robust setup to tackle much larger challenges. You'll learn how to guide Claude through complex, cross-file refactoring tasks, such as converting a synchronous FastAPI module to be fully asynchronous.

Can't find a good explanation? Sign up and we'll make it for you

Sign up