Database Migration Safety Gate - Claude Code Hook
PreToolUse hook that inspects a database migration before it is written and blocks irreversible or lock-heavy operations — DROP TABLE/COLUMN, TRUNCATE, table/column RENAME, and CREATE INDEX without CONCURRENTLY — following the strong_migrations safe-migration checks. An explicit acknowledgement comment lets an intentional change through, so risky schema edits are a deliberate choice rather than an accident.
Open the source and read safety notes before installing.
Safety notes
- Runs before Write, Edit, and MultiEdit on files under a migrations path or with a .sql extension and reads the pending SQL from the tool input.
- Blocks the write with exit code 2 when it detects DROP TABLE/COLUMN, TRUNCATE, a table/column RENAME, or CREATE INDEX without CONCURRENTLY; the hook never connects to or runs SQL against any database.
- Provides an explicit escape hatch - add a '-- safety-gate: allow' comment to the migration to acknowledge the risk and let it through.
- Uses regex heuristics, so it can miss obfuscated statements or flag an intentional change; review every migration regardless of the result.
Privacy notes
- Reads only the pending migration content from the tool input; it opens no database connections and makes no network calls.
- Prints the file path and the names of the detected risky operation classes to local hook stderr; it writes no logs.
- The file path shown in output may reveal local directory structure in your terminal.
Prerequisites
- Claude Code CLI with hooks enabled.
- bash and jq on PATH; the hook fails open and stays silent when jq is missing.
Schema details
- Install type
- cli
- Troubleshooting
- No
- Scope
- Source repo
- Trigger
- PreToolUse
- Script language
- bash
Script body
#!/usr/bin/env bash
set -u
# Claude Code PreToolUse hook. Inspects a pending database migration and blocks
# irreversible or lock-heavy operations (the strong_migrations safe-migration
# check classes). Exit code 2 blocks the write and feeds the reason back to
# Claude. An explicit "-- safety-gate: allow" comment acknowledges the risk and
# lets the migration through. Fails open when jq is missing.
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
# Only gate files that look like database migrations.
case "$FILE" in
*migrations/*|*migrate/*|*/db/migrate/*|*.sql) : ;;
*) exit 0 ;;
esac
CONTENT=$(printf '%s' "$INPUT" | jq -r '
[ .tool_input.content,
.tool_input.new_string,
(.tool_input.edits[]?.new_string) ]
| map(select(. != null)) | join("\n")
')
[ -z "$CONTENT" ] && exit 0
# Escape hatch: an explicit acknowledgement lets an intentional change proceed.
if printf '%s' "$CONTENT" | grep -qiE 'safety-gate:[[:space:]]*allow|safety_assured|strong_migrations[[:space:]]*:[[:space:]]*disable'; then
exit 0
fi
findings=""
flag() {
if printf '%s' "$CONTENT" | grep -iqE "$2"; then
findings="${findings}${1}"$'\n'
fi
}
flag "DROP TABLE (irreversible data loss)" 'drop[[:space:]]+table'
flag "DROP COLUMN (irreversible data loss)" 'drop[[:space:]]+column'
flag "TRUNCATE (removes all rows)" '(^|[^a-z])truncate[[:space:]]'
flag "Table/column RENAME (breaks running code mid-deploy)" 'rename[[:space:]]+(table|column|to)[[:space:]]'
# CREATE INDEX without CONCURRENTLY locks writes on large tables.
if printf '%s' "$CONTENT" | grep -iE 'create[[:space:]]+(unique[[:space:]]+)?index' | grep -ivqE 'concurrently'; then
findings="${findings}CREATE INDEX without CONCURRENTLY (locks writes on large tables)"$'\n'
fi
if [ -n "$findings" ]; then
echo "Unsafe database migration in ${FILE:-pending write} blocked by the safety gate:" >&2
printf '%s' "$findings" | while IFS= read -r f; do
[ -n "$f" ] && echo " - $f" >&2
done
echo "Use a safe pattern (add columns nullable then backfill, create indexes CONCURRENTLY, expand-then-contract for renames) or add a '-- safety-gate: allow' comment to acknowledge the risk and proceed." >&2
exit 2
fi
exit 0Full copyable content
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/database-migration-safety-gate.sh"
}
]
}
]
}
}About this resource
Features
- Gates database migrations before they are written, blocking irreversible or lock-heavy operations rather than discovering them after a bad deploy.
- Detection classes follow the strong_migrations safe-migration checks:
DROP TABLE,DROP COLUMN,TRUNCATE, table/columnRENAME, andCREATE INDEXwithoutCONCURRENTLY. - Blocks with exit code
2and feeds the reason back to Claude, so the model can switch to a safe pattern instead of shipping the risky statement. - Includes an explicit escape hatch — a
-- safety-gate: allowcomment acknowledges the risk and lets an intentional change through. - Pure local
bash+jq; it never connects to a database, runs SQL, or makes a network call.
How it works
On PreToolUse, the hook checks whether the target path looks like a migration (a migrations/, migrate/, or db/migrate/ directory, or a .sql file). If so, it reads the pending SQL from the tool input and scans it for the unsafe operation classes above. Any match prints a blocked-reason list to stderr and exits 2, which stops the write — unless the content carries an acknowledgement comment.
Safe-pattern guidance it nudges toward
- Add columns nullable, backfill, then add the
NOT NULLconstraint in a later migration. - Create indexes with
CONCURRENTLYto avoid locking writes on large tables. - Use expand-then-contract for renames so running code never breaks mid-deploy.
- Treat
DROP/TRUNCATEas deliberate, acknowledged steps.
Use cases
- Stop an agent from generating a
DROP COLUMNor non-concurrent index migration during a refactor. - Enforce safe-migration discipline in a team without wiring a full migration linter into CI.
- Make irreversible schema changes an explicit, acknowledged decision.
Installation
- Create the hooks directory:
mkdir -p .claude/hooks - Create the hook file:
touch .claude/hooks/database-migration-safety-gate.sh - Paste the script body into that file and make it executable:
chmod +x .claude/hooks/database-migration-safety-gate.sh - Add the configuration below to
.claude/settings.json(project) or~/.claude/settings.json(user).
Requirements
- Claude Code CLI with hooks enabled
- bash and jq
Hook configuration
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/database-migration-safety-gate.sh"
}
]
}
]
}
}
Limitations
- Regex heuristics target common SQL phrasings; ORM-DSL migrations (for example
remove_columnin Rails or a KnexdropColumncall) and heavily obfuscated SQL may not match — extend the patterns for your toolchain. - Detecting
DELETE/UPDATEwithout aWHEREclause reliably requires real SQL parsing and is intentionally left out to avoid false positives.
Source and references
- strong_migrations safe-migration checks: https://github.com/ankane/strong_migrations#checks
- strong_migrations repository: https://github.com/ankane/strong_migrations
- Claude Code hooks documentation: https://docs.anthropic.com/en/docs/claude-code/hooks
Source citations
Signals
Loading live community signals…
A short, calm digest of reviewed Claude resources. Unsubscribe any time.