Setting Up Claude Code With Safe Production Database Access

There's a workflow I get asked about all the time and it goes roughly: "I want Claude Code to be able to answer questions about my production database, but I don't want to give it my prod URL." This is the right instinct. This post is the ten-minute version of how to actually do it.
We'll use QueryBear because that's what I built. The shape would be similar with any MCP gateway that has read-only enforcement and an allowlist.
The thing we are not going to do
Just to be explicit, here's the bad version of this setup:
# don't do this
export DATABASE_URL=postgresql://prod_user:prod_pass@prod-host/prod_db
claude codeThe agent now has the same blast radius as prod_user. If the agent writes a destructive query — even by mistake, even via a CTE the agent didn't realize was writable — it runs. If the agent reads a column with PII, that PII goes into Claude's context window and from there into your terminal scrollback, possibly your shell history, possibly a screenshot you take to share with someone. Don't do this.
Read-only Postgres roles are better but, as I've written elsewhere, not enough. We want a real gateway.
Step 1: connect QueryBear to a read replica
In QueryBear's dashboard, "New connection," paste a Postgres URL pointing at a read replica. Not the primary. Use a role with SELECT only, scoped to the schemas you want the agent to be able to see.
Why a replica:
- Agent-driven queries are unbounded in shape. Some of them are expensive.
- Agents pick weird ordering. Some of those orderings spill to disk.
- A 30-second analytical query holds an
AccessShareLockthat can make aVACUUM FULLorALTER TABLEblock.
A replica gives the agent room to be wasteful without taking down your app.
If you don't have a replica, use a copy of your prod database refreshed nightly. Better than direct, way better than no separation.
Step 2: configure the allowlist
In the QueryBear connection settings, go to "Allowed tables." Pick the tables the agent should be able to see.
For a typical SaaS:
users— yesorders,order_items— yesproducts— yessupport_tickets,support_messages— yesaudit_log— no (sensitive)password_resets,auth_tokens— nowebhook_payloads— no (often contains third-party PII)
Then "Blocked columns":
users.password_hash— blockusers.totp_secret— blockusers.ssn_last_4— blockusers.salary— block
Save. The agent will only see these tables and only these columns when it asks for the schema. Anything it tries against off-list objects gets a parser-level rejection with a readable message.
Step 3: row limits and timeouts
In "Connection limits":
- Default row limit: 1000. The agent rarely needs more, and you don't want it pulling 4M rows into context by accident.
- Statement timeout: 10 seconds. This catches the worst slow queries before they cause replica lag.
- Max EXPLAIN cost: 100000. We
EXPLAINfirst, refuse if the planner thinks it'll be brutal.
These are gateway-side. They override anything Claude Code asks for.
Step 4: install the QueryBear MCP server in Claude Code
In the QueryBear dashboard, go to "MCP" → "Connect Claude Code." You get a URL that looks like:
https://mcp.querybear.com/mcp
In Claude Code, run:
claude mcp add querybear https://mcp.querybear.com/mcpClaude Code will open the OAuth flow. Authorize. Claude Code now has three tools available: list_connections, get_schema, run_query. All three go through QueryBear's gateway.
Step 5: actually use it
Open a Claude Code session in your project. Try something like:
How many users signed up in the last 7 days?
What happens under the hood:
- Claude Code sees the question, decides this needs a database.
- It calls
get_schemaon QueryBear. QueryBear returns the allowlisted tables/columns. - Claude generates a query like
SELECT count(*) FROM users WHERE created_at >= now() - interval '7 days'. - It calls
run_query. QueryBear's parser checks: tables on allowlist (yes), columns not blocked (yes), no DML/DDL (correct), functionnow()allowed (yes),count(*)aggregate fine. Adds an implicitLIMITif the query didn't have one. Setsstatement_timeouton the connection. Runs. - Claude gets back
{ count: 412 }. It tells you "412 users signed up in the last 7 days." - You see that number. The whole round-trip takes under a second.
The first time you do this and watch a DROP TABLE get rejected by the gateway because Claude misunderstood your prompt, you'll appreciate the layers.
What you should and shouldn't ask Claude Code
Things that work great with this setup:
- "How many users signed up this week?"
- "Show me the top 10 customers by total order value."
- "Which support tickets have been open for more than 3 days?"
- "What's the median time-to-first-tool for new accounts?"
- "Find me users who paid in March but haven't logged in since."
Things that are out of scope:
- Anything involving writes. Don't ask Claude to delete users or update fields. The gateway will reject and you'll waste time. Use a separate workflow with explicit confirmations for destructive ops.
- Anything that requires data the allowlist doesn't expose. If you blocked
password_hash, "find users with weak passwords" isn't going to work. That's the point. - Cross-database joins. The MCP server is per-connection.
A workflow tip that took me a month to figure out
Save the queries you re-run. Claude is good at writing queries. It's not consistent. The tenth time you ask "how many signups today" you'll get the tenth slightly-different version of the same query.
In QueryBear, after running a query you like, hit "save as tool." Give it a name like daily-signups. The next time you want that number, in Claude Code, just say:
Use the daily-signups tool.
Claude calls it directly. Same query every time. Same number. No SQL roulette. This is the workflow change that flips QueryBear from "AI database thing" into "the actual UI I use to look at my data."
Things to verify before pointing this at real production
A short pre-flight checklist:
- The connection is to a read replica or a daily-refreshed copy. Not the primary.
- The role has
SELECTand nothing else.\dpin psql should show noINSERT/UPDATE/DELETEgrants. - Allowlist covers the tables you want the agent to see, and only those.
- Blocked columns include everything you'd be unhappy seeing in your terminal.
statement_timeoutis set in the gateway config.- Audit log is enabled (in QueryBear it's on by default; if you're rolling your own, this is the part that gets skipped most).
If all six are checked, you can hand this to a teammate without sweating.
The summary
You can give Claude Code real production database access. You should not give it your prod URL.
The right shape is: read replica → read-only role → gateway with parser, allowlist, column masking, row limit, timeout → MCP exposed to the agent. Each layer fails independently. None of them are work the agent can route around because the agent is on the far side of the gateway, not the database.
Ten minutes of setup. Goes a long way past BEGIN TRANSACTION READ ONLY.
5 comments
- claude_curious
Did this exact setup last weekend. The whole thing took 12 minutes. Now I ask Claude things like 'find the support tickets where the user opened more than 3 in the last week' and it just works. Game changer.
- the_alex
The 'don't do this' opening is the right framing. I have absolutely seen people set DATABASE_URL to prod and run claude code. They learned.
- rachel.k
Saving queries as MCP tools is the move I missed for the first month. Once you do it, the 10th time you ask 'daily signups' it's a single tool call instead of a fresh Claude prompt.
- engineering_today
Six-item pre-flight checklist is what I'm going to demand from any teammate who wants to add a DB MCP to their Claude Code config.
- mcp_dabbler
Cursor users — same setup works there too. Just `cursor mcp add` instead.