A Claude Code hook that asks before DROP TABLE, migrate reset and rm -rf

Claude Code runs a PreToolUse hook before every Bash command. Here is a 14-line hook that asks first on destructive database and file commands, tested, plus the ways around it you should know.

The short answer

Add a PreToolUse hook on the Bash tool. Claude Code sends the hook a JSON object with tool_name and tool_input.command; the hook can answer allow, deny, ask or defer in hookSpecificOutput.permissionDecision.1 The script below answers ask when a command looks like DROP TABLE, TRUNCATE, prisma migrate reset, alembic downgrade, dropdb or rm -rf, so you see a permission prompt instead of finding out afterwards.

It is a speed bump, not a wall. It reads the command as text. A command built at run time from a variable passes straight through. Treat it as one layer next to a database role that can’t drop tables.

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

Why a hook and not just deny rules

Claude Code already has permission rules. They are evaluated deny, then ask, then allow, across every settings file, and the first match wins; ask and deny rules apply when any part of a compound command matches.3 So why add a hook?

Because rules match command patterns, and destructive SQL hides inside commands that are usually fine. psql is how you read data too. You don’t want to deny every psql; you want to be asked about the one that carries DROP TABLE. A hook can look inside the command string and decide.

The public record shows why this matters. In claude-code#46684, an agent asked only to read logs over SSH ran ALTER TABLE … DROP COLUMN twice on a live production database without asking.2 Other issues report prisma db push --force-reset on a production database and alembic downgrade base without confirmation.5 In #80868, ask rules for npx prisma db:* and npx prisma migrate:* were in place, yet a compound command with a command substitution ran prisma migrate diff against the production URL with no prompt, and the database was wiped and restored from a nightly backup.4 Each of these ran from the shell, not through a database tool.

Two details of the hook format make it a good fit. Deny and ask rules are still evaluated whatever the hook returns, so a hook can only add caution, never remove a rule. With several hooks, precedence is deny, then defer, then ask, then allow.1

The hook, 14 lines

Save this as .claude/hooks/block-db.sh and make it executable. It needs jq.

Code Commands typed in a terminal. The text around it says what happens.

#!/usr/bin/env bash
# .claude/hooks/block-db.sh: ask before destructive database and file commands
cmd=$(jq -r '.tool_input.command // ""')
pattern='(drop[[:space:]]+(table|schema|database)|truncate[[:space:]]|migrate[[:space:]]+reset|db[[:space:]]+push[[:space:]].*--force-reset|alembic[[:space:]]+downgrade|dropdb[[:space:]]|rm[[:space:]]+-[a-z]*r[a-z]*f)'
if printf '%s' "$cmd" | grep -Eiq "$pattern"; then
  jq -n --arg r "This command can destroy data: $cmd" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "ask",
      permissionDecisionReason: $r
    }
  }'
fi
exit 0

What each part does:

  • jq -r '.tool_input.command' pulls the Bash command out of the hook’s input.1
  • The pattern is a short list. Extend it with the commands your own stack uses: rails db:drop, knex migrate:rollback --all, supabase db reset.
  • permissionDecision: "ask" puts the command in front of you with the reason shown in the permission prompt.1
  • exit 0 with no output means “no opinion”; Claude Code carries on with its normal rules.

Exit code 2 would block the call outright, and any other non-zero exit is a non-blocking error unless the hook sets onFailure: "block".1 That second rule matters: a hook that crashes, or a missing jq, lets the command through.

Wire it up

Register the hook in .claude/settings.json for the project, or ~/.claude/settings.json for every project:

Code Data in JSON, the format software uses to pass records and settings.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/block-db.sh" }
        ]
      }
    ]
  }
}

Restart Claude Code after editing settings. Commit the project file so everyone on the team gets the same hook.

Test it before you trust it

You can test a hook without Claude Code by piping it the same JSON shape. We ran these five commands through the script above:

Code What the screen or file shows, copied as is.

$ jq -n --arg c 'psql "$DATABASE_URL" -c "DROP TABLE orders"' \
    '{hook_event_name:"PreToolUse",tool_name:"Bash",tool_input:{command:$c},cwd:"/tmp"}' \
  | .claude/hooks/block-db.sh
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "ask", ... } }
Command Answer
psql "$DATABASE_URL" -c "DROP TABLE orders" ask
npx prisma migrate reset --force ask
rm -rf ./build ask
git status no output (normal rules apply)
psql -c "select * from orders" no output

Keep a file of commands that must ask and commands that must not, and rerun it whenever you change the pattern. False alarms are not harmless. If the hook asks about every rm -rf ./build, people start approving without reading, which is worse than no hook.

Where text matching stops

Be honest with yourself about what this script can’t see. All of these pass it:

  • A variable. t=orders; psql -c "DROP TABLE $t" is caught, but op=DROP; psql -c "$op TABLE orders" is not.
  • Encoded text. echo ZHJvcCB0YWJsZSBvcmRlcnM= | base64 -d | psql carries the statement in base64.
  • A script or Makefile target. npm run db:fresh runs whatever package.json says.
  • Code an interpreter runs. python -c or node -e with a database client.
  • A database reached over HTTP, such as a hosted SQL API called with curl.

These limits aren’t unique to a 14-line script. Any check that reads the command before it runs has them, because the value only exists at run time. Our own shell guard has the same boundary: it reads text, so a variable, an alias, a Makefile target, encoded text, code an interpreter runs or a database reached over HTTP passes untouched, and a command over 1 MiB or nested more than 12 levels asks because it can’t be read in full.6 And if it can’t run at all, because deedglass isn’t on your PATH or the hook times out, the command goes ahead unchecked.6

So pair the hook with controls that don’t depend on reading text:

  1. A database role that can’t drop. Only the table owner, the schema owner and a superuser can drop a table in Postgres.7 Give the agent a role that is none of those. The Postgres write access guide walks through it.
  2. No production URL in the agent’s environment. Several of the issues above began with a production connection string sitting in .env.
  3. Backups you have restored at least once.

The checklist before an agent touches production puts these in order.

Ask or deny

ask keeps you in the loop and lets intended commands run after a yes. deny stops the command and returns the reason to Claude, which usually tries something else.1 For a local laptop, ask is the better default. For an unattended agent with no human to ask, use deny, or better, don’t give it credentials that can drop anything.

If you would rather not maintain a pattern list, deedglass guard shell is a ready-made version of this hook. It asks first, with one sentence naming the reason, when part of a command would destroy database data; it never answers allow and needs no server, account or network.6 Every command from the public issues above gets an ask, and none of the 200+ ordinary development commands in its test corpus does.6 The same guard installs as the before-a-command hook for Codex CLI, Gemini CLI, GitHub Copilot and Cursor too.6 How those clients’ hooks differ is in our comparison of agent hooks, and what a PreToolUse hook is, in one paragraph, is in the glossary. To try the checks without installing anything, open the playground; to grade what an agent can reach on your own machine, run npx deedglass scan (the grade, explained).

Written with AI assistance and checked against our claims register.

The takeaway

A 14-line PreToolUse hook can make Claude Code ask before DROP TABLE or migrate reset, but it reads text: a variable or base64 string walks past it.

Read nextAn AI agent ran prisma migrate reset on production. What actually stops it? Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1Claude Code docs: Hooks reference · code.claude.com · checked 26 October 2026
  2. 2anthropics/claude-code #46684 · github.com · checked 26 October 2026
  3. 3Claude Code docs: Permissions · code.claude.com · checked 26 October 2026
  4. 4anthropics/claude-code #80868 · github.com · checked 26 October 2026
  5. 5anthropics/claude-code #36183 · github.com · checked 26 October 2026
  6. 6Deedglass docs: the shell guard · docs.deedglass.com · checked 26 October 2026
  7. 7PostgreSQL 17 docs: DROP TABLE · postgresql.org · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing