File of the day
A Claude Code hook that keeps the agent from wiping your database
Slava Sazhin 23 September 2026 2 min read 1 view
A PreToolUse hook that stops Claude Code before it runs DROP TABLE, TRUNCATE or DELETE without WHERE. One script, a minute to install.
A new series starts today, File of the day: one file from our own setup that you can put into yours the same day. A hook, a prompt, a block for your CLAUDE.md. A few paragraphs on what it does, then the file itself, in full, free to read and download.
What it does
Claude Code runs this script before every terminal command. If the command would destroy data in a database, the hook refuses it and tells Claude why, and Claude looks for another way. It stops:
DROP TABLE,TRUNCATE TABLEandDROP DATABASEALTER TABLEthat drops, renames or changes a column (ADD COLUMNpasses)DELETE FROMwithoutWHERE
Why a hook and not a line in CLAUDE.md
A rule in CLAUDE.md is a request: the model reads it and usually follows it. A hook runs before the command whatever the model decided, which is what you want when Claude works with permission prompts switched off and nobody is watching.
When you really do need to drop a table
Put CLAUDE_USER_APPROVED=1 in front of the command and it goes through. Add one line to your ~/.claude/CLAUDE.md so that Claude uses the prefix only after you say yes in the conversation; the install note below has the wording. The prefix is a speed bump rather than a lock: Claude can still drop a table, but only on purpose and in plain sight in the transcript. DELETE FROM without WHERE never goes through, prefix or not.
Install
You need jq and perl (on macOS, brew install jq). The script goes into ~/.claude/hooks/, is made executable and is registered under PreToolUse in ~/.claude/settings.json. Every step, and a one-line check that it works, is in the note below. Or hand both files to Claude and ask it to install them.
The files
#!/bin/bash
# PreToolUse hook: stops Claude from running a bash command that destroys data in a database.
# Blocks DROP TABLE, TRUNCATE TABLE, DROP DATABASE, destructive ALTER TABLE and DELETE FROM without WHERE.
# Allowed with the CLAUDE_USER_APPROVED=1 prefix in the command itself; DELETE FROM without WHERE never passes.
# Without jq the command cannot be read. The guard hook then never lets anything through silently:
# exit 2 blocks the call, and Claude sees the reason in stderr.
if ! command -v jq >/dev/null 2>&1; then
echo "BLOCKED: the no-data-deletion hook needs jq (brew install jq)" >&2
exit 2
fi
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Deny with a reason: the model gets permissionDecisionReason, understands why the command
# did not pass, and adjusts on its own.
hook_deny() {
jq -cn --arg r "${1}" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
exit 0
}
if [ -z "$COMMAND" ]; then
exit 0
fi
# bash removes the "backslash + newline" pair when reading, while grep matches one
# physical line. So the check runs on two forms at once: joined and raw.
JOINED=$(printf '%s' "$COMMAND" | perl -0pe 's/\\\n//g')
[ -z "$JOINED" ] && JOINED=$COMMAND
COMMAND="$JOINED
$COMMAND"
approved() {
echo "$COMMAND" | grep -q 'CLAUDE_USER_APPROVED=1'
}
# DROP TABLE / TRUNCATE TABLE
if echo "$COMMAND" | grep -qiE 'DROP\s+TABLE|TRUNCATE\s+TABLE'; then
approved || hook_deny "BLOCKED: DROP TABLE/TRUNCATE are forbidden without CLAUDE_USER_APPROVED=1 (data loss)"
fi
# DROP DATABASE
if echo "$COMMAND" | grep -qiE 'DROP\s+DATABASE'; then
approved || hook_deny "BLOCKED: DROP DATABASE is forbidden without CLAUDE_USER_APPROVED=1 (deletes the whole database)"
fi
# Destructive ALTER TABLE: DROP/RENAME/ALTER COLUMN. ADD COLUMN and ADD CONSTRAINT pass freely.
if echo "$COMMAND" | grep -qiE 'ALTER\s+TABLE'; then
if echo "$COMMAND" | grep -qiE '\b(DROP\s+COLUMN|DROP\s+CONSTRAINT|RENAME\s+(TO|COLUMN)|ALTER\s+COLUMN)\b'; then
approved || hook_deny "BLOCKED: destructive ALTER TABLE (DROP/RENAME/ALTER COLUMN) is forbidden without CLAUDE_USER_APPROVED=1. ADD COLUMN is allowed freely."
fi
fi
# DELETE FROM without WHERE. Uses perl because grep on macOS cannot look ahead.
if echo "$COMMAND" | perl -ne 'BEGIN{$f=0} $f=1 if /DELETE\s+FROM(?!\s+.*WHERE)/i; END{exit !$f}'; then
hook_deny "BLOCKED: DELETE FROM without WHERE is forbidden"
fi
exit 0
# How to install the hook against data deletion
The hook fires before every command Claude runs in the terminal and blocks commands that destroy data in a database: DROP TABLE, TRUNCATE TABLE, DROP DATABASE, ALTER TABLE with DROP/RENAME/ALTER COLUMN and DELETE FROM without WHERE.
What you need: `jq` and `perl`. macOS already has perl, jq is installed with `brew install jq`. Without jq the hook blocks every terminal command.
Where to put it:
```
mkdir -p ~/.claude/hooks
cp no-data-deletion-hook.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/no-data-deletion-hook.sh
```
What to add to `~/.claude/settings.json` (if the `hooks` section already exists, add the block inside `PreToolUse`):
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "$HOME/.claude/hooks/no-data-deletion-hook.sh" }
]
}
]
}
}
```
What to add yourself: a line in `~/.claude/CLAUDE.md` - "The CLAUDE_USER_APPROVED=1 prefix is added only after my direct permission in this conversation".
How to check that it works:
```
echo '{"tool_input":{"command":"psql -c \"DROP TABLE users\""}}' | ~/.claude/hooks/no-data-deletion-hook.sh
```
You will get a line with `"permissionDecision":"deny"`. Then, in a new Claude session, ask it to run `echo "DROP TABLE test"`: the command will not run, and Claude will show the reason for the block. The list of connected hooks is shown by the `/hooks` command.
Was this article worth your time?
Give Claude a computer of its own
Your own Linux machine with Claude Code on it, working around the clock while your laptop is shut. From €2.99 a month.