Hide personal data columns from an AI agent with Postgres column grants

An agent often needs a customers table but not the email and phone columns in it. Postgres can grant single columns. We tested what the agent can and can't do with that.

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 reads what Postgres actually committed, not what the agent reported. In our demo a hidden trigger turned 1 row into 301; a person countersigned the undo and all 301 rows came back.

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 write SELECT * 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 the WHERE clause just as in the select list.
  • The update needs read access on the key. where id = 2 reads id, so the role needs SELECT on id as well as UPDATE on plan.

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 EXECUTE on functions to PUBLIC by default.2 A SECURITY DEFINER function 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_data reads 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:

  1. Say it up front. Put the allowed columns in the tool description or system prompt, so the agent writes select id, name, plan the first time.
  2. Don’t hand it a second credential. If an admin connection string sits in .env, the column grants are a speed bump.
  3. 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.
  • SELECT granted on named columns only; UPDATE only on columns it must change.
  • Key columns used in WHERE clauses 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.

The takeaway

With column grants our agent role read id, name and plan, updated only plan, and was denied SELECT *, the email column, and WHERE email LIKE.

Read nextYour Postgres view bypasses row-level security. Here's the fix. Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1PostgreSQL docs: GRANT · postgresql.org · checked 13 October 2026
  2. 2PostgreSQL docs: Privileges · postgresql.org · checked 18 October 2026
  3. 3PostgreSQL docs: CREATE VIEW · postgresql.org · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing