Aug 05, 2025 claude-code automation hooks development

Demystifying Claude Code Hooks

Claude Code hooks are incredibly powerful but can be frustratingly opaque when you’re trying to actually implement them. Even though I’ve been writing software for 20 years, I found the official documentation to be confusing—it didn’t lay out a few important concepts clearly enough for me. This post aims to bridge that gap by walking through a practical example: running the Rubocop linter and RSpec on a Rails project before committing changes.

Where Hooks Live

The first concept to understand is where your hooks configuration goes. You have two options:

  1. Global settings: ~/.claude/settings.json - Use sparingly for universal behaviors
  2. Project settings: .claude/settings.json - Project-specific automation

Project settings are ideal for commands like “run a linter on any changed *.rb files before committing.” They travel with your repository and ensure every team member gets the same automated checks.

Catching All File Modifications

One of the most common use cases is running checks whenever Claude modifies files. The hook events documentation lists several file-related events, but to catch all file modifications, you need to combine three events using regex syntax:

"Edit|MultiEdit|Write"

This pattern matches any tool that modifies files, ensuring your hooks run regardless of which specific editing operation Claude performs.

Organizing Your Hook Scripts

Here’s a practical tip that will save you headaches: create a dedicated directory for your hook scripts. I like adding a claude-hooks directory to the root of my project. Without this organization, you’ll end up embedding all your hook logic into single-line bash commands, which quickly becomes unmaintainable.

Building a Pre-commit Hook

Let’s walk through creating a realistic hook that runs Rubocop and Rspec before any git commit. First, create your hook script:

claude-hooks/precommit.sh:

#!/usr/bin/env bash

bin/rubocop --autocorrect && bundle exec rake

This script runs Rubocop with auto-correction, and only proceeds to run tests if the linter passes.

Next, wire it up in your .claude/settings.json:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_TOOL_INPUT\" | jq -r '.command' | grep -q '^git commit'; then echo 'Running pre-commit checks...'; ./claude-hooks/precommit.sh; fi",
            "timeout": 180
          }
        ]
      }
    ]
  }
}

Dissecting the Hook Command

Let’s break down what this command actually does:

  1. echo "$CLAUDE_TOOL_INPUT" - Outputs the JSON object containing Claude’s intended tool execution
  2. jq -r '.command' - Extracts the command field from the JSON using jq (the -r flag outputs raw strings without JSON quotes)
  3. grep -q '^git commit' - Checks if the command starts with “git commit” (the -q flag suppresses output, we only care about the exit code)
  4. if...then - Only runs our pre-commit script if this is actually a git commit command

The $CLAUDE_TOOL_INPUT environment variable contains a JSON object with details about the tool Claude is about to execute. For bash commands, this includes the exact command string. You can find examples that show much of the schema in the HookInput documentation.

Debugging Your Hooks

When developing hooks, this debugging snippet is invaluable:

"PostToolUse": [
  {
    "matcher": "Edit|MultiEdit|Write",
    "hooks": [
      {
        "type": "command",
        "command": "cat > foo.json"
      }
    ]
  }
]

This hook captures the JSON output from any file modification and dumps it to foo.json, letting you inspect exactly what data Claude provides to your hooks.

Managing Your Hooks

You can review and manage your configured hooks using the /hooks slash command in the Claude CLI. This command provides an interactive interface for viewing, editing, and testing your hook configurations. The slash commands documentation covers this and other useful commands.

Final Thoughts

Hooks transform Claude Code from a helpful assistant into a development workflow orchestrator. Once you understand the three key concepts—where to place your configuration, how to match events, and how to organize your scripts—you can automate virtually any aspect of your development process. The ability to intercept and validate Claude’s actions before they execute gives you unprecedented control over your coding environment.

The real power isn’t just in automation, but in creating consistent, repeatable development practices that travel with your codebase. Your entire team benefits from the same quality checks and automation, regardless of whether they’re using Claude Code directly or just working in the repository.