Claude Code Hooks can block dangerous dbt CLI commands before they execute. The existing hooks note covers blocking file edits to protected paths — this note covers blocking dangerous dbt CLI commands that target production, such as running against the wrong target, triggering --full-refresh on a long-rebuilding table, or running dbt build without a selector.
The key mechanism is a PreToolUse hook that inspects the command Claude is about to run. If it matches a dangerous pattern, the hook exits with code 2, which blocks execution and surfaces an error message to Claude. Exit 0 lets the command proceed. Any other exit code logs a warning but doesn’t block — a distinction that catches people off guard. See Claude Code Hooks for the full exit code behavior.
The Safety Script
Create .claude/hooks/dbt-safety.sh:
#!/usr/bin/env bashset -euo pipefail
input=$(cat)command=$(echo "$input" | jq -r '.tool_input.command // empty')
# Is this a dbt command targeting production?if echo "$command" | grep -q "dbt" && echo "$command" | grep -q "\-\-target.*prod"; then
# Block full-refresh on production if echo "$command" | grep -q "\-\-full-refresh"; then echo "Blocked: --full-refresh on production. Run this manually if you're sure." >&2 exit 2 fi
# Block run/build without explicit selectors if echo "$command" | grep -qE "dbt (run|build)" && ! echo "$command" | grep -qE "\-\-(select|models)"; then echo "Blocked: dbt run on production needs explicit --select" >&2 exit 2 fifi
exit 0The hook reads the tool input from stdin as JSON, extracts the command string, and checks two patterns:
-
--full-refreshon production — Always blocked. A full refresh rebuilds the table from scratch, dropping all existing data first. On a large production table, this means hours of rebuild time and potentially missing data while the rebuild runs. If you actually need a full refresh on prod, run it manually outside Claude Code. -
Unscoped
dbt runordbt buildon production — Blocked unless--selector--modelsis specified. Without a selector, dbt runs everything in the project. On a production target, that’s a dangerous default.
Registering the Hook
The configuration goes in .claude/settings.json:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/dbt-safety.sh" } ] } ] }}The Bash matcher triggers the hook on every bash command Claude tries to run. The hook itself does the filtering — it only inspects commands that contain dbt and --target.*prod. Everything else passes through at exit 0.
Note that matchers are case-sensitive. bash won’t match Bash. Check the exact tool names in Claude’s output if your hooks aren’t firing.
Extending the Pattern
The basic script covers the two most common production mistakes, but you can extend it for your project’s specific risks:
Block dbt seed on production. Seeds replace table contents entirely. On production, this can overwrite lookup tables that other models depend on:
if echo "$command" | grep -qE "dbt seed" && echo "$command" | grep -q "\-\-target.*prod"; then echo "Blocked: dbt seed on production. Seeds replace entire tables." >&2 exit 2fiBlock dbt run-operation on production. Custom operations (like drop statements in macros) can be destructive:
if echo "$command" | grep -qE "dbt run-operation" && echo "$command" | grep -q "\-\-target.*prod"; then echo "Blocked: dbt run-operation on production. Review the operation manually." >&2 exit 2fiRequire --defer on production builds. If your workflow expects production builds to use --defer with a production manifest (so unchanged models aren’t rebuilt), you can enforce that:
if echo "$command" | grep -qE "dbt (run|build)" && echo "$command" | grep -q "\-\-target.*prod"; then if ! echo "$command" | grep -q "\-\-defer"; then echo "Blocked: production builds require --defer --state ./prod-manifest" >&2 exit 2 fifiWhy Not Just Use CLAUDE.md?
You could add “never run —full-refresh on production” to your CLAUDE.md. Claude would follow that instruction most of the time. But CLAUDE.md is probabilistic — a complex conversation, a long session, or a particularly persuasive prompt can cause instructions to be deprioritized.
Hooks are deterministic. The shell script either blocks the command (exit 2) or allows it (exit 0). There’s no room for LLM interpretation. For operations where the cost of a mistake is measured in hours of downtime or corrupted production data, deterministic enforcement is worth the setup overhead.
The practical approach: put the rule in both places. CLAUDE.md prevents Claude from trying the dangerous command in the first place. The hook catches it if Claude tries anyway. Defense in depth.
Version Control Your Safety Hooks
Put .claude/hooks/ and .claude/settings.json in git. When you onboard a new team member, they get all your production guardrails automatically. When someone on the team encounters a new dangerous pattern, they add a check to the shared safety script and everyone benefits on the next pull.
This pattern is an application of the layered review approach: catching dangerous operations at the earliest point in the workflow.