How to Let AI Query Your Database Without Deleting It: Read-Only Guardrails for Claude Code

AI, Security, and Tips & Tricks

How to Let AI Query Your Database Without Deleting It: Read-Only Guardrails for Claude Code

A story made the rounds recently that put a chill down the spine of every developer experimenting with AI coding tools: someone gave an AI assistant live access to a production database, asked it to help clean things up, and watched it issue destructive commands that wiped real data. The specifics vary depending on who's telling it, but the shape of the story is always the same — an autonomous agent, a real connection string, write permissions, and no seatbelt.

Here's the uncomfortable truth: the AI wasn't malfunctioning. It did exactly what it was designed to do — generate and execute a plausible-looking command to accomplish the goal it was given. The failure wasn't the model. The failure was the access. We handed a probabilistic text generator the same keys we'd hesitate to give a brand-new junior engineer on their first day, and then acted surprised when it used them.

The good news is that this is an entirely solvable problem, and the solution isn't never let AI touch your database. AI is genuinely useful for exploring schemas, drafting queries, and debugging slow reports. The fix is to make destructive actions impossible, not just discouraged. This guide walks through the concrete guardrails — real GRANT statements, MCP-layer flags, sandbox patterns, and connection scoping — that let an AI query your database all day long without ever being able to delete it.


Why write access to prod goes wrong

Before the recipe, it's worth being precise about the failure mode, because the AI went rogue is the wrong mental model and leads to the wrong fixes.

When you connect an AI coding assistant to a database — usually through a Model Context Protocol server that bridges the model and your data — the model doesn't understand your data the way you do. It pattern-matches. Ask it to remove the test accounts and, if the connection it's been handed can run DELETE and DROP, it may generate a statement that's syntactically valid, semantically reasonable-looking, and catastrophically wrong: a missing WHERE clause, a TRUNCATE on the wrong table, a schema-altering migration it invented on the spot.

Three factors compound the risk:

  • Autonomy. Agentic tools chain actions together. A single prompt can trigger a sequence of tool calls with no human confirming each SQL statement before it runs.
  • Confidence without comprehension. The model has no model of this data is irreplaceable. It weighs a DROP TABLE the same way it weighs a SELECT.
  • Overprivileged connections. Most developers connect using their personal credentials or the app's connection string — both of which typically carry full read/write/DDL rights.

Fix the third factor and the first two stop mattering. If the connection physically cannot execute a DELETE, it does not matter how confidently the AI generates one. That's the principle everything below is built on.

The read-only principle: dedicated roles, not shared credentials

The single highest-leverage guardrail is also the oldest one in the database security playbook: give the AI its own database role, and grant that role only SELECT. No INSERT, no UPDATE, no DELETE, no DROP, no DDL. If the tool's only job is to read and analyze, it never needs anything else.

This is defense the database itself enforces. It doesn't rely on the AI behaving well, on a system prompt being obeyed, or on a middleware layer catching a bad command. The permission simply isn't there.

Here's the recipe for each of the big three.

PostgreSQL — create a login role, grant connect and read, and (critically) set default privileges so future tables are covered too:

-- 1. Create a dedicated, least-privilege login role
CREATE ROLE ai_readonly WITH LOGIN PASSWORD 'use-a-strong-secret-from-your-vault';

-- 2. Allow it to connect and read the schema
GRANT CONNECT ON DATABASE myapp TO ai_readonly;
GRANT USAGE ON SCHEMA public TO ai_readonly;

-- 3. Grant SELECT on all *existing* tables
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_readonly;

-- 4. Make sure tables created *later* are also readable (this is the step people forget)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO ai_readonly;

The ALTER DEFAULT PRIVILEGES line matters: without it, the AI role silently loses visibility every time you add a table, and someone fixes it later by granting something broader. Lock it down once, correctly.

MySQL / MariaDB — the same idea, scoped to SELECT only:

CREATE USER 'ai_readonly'@'%' IDENTIFIED BY 'use-a-strong-secret-from-your-vault';
GRANT SELECT ON myapp.* TO 'ai_readonly'@'%';
FLUSH PRIVILEGES;

Because GRANT SELECT ON myapp.* is scoped at the database level, it automatically applies to tables you create later — no default-privileges dance required. Tighten the host portion ('ai_readonly'@'10.0.%') to your network rather than '%' wherever you can.

SQL Server — this one is almost too easy, because it ships with a built-in role for exactly this purpose:

CREATE LOGIN ai_readonly WITH PASSWORD = 'use-a-strong-secret-from-your-vault';
CREATE USER ai_readonly FOR LOGIN ai_readonly;

-- db_datareader = SELECT on every current and future table in the database
ALTER ROLE db_datareader ADD MEMBER ai_readonly;

db_datareader grants SELECT on all user tables and views, current and future, and nothing else. It's the SQL Server answer to read everything, change nothing.

Whatever engine you're on, verify the guardrail the fun way: connect as the new role and try to break something. Run a DELETE or a DROP. You want a permission-denied error. If you get one, the AI will too — and no prompt injection, no hallucinated migration, no missing WHERE clause can get past it.


Setting up a least-privilege role is a one-time job that pays off every time an AI — or a script, or a new hire — touches your data. If you're already thinking about who can change what and when, that same discipline belongs in your deployment process: DeployHQ's build pipelines let you gate exactly which commands run against an environment, so schema changes go through review instead of an ad-hoc terminal session. More on that below.


Guardrails at the MCP layer

The read-only role is your backstop. The MCP server is your second line of defense, and it's worth configuring even when the role is already locked down — belt and suspenders.

Most AI database access flows through an MCP server that translates the model's requests into real queries. If you're new to the pattern, our complete guide to building an MCP server covers how these bridges work, and our roundup of the best MCP servers for web developers covers the ecosystem. For databases specifically, DBHub is a common choice, and it exposes two flags that matter here:

  • --readonly — DBHub refuses to execute any query that isn't a read. Even if the AI generates a DELETE, the server rejects it before it reaches the database. This is a software-layer mirror of your read-only role, and having both means a misconfiguration in one doesn't expose you.
  • --max-rows — caps the number of rows any single query can return. This defends against a different failure: not destruction, but a runaway SELECT * FROM events on a billion-row table that pins your database's CPU and memory. Set a sane ceiling (a few thousand rows) so an over-eager query degrades gracefully instead of taking prod down.

A typical hardened DBHub invocation looks like this:

dbhub --transport stdio \
  --dsn "postgres://ai_readonly:...@db-host:5432/myapp?sslmode=require" \
  --readonly \
  --max-rows 5000

Note the DSN uses the ai_readonly role from the previous section, not your admin credentials. The flags and the role reinforce each other. (DBHub's flags are documented in the DBHub repository — worth reading before you wire it up.)

If you're running this inside Claude Code, the Claude Code cheatsheet is a handy reference for the CLI flags and permission settings you'll touch while setting this up.

The sandbox pattern: give it a copy it can't hurt

Read-only access covers the vast majority of what you'd actually want an AI to do with a database — explore, analyze, draft queries. But sometimes you genuinely need it to write: test a migration, populate seed data, validate that a DELETE does what you expect. For anything destructive, the rule is simple: never do it against a database you care about.

The pattern is a disposable copy:

  1. Snapshot or restore a copy of the relevant data into a throwaway database — a local container, an ephemeral schema, or a restored backup on a scratch instance.
  2. Point the AI at the copy. Give it a full read/write role on that copy only. Now it can DELETE, TRUNCATE, and ALTER to its heart's content.
  3. Throw it away when you're done. Nothing the AI did touched production.

A Docker-based scratch database is the fastest version of this:

# Spin up a disposable Postgres the AI can freely mutate
docker run --rm -d --name ai-sandbox \
  -e POSTGRES_PASSWORD=sandbox \
  -p 5433:5432 postgres:16

# Load a sanitized dump into it
pg_restore -h localhost -p 5433 -U postgres -d postgres ./sanitized-dump.dump

Two things make this safe rather than just convenient. First, the --rm flag means the container — and everything the AI did to it — evaporates when you stop it. Second, and non-negotiable for anything derived from production: sanitize the dump. Strip or mask PII before it lands anywhere an AI (and, via the model provider, potentially a third party) can read it. A sandbox that leaks real customer data isn't a safe sandbox.

A file-based engine makes this pattern even cheaper: with a local SQLite database, a disposable copy is a single cp command — you experiment where undo is free, then promote.

Connection scoping: SSH tunnels, TLS, and env-not-config

Even a perfectly permissioned read-only role is a liability if the credentials leak or the connection is exposed. Scope the connection itself.

Don't expose the database to the internet. The AI tool should reach your database over an SSH tunnel or a private network, not a publicly routable port. An SSH tunnel forwards a local port through an authenticated, encrypted channel:

# Forward local :5432 to the DB, reachable only via the bastion host
ssh -L 5432:db-internal:5432 deploy@bastion.example.com -N

Then point the AI at localhost:5432. The database's real port never faces the public internet. If you're already comfortable running deploy commands over SSH, this will feel familiar — our SSH commands reference covers the connection patterns.

Require TLS. Encrypt the connection so credentials and query results can't be sniffed in transit. In Postgres that's ?sslmode=require (or stricter — verify-full — if you've got the CA set up) on the DSN; MySQL and SQL Server have equivalents.

Keep credentials in the environment, not in config files. This is where a lot of otherwise-careful setups fall down. A connection string with a password committed to a mcp.json, a .env that's accidentally tracked, or a config file synced to a dotfiles repo is a credential leak waiting to happen. Reference secrets from environment variables or a secrets manager, and make sure the config that does get committed contains only variable references:

// good: no secret in the file
{ "dsn": "${DATABASE_URL_READONLY}" }

If you're setting up Claude Code for the first time, our getting-started guide for the terminal AI assistant walks through where its config lives and how to keep secrets out of it.


Guardrails are worth setting up once and reusing everywhere. If you'd rather your infrastructure enforce these boundaries by default — scoped credentials, encrypted connections, reviewed changes — that's exactly the discipline DeployHQ's deployment features are built around. Wire your database changes into a pipeline instead of a prompt.


Where deploys fit: schema changes belong in a pipeline

Here's the part most AI database safety advice skips entirely. Once you've locked the AI down to read-only, a fair question is: how do the actual, intended changes get made? Schema migrations, new indexes, column additions — those still need to happen.

The answer is emphatically not temporarily grant the AI write access and let it run the migration over SSH. That reintroduces every risk you just eliminated, plus a new one: an unreviewed, unlogged, unrepeatable change to production with no rollback path.

Schema changes belong in a reviewed build pipeline, the same way application code does. The migration is written (with the AI's help, drafting against a sandbox is great) and committed to version control. It's reviewed by a human. It runs as a deploy step, in order, with the change logged and the previous state recoverable. This is exactly the model in our guide to database migration strategies for zero-downtime deployments, and the broader practice of making database deployments repeatable and reversible.

The contrast is stark:

Ad-hoc AI SSH session Reviewed deploy pipeline
Runs immediately, no review Human reviews before it runs
No record of what changed Every change logged
No rollback One-click rollback to the prior state
Runs as an over-privileged user Runs as a scoped deploy identity
Not repeatable across environments Same migration, staging then prod

When a schema change is a version-controlled, reviewed deploy step, the fact that an AI helped write it is no longer scary — because the AI never executed it against prod. A human did, through a system built to make that action safe, observable, and reversible. Rolling migrations into a broader zero-downtime deployment strategy means even the intended changes never take the site down.

This is the whole philosophy in one line: let AI draft, let a pipeline deploy.

The copy-paste safety checklist

Print this, pin it, run through it before you connect any AI tool to any database that has data you'd miss.

AI + DATABASE ACCESS — SAFETY CHECKLIST

ROLE
[ ] AI connects via a DEDICATED database role, not personal or app credentials
[ ] That role has SELECT only — no INSERT/UPDATE/DELETE/DROP/DDL
[ ] Postgres: ALTER DEFAULT PRIVILEGES set so future tables stay read-only
[ ] SQL Server: role added to db_datareader (nothing broader)
[ ] Verified by connecting as the role and confirming DELETE/DROP is denied

MCP LAYER
[ ] MCP server (e.g. DBHub) launched with --readonly
[ ] --max-rows set to a sane ceiling to prevent runaway result sets
[ ] DSN uses the read-only role, never admin credentials

DESTRUCTIVE WORK
[ ] Any write/migration testing happens on a DISPOSABLE copy, never prod
[ ] Copies are PII-sanitized before the AI can read them

CONNECTION
[ ] Database is NOT exposed on a public port (SSH tunnel or private network)
[ ] TLS/SSL required on the connection (sslmode=require or stricter)
[ ] Credentials live in env vars / secrets manager, NOT in committed config

DEPLOYS
[ ] Schema changes go through a reviewed, version-controlled pipeline
[ ] No temporary write grants for "just this one migration"
[ ] Every change is logged and has a rollback path

Work down that list and the viral horror story simply can't happen to you. Not because your AI is smarter or better-behaved than the one in the story — but because you removed its ability to do harm in the first place. That's what security by design looks like: the safe path is the only path available.

This is the first article in our series on working safely with AI and databases, with companion deep-dives on MySQL-specific connection gotchas and a disposable-file SQLite workflow to follow.


Ready to move database changes out of ad-hoc terminal sessions and into a reviewed, reversible pipeline? Start deploying with DeployHQ for free and give your schema changes the same guardrails you just gave your AI.


Questions or want to share how you've locked down AI database access? Email us at support@deployhq.com or find us on X at @deployhq.