The fastest way to give an agent access to your data is to hand it a warehouse credential. It works immediately, it demos beautifully, and it is the configuration that shows up in every incident review I have ever read on this subject.
What follows is the architecture that replaces it. Not “don’t let the agent query things” — querying is the entire value — but the specific layers that make an arbitrary query from a non-deterministic caller a bounded operation instead of an open question.
This is the access side. The meaning side — semantic layers, metric definitions, why a model writing SQL against raw tables goes wrong — is covered in Why LLMs Are Bad at Big Data and Hallucination Is a Data Contract Problem. Assume both here.
What you are actually defending against
Three distinct risks, which get conflated and need different controls:
Cost. An agent can write a query that scans your entire warehouse. It will not mean to. A missing join predicate, an unpartitioned filter, a retry loop on a timeout — any of these turns a question into a five-figure bill, and none of them looks wrong in the generated SQL.
Mutation. Anything that changes state: writes, schema changes, dropped objects. Rare, catastrophic, and entirely preventable at the engine level.
Exfiltration. The one that gets least attention and matters most. An agent that can read sensitive data and also write somewhere — a file, an email, an external table, an API call — is a data egress path with a language model deciding what goes through it. Query budgets do nothing about this.
Controls that address cost do not address exfiltration. Design for all three explicitly.
The layers
1. A separate identity
The agent gets its own database role. Not a shared service account, not the application’s credentials, not a person’s. This is table stakes and it is skipped constantly, usually because the application credential was already sitting in the config.
The reason it matters is not access control — it is attribution. When something expensive or strange happens, you need to know it was the agent, immediately, without correlating timestamps. A shared identity turns every investigation into an archaeology exercise.
Scope the credential to the run and expire it with the run. A long-lived warehouse credential sitting in an agent’s environment is the thing an attacker is trying to reach.
2. Never the primary
Point the agent at a read replica or a dedicated analytics warehouse. Not because the queries are dangerous — because they are unpredictable in cost, and an unpredictable workload sharing capacity with your transactional system is an outage waiting for a slow Tuesday.
This also gives you a clean place to apply everything below without negotiating with the systems your business runs on.
3. Read-only at the engine, not by convention
GRANT SELECT and nothing else. No INSERT, UPDATE, DELETE, no DDL, no temp table creation unless you have thought about it.
What does not count as read-only enforcement: checking the generated SQL for the word “DROP”. Statement filtering by string matching is theatre — comments, casing, nested statements, and stored procedure calls all walk straight past it. If you must inspect statements, parse them into an AST and allow-list node types. Better still, make the permission system enforce it so that a missed case is a permission error rather than an incident.
4. Row and column scoping
Row-level security so the agent sees only the slice it needs. Column masking or exclusion for anything sensitive — and prefer excluding the column from the accessible schema over masking it, because a column the agent cannot see is a column it cannot ask for, reason about, or leak.
Be specific about PII. If the task is “summarise support volume by region,” the agent does not need email addresses, and every layer that has to protect them is a layer that can fail. The strongest control is not having the data reachable.
5. Query budgets
Four numbers, all enforced by the engine rather than the application:
- Bytes scanned per query — the one that actually maps to cost on most modern warehouses.
- Wall-clock timeout per query.
- Row limit on results.
- Concurrency cap for the agent’s role, so a retry storm cannot open forty sessions.
Set per-run and aggregate ceilings. Per-run alone will not catch ten thousand individually reasonable queries, and ten thousand individually reasonable queries is a completely normal failure mode for a system that retries.
6. Refuse rather than truncate
This one is subtle and it is where I see good implementations go wrong.
When a result exceeds the row limit, the tempting behaviour is to return the first N rows. Do not. An agent that receives 1,000 of 40,000 rows and computes an average has produced a number that is wrong, confident, and unmarked. Silent truncation converts a resource limit into a correctness bug.
Return an explicit error the agent can reason about: result exceeded 1,000 rows; refine the query or aggregate in SQL. That is actionable, and it pushes the work to where it belongs — aggregate in the warehouse, don’t enumerate into the context.
7. Close the egress paths
Modern warehouses can write to object storage, call external functions, create external tables, and run user-defined code. Every one of those is a way for data to leave, and none of them is affected by a row limit.
Revoke them. Specifically: no external table creation, no COPY/UNLOAD to storage, no external function invocation, no UDF creation, no network egress from the query engine. Then look at what the agent can do outside the warehouse — if it can query sensitive rows and also send email, you have built an exfiltration channel regardless of how tight the SQL permissions are. That combination is the thing to threat-model, and Threat Modeling an AI Agent covers how.
What to log
Every query the agent runs, with the run identifier that produced it. The full statement text, the bytes scanned, the rows returned, the duration, and the identity used. Tie it to the run record so a postmortem can move from “the number was wrong” to “here is the query that produced it” in one step rather than an afternoon.
Log the queries that were refused, too. Refusals are the highest-signal data you have — they tell you where the agent is trying to go, which is usually more informative than where it succeeded in going.
The failure modes specific to this
The accidental cross join. A missing predicate turns a join into a cartesian product. Bytes-scanned limits catch it; row limits alone often do not, because the engine has already done the work by the time rows come back.
The retry storm on timeout. A query times out, the agent retries, the retry times out. Each attempt costs full price. Retry budgets are not optional here, and a timeout should be treated as a signal to change approach rather than to try again harder.
Truncation-as-answer. Covered above, and worth repeating because it is the one that produces wrong business decisions rather than large bills.
The credential in the context. If connection details reach the model’s context, they can be echoed into logs, summaries, or output. Credentials belong in the execution environment, never in a prompt or a tool argument.
The helpful stored procedure. A read-only role that can execute a procedure which itself writes has write access. Check what the callable surface actually does, not what its name implies — an undeclared side effect is a contract bug, as ever.
Rolling it out
Start with one schema, read-only, aggregates only, with the tightest budget you think could possibly work. Run it in shadow mode first — capture the SQL the agent would have run without executing it. Reading fifty generated queries by hand will tell you more about whether this is ready than any evaluation metric, and it costs an hour.
Then widen by data sensitivity, not by convenience. The order that works is: aggregated non-sensitive, detailed non-sensitive, aggregated sensitive, and detailed sensitive only if you genuinely cannot do the job otherwise.
At each step, the question is not “did it work?” but “what is the worst query this could now run, and what would that cost me?” If you can answer with a bounded number, you have scoped the authority. If you cannot, you have not — you have just not been unlucky yet.
The reliability practice this sits inside is The Agent Reliability Handbook. For making the resulting numbers checkable rather than merely plausible, see Making an Agent Explain What It Did to Your Data.