Row and Column Permissions for AI Agents

Read-only is the first thing people set up when they give an agent database access, and it's necessary. It's also not sufficient, because "can only read" and "can read everything" are very different security postures. An agent that can read every row and every column of your users table can read your customers' emails, their hashed passwords, their addresses, and whatever else lives there. Read-only didn't protect any of that. It just stopped writes.
If you're going to let an agent (or the people prompting it) query production, you want control over which rows and which columns it can see, not just whether it can write. Here's how to think about both.
Column-level: the agent doesn't need the PII
Most questions an agent answers don't require sensitive columns at all. "How many users signed up last week" needs a count, not anyone's email. "Average order value by plan" needs amounts and plan tiers, not names or card details. The sensitive columns are along for the ride, exposed by default, used by nothing.
So restrict them. In Postgres you can grant SELECT on specific columns rather than the whole table:
-- agent can see these columns and only these
grant select (id, plan, created_at, country) on users to agent_readonly;
-- it cannot select email, full_name, password_hash, etc.Now a query touching email fails outright. The agent can still answer the aggregate questions, because those never needed the PII. You've shrunk what a leak could expose to the columns that are actually safe to expose.
For columns you want available but not in the raw, a view is the cleaner tool: expose a users_safe view that selects only the non-sensitive columns, or that masks the sensitive ones (showing a domain instead of a full email, say), and grant the agent access to the view instead of the table.
Row-level: the agent should only see some rows
Column control handles "which fields." Row-level control handles "which records," and it's how you scope an agent to a subset of the data. Common cases: an agent that should only see one tenant's rows in a multi-tenant database, or that should never see internal test accounts, or that's scoped to a region.
Postgres row-level security (RLS) is built for this. You enable it on the table and write policies that decide which rows a given role can see:
alter table orders enable row level security;
create policy agent_sees_non_test on orders
for select
to agent_readonly
using (is_test = false);Now every query the agent runs against orders is silently filtered to non-test rows. It can't see the test data even if it tries, and it doesn't have to remember to add where is_test = false to every query, because the database enforces it. For multi-tenant scoping, the policy keys off a tenant id instead, and the agent physically cannot read across the boundary.
Why "the agent will just filter" isn't enough
The tempting shortcut is to skip database-level controls and trust the agent to only query what it should — tell it in the prompt to avoid PII and test rows. Don't rely on that. The whole reason you're adding guardrails is that the agent's behavior is probabilistic. A prompt instruction is a suggestion. A GRANT is a wall. When the prompt and the grant disagree, you want the grant to win, which means the restriction has to live in the database or the access layer, not in the instructions.
This is the same reason read-only enforcement belongs at the query-parsing layer rather than in a "please don't write" prompt. Defense that depends on the agent cooperating isn't defense.
Putting it together
A solid agent posture is three layers stacked: read-only so it can't mutate, column grants (or safe views) so it can't see sensitive fields, and row policies so it can't see records outside its scope. Each one closes a gap the others don't. Read-only alone leaves all your data readable. Column control alone leaves every row exposed. Together they mean the agent can answer real questions while genuinely unable to reach the data you didn't intend it to.
You can wire all of this with native Postgres features and a carefully scoped role, and for a single database that's a reasonable afternoon. Where it gets tedious is keeping the column lists, views, and policies consistent across multiple databases and multiple agents, which is part of what QueryBear manages centrally — but the underlying controls are standard SQL, and you should use them whether or not you use a product. The principle is the part that matters: scope the rows, scope the columns, and never let the only thing standing between an agent and your customers' PII be a polite request in the prompt.