Skip to main content
An agent allowed to read a table reads every value in it — including the personal data you’d never paste into a chat: emails, dates of birth, salaries, card numbers. Column masking lets the agent keep querying — listing customers, joining orders, analyzing trends — while those values come back redacted (0.12.0+). It’s per-column and layered on table_access: a masked column must still be readable, and masking only shapes a value the agent is already allowed to read — it never grants access. The engine masks at the source — it rewrites the query so the real value is replaced before the query filters, joins, aggregates, or returns. It resolves each field back to its base column, so an alias, a join, or a bare SELECT * can’t dodge a mask.

Transforms

Each mask names a transform. Three are presets (a bare string); four are parametric (a t: map with options). Every transform except noise is deterministic — the same input always yields the same output, so masked columns stay join- and group-safe. The four parametric transforms take options:
{ keepStart?, keepEnd?, glyph? }
Reveal keepStart leading and keepEnd trailing characters (sum ≤ 64), masking the rest with glyph (default ).
{ granularity }
year / month / day truncates a date (UTC); a positive integer rounds a number down to that bucket width.
{ kind }
email, name, or phone — the fake keeps the shape of a real value of that kind.
{ ratio }
Proportional spread in (0, 1]; 0.1 lands within ±10% of the input.
noise breaks joins and grouping by design — use it for rough magnitude in analytics, never on a key.

Add a mask

A project’s Exposure scan reads only your database’s information_schema — column names and types, never row data — and flags likely-PII columns with a suggested, type-valid transform you apply in one click. The masked preview runs a read through the same query path your agent uses and shows exactly what the agent gets back, so you can prove a mask before you rely on it. Saving a mask triggers a brief reconnect, and every mask change is recorded in the audit log.

Good to know

Masking is fail-closed: a query shape the engine can’t prove safe to rewrite — a view over a masked table, a whole-row serialization like to_jsonb(users), an opaque function — is denied whole (a column_masking reject), never partially masked. And a filter or sort sees the masked value, so the agent can’t infer its way to the real one — WHERE credit_card = '4111…' matches nothing once the card is masked. But an order- or format-preserving transform like generalize can still leak coarse buckets through a range filter. If a column is a secret the agent must never act on, deny the table rather than mask it.

Next steps

Write a policy

Grant per-table reads and writes, then layer masks on top.

Denial reasons

The shapes masking denies, and how to rework a query.