Stop Putting Your DATABASE_URL in Your AI Agent

I see the same setup over and over. Someone wants their coding agent to be able to check the database, so they drop the production connection string into the agent's environment and move on.
export DATABASE_URL=postgresql://app:hunter2@prod-db/mainIt works on the first try, which is exactly the problem. It works, so nobody looks at it again.
Here's what you actually did. You handed a process that generates text one token at a time the same level of access your application has. If your app user can write, the agent can write. If it can drop tables, the agent can drop tables. The agent isn't malicious. It's just doing its best to satisfy a prompt, and "best" is a probability distribution, not a contract.
The failure isn't the one you're picturing
People imagine the disaster as a rogue DROP TABLE. That happens, but it's rare, and it's loud enough that you'd catch it.
The quieter failures are the ones that get you:
- You ask it to "clean up the test accounts" and it interprets your staging convention slightly wrong, so it deletes 200 real rows.
- It writes an unbounded
SELECT *against a 40-million-row table and pins your primary while a customer is mid-checkout. - It runs a query that's correct but holds a lock longer than you'd ever write by hand, and now three other queries are stacked up behind it.
None of these require the agent to be wrong about its intent. They only require it to be slightly wrong about your data, which it always is, because it has never seen your data.
"I gave it a read-only user"
Good instinct. It's also not enough on its own. A read-only Postgres user can still take your database down. A bad join across two big tables, a regex on an unindexed text column, a cartesian product from a missing predicate. Read-only protects your rows. It does nothing for your CPU, your connections, or your I/O.
I wrote more about that specific gap in why read-only doesn't stop Postgres, but the short version is: "can't write" and "can't hurt me" are different claims, and you want both.
What I actually want between an agent and a database
After getting this wrong a few times myself, here's the list I landed on. Not "use my product." Just the properties that matter, whoever provides them:
A layer that enforces read-only by parsing the query, not by trusting a connection flag. A hard cap on rows returned, so a runaway query can't flood the agent's context with a million records. A statement timeout that kills anything slow before it becomes everyone's problem. Rate limits, because an unsupervised agent at 3am will happily retry a failing query forever. And a log of every single query it ran, so when something looks off you can actually answer "what did it do."
That last one matters more than people expect. The first time an agent does something surprising, you will want a transcript. If your answer is "I'd have to check the Postgres logs," you've already lost the afternoon.
The cheap version
You don't have to buy anything to start. Make a dedicated database role. Grant it SELECT on exactly the tables the agent needs and nothing else. Set statement_timeout and a small default_transaction_read_only on the role. Put it behind a connection pooler with a tight limit. That covers most of the blast radius for an afternoon of work.
The reason I ended up building QueryBear is that the cheap version stops scaling once you have more than one database, more than one agent, and a team that wants the audit trail without each person hand-rolling roles. But for a solo project, the dedicated read-only role plus a timeout gets you 80% of the way and you should do it today.
The one thing you shouldn't do is the thing almost everyone does first. Don't paste the prod connection string into the agent and hope. Hope is not a security boundary.