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:
- Global settings:
~/.claude/settings.json
- Use sparingly for universal behaviors - 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:
echo "$CLAUDE_TOOL_INPUT"
- Outputs the JSON object containing Claude’s intended tool executionjq -r '.command'
- Extracts thecommand
field from the JSON using jq (the-r
flag outputs raw strings without JSON quotes)grep -q '^git commit'
- Checks if the command starts with “git commit” (the-q
flag suppresses output, we only care about the exit code)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.