The short answer
Grant the agent’s role SELECT on the columns it needs, not the table: grant select (id, name, plan) on customers to agent_ro;. PostgreSQL can grant SELECT, INSERT, UPDATE and REFERENCES on single columns.1 In our test, the agent’s role read those columns, got permission denied for SELECT * and for the email column, and couldn’t even filter on email in a WHERE clause. What it could still see is that the email column exists.
Column grants work best on top of a role that starts with nothing. PostgreSQL grants no table privileges to PUBLIC by default, so a new role reaches no table until you grant it.2
Deedglass
A database change, checked and put back
-
1The agent’s change
Change order 42
UPDATE orders … WHERE id = 42
The agent: “1 row updated” -
2What the database really did
301 rows
A hidden trigger changed 300 more
Doesn’t match -
3Put back
301 rows restored
Signed off by a person
Matches
The setup
We ran this on PostgreSQL 18.6, with a customers table holding two fake rows:
Code A database command in SQL. The text around it says what it does.
create table customers(id int primary key, name text, email text, phone text, plan text);
insert into customers values
(1,'Asha','asha@example.com','+91 90000 00001','pro'),
(2,'Ravi','ravi@example.com','+91 90000 00002','free');
create role agent_ro login password '...';
grant select (id, name, plan) on customers to agent_ro;
grant update (plan) on customers to agent_ro;
The agent can read three columns and change one. That’s a common shape: an agent that handles plan changes needs to find the customer and change the plan, not read their contact details.
What the agent can and can’t do
Every result below is what psql printed with set role agent_ro:
| Statement | Result |
|---|---|
select * from customers; |
ERROR: permission denied for table customers |
select id, name, plan from customers; |
2 rows |
select email from customers; |
ERROR: permission denied for table customers |
select count(*) from customers where email like '%example.com'; |
ERROR: permission denied for table customers |
update customers set plan = 'pro' where id = 2; |
UPDATE 1 |
update customers set email = 'x@example.com' where id = 2; |
ERROR: permission denied for table customers |
Three of these matter more than they look:
SELECT *fails. Agents writeSELECT *constantly. With column grants it errors instead of quietly returning personal data. The agent has to name columns, which also makes the query easier for a person to review.- Filtering on a hidden column fails. Without this, an agent could learn an email address one character at a time with
WHERE email LIKE 'a%'. In our test the hidden column was refused in theWHEREclause just as in the select list. - The update needs read access on the key.
where id = 2readsid, so the role needsSELECTonidas well asUPDATEonplan.
What column grants don’t hide
We ran two catalog queries as the same role:
Code What the screen or file shows, copied as is.
cols_in_information_schema
----------------------------
3
cols_in_pg_attribute
------------------------------
id, name, email, phone, plan
information_schema.columns showed only the three columns the role may use. pg_catalog.pg_attribute listed all five. So the agent can learn that email and phone exist, though not what’s in them. For most teams that’s fine. If a column’s name is itself sensitive, column grants won’t hide it.
Other limits:
- Rows aren’t filtered. Column grants decide which columns; row-level security decides which rows. Use both if the agent should see only some customers.
- Functions run with their own rights. PostgreSQL grants
EXECUTEon functions toPUBLICby default.2 ASECURITY DEFINERfunction that returns email addresses hands them to anyone who can call it. Review functions the role can execute. - Admin roles see everything. A role with
pg_read_all_datareads all tables and columns.2 If the agent’s role belongs to a provider admin group, column grants on it don’t matter; see what rds_superuser and neon_superuser carry. - Logs and copies. The column is hidden in this table. It isn’t hidden in a replica, a nightly export, or application logs the agent can also read.
Column grants or a view
The other common way to hide columns is a view with only the safe ones: create view customers_public as select id, name, plan from customers; and a grant on the view.
| Column grants | A view | |
|---|---|---|
SELECT * |
Errors | Returns the safe columns |
| Agent sees the table’s real name | Yes | Only if you also grant the table |
| Row-level security on the table | Applies to the agent’s role | Applies the view owner’s policies, unless security_invoker = true3 |
| Updates | Per column | Simple views can be updated through, as the owner |
| Catalog shows hidden column names | Yes, in pg_attribute |
The view’s columns only, if no table grant |
The view is friendlier to an agent (no errors on SELECT *), but it runs with its owner’s rights by default, which is how views end up bypassing row-level security. If you choose a view, set security_invoker = true and give the agent’s role column grants on the base table anyway, so the two checks agree.
The agent will hit errors: plan for them
An agent that gets permission denied tends to try something else. Some will rewrite the query with named columns, which is what you want. Others will look for a way around: a different role, a different table, a connection string in a config file. Three habits help:
- Say it up front. Put the allowed columns in the tool description or system prompt, so the agent writes
select id, name, planthe first time. - Don’t hand it a second credential. If an admin connection string sits in
.env, the column grants are a speed bump. - Treat repeated denials as a signal. In logs, a burst of denied queries on one table means the agent is probing, or the task needs data you didn’t plan for. Either way, a person should look.
If the hidden columns hold health data, the PHI glossary entry covers what usually counts.
A checklist
- The agent’s role starts with no table grants and isn’t in an admin group.
-
SELECTgranted on named columns only;UPDATEonly on columns it must change. - Key columns used in
WHEREclauses are readable. - Row-level security added if it should see only some rows.
- Functions the role can execute reviewed for ones that return hidden columns.
- No other credential to the same database in files the agent can read.
To check whether an agent already holds a broader credential, paste the agent’s MCP config into the check page or run npx deedglass scan (how the grade works). To try the checks in your browser without installing anything, open the playground.
Written with AI assistance and checked against our claims register.