ServicesAboutNotesContact Get in touch →
EN FR
Note

Claude Code Hooks

How hooks give Claude Code deterministic guardrails — shell commands that execute at specific lifecycle points to enforce rules, auto-format code, and block dangerous operations

Planted
claude codeautomationdata engineering

Hooks are shell commands that execute at specific points in Claude Code’s lifecycle. Where CLAUDE.md provides guidance that Claude should follow, hooks provide deterministic control that Claude must obey. A hook that blocks edits to production schemas will block them every time, regardless of how convincing your prompt is.

CLAUDE.md is probabilistic — Claude follows it reliably, but complex prompts or long conversations can cause instructions to be deprioritized. Hooks are deterministic — shell commands that either pass or fail, with no room for LLM interpretation.

Hook Lifecycle Points

Claude Code exposes several lifecycle hooks, but two are most useful for analytics engineering workflows:

PreToolUse runs before Claude executes a tool (like editing a file, running a command, or writing code). This is your checkpoint — you can inspect what Claude is about to do and block it if it violates a rule.

PostToolUse runs after Claude finishes using a tool. This is your cleanup step — auto-format code, compile models, run linters, or trigger downstream checks.

Other lifecycle hooks exist (like PreToolCall for the initial request), but PreToolUse and PostToolUse cover 90% of practical use cases.

How Hooks Work

Hooks are configured in your project’s .claude/settings.json file. Each hook has:

  • A matcher that determines which tool activations trigger it (e.g., Edit|Write for file modifications, Bash for command execution)
  • A type (command for shell execution)
  • A command that receives the tool’s input via stdin as JSON

The hook’s exit code determines the outcome:

Exit CodeBehavior
0Hook passes, operation proceeds
2Hook blocks the operation, error message surfaces to Claude
Other non-zeroError is logged but operation continues

Exit code 2 is the critical one. When a hook returns 2, Claude sees the error and can adjust its approach. It doesn’t silently retry — it understands the operation was blocked and why.

Auto-Formatting SQL After Edits

One of the most practical hooks for dbt projects: automatically running sqlfluff after Claude edits any SQL file.

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read f; if echo \"$f\" | grep -q '\\.sql$'; then sqlfluff fix \"$f\" --dialect bigquery; fi; }"
}
]
}
]
}
}

This hook intercepts any file edit, checks if the file is SQL, and runs sqlfluff with BigQuery dialect if so. The result: every SQL file Claude touches gets automatically formatted to your project’s style rules. No need to prompt for formatting, no drift from your sqlfluff configuration.

The same pattern works for other formatters. You could run sqlfmt, prettier for YAML files, or any other tool that accepts a file path.

Blocking Edits to Protected Files

For dbt projects with production mart models that should only change through a formal review process:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 -c \"import json, sys; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path',''); sys.exit(2 if 'models/marts/prod/' in p else 0)\""
}
]
}
]
}
}

When Claude attempts to edit any file in models/marts/prod/, the hook returns exit code 2, blocking the operation. Claude sees the block and either asks for confirmation or works around it. This is a hard guardrail — no amount of prompting will bypass it.

You can extend this pattern to protect any critical path: seeds/ if seed data shouldn’t be modified by AI, macros/generate_schema_name.sql if your custom schema logic is carefully tuned, or entire directories owned by other teams.

Auto-Compiling After Model Changes

After Claude edits a dbt model, you might want to automatically compile it to catch syntax errors immediately:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read f; if echo \"$f\" | grep -q 'models/.*\\.sql$'; then dbt compile --select $(basename \"$f\" .sql) 2>&1 | tail -5; fi; }"
}
]
}
]
}
}

This catches issues like invalid ref() calls, syntax errors, and broken Jinja before Claude moves on to the next task. The compiled SQL output also gives Claude immediate feedback about what its changes produce.

Enforcing TDD Discipline

Hooks can enforce test-driven development by intercepting model file creation and checking whether tests exist first:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read f; if echo \"$f\" | grep -q 'models/.*\\.sql$'; then model=$(basename \"$f\" .sql); grep -rq \"$model\" models/ --include='*.yml' || exit 2; fi; }"
}
]
}
]
}
}

If Claude tries to create a new model SQL file and no corresponding YAML entry exists with tests defined, the hook blocks it. Claude then understands it needs to write the schema.yml tests first.

Practical Considerations

Keep hooks fast. Every PostToolUse hook runs after every tool invocation that matches. A hook that takes 10 seconds to run will slow down Claude’s entire workflow. sqlfluff on a single file is fast; running dbt build on a large graph is not.

Use matchers wisely. The Edit|Write matcher fires on every file edit, so your hook command needs to filter appropriately (checking file extension, path, etc.). Without that filter, you’d try to sqlfluff a Python file or a YAML file.

Combine with CLAUDE.md. Hooks enforce hard boundaries. CLAUDE.md provides soft guidance. Use hooks for rules that must never be broken, and CLAUDE.md for conventions that Claude should follow but where occasional deviation is acceptable.

Test hooks locally first. A hook with a bug (like an infinite loop or a command that always exits with code 2) will block all Claude Code operations. Test your hook commands manually with sample JSON input before adding them to settings.

When Hooks Are Overkill

Not everything needs a hook. If you just want Claude to prefer a certain SQL style, put it in CLAUDE.md. If you want Claude to run tests after implementation, just ask. Hooks are for the handful of rules where deterministic enforcement is worth the configuration overhead: protecting production files, enforcing formatting standards, blocking operations that could cause real damage.