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 (at: 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.Add a mask
- Cloud (dashboard)
- Self-host (YAML)
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 liketo_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.