query results and describe_table sample rows. No code changes, no database views, no application-layer middleware — just a YAML file.
Why column masking?
AI models are powerful data analysts, but they don’t need to see raw PII to write correct SQL. With column masking:- Emails, SSNs, phone numbers are redacted before the AI ever sees them
- Query results are masked in real time — the AI gets useful structure without sensitive values
- Sample rows in
describe_tableare also masked, so table analysis doesn’t leak PII - Masking is enforced server-side — the AI cannot bypass it, regardless of what SQL it generates
Enabling column masking
Column masking is part of the policy engine. Addmask directives to any column in your policy YAML:
Mask types
There are four mask types. Each is designed for a different use case.redact
Replaces the value with ***. Use for columns the AI never needs to see.
Best for: email addresses, API keys, passwords, tokens.
hash
Replaces the value with a deterministic SHA-256 hex string (64 characters). Same input always produces the same hash, so the AI can still detect duplicates, join on hashed values, and count distinct entries — without seeing the original data.
Best for: columns where the AI needs to detect patterns (GROUP BY, JOIN, COUNT DISTINCT) without seeing raw values. Names, external IDs, usernames.
partial
Reveals only the last 4 characters, replacing the rest with asterisks. Works correctly with unicode characters.
Best for: phone numbers, credit card numbers, account numbers — where partial visibility helps the AI understand the format.
null
Replaces the value with NULL. The AI sees that the column exists but has no data.
Best for: columns that must be completely hidden. The AI can still query other columns in the same table.
How masking works
Query results
When aquery tool call returns results, Isthmus applies masks to every row before sending the response to the AI:
email: redact and name: hash:
Describe table sample rows
Thedescribe_table tool returns up to 5 sample rows. These are masked identically — same rules, same mask types:
Column name matching
Masking matches by column name only, not by table. If you maskemail, it applies to every column named email in every query result — regardless of which table it comes from, including JOINs, subqueries, and aliases.
This is by design. SQL queries with JOINs, CTEs, and subqueries make it impossible to reliably map result column names back to source tables. Rather than building a fragile runtime mapper, Isthmus uses a simple, predictable rule: same column name = same mask.
Conflict detection
Because masking is by column name, Isthmus validates at startup that no column name has conflicting mask types across tables. If two tables define different masks for the same column name, Isthmus rejects the policy file:Full example
A realistic policy YAML with masking:email appears in both customers and employees with the same mask (redact), which is valid.
Type behavior
Masked values may change type. This is intentional and documented here for completeness:Choosing the right mask type
Compliance context
Column masking helps satisfy data protection requirements across common regulatory frameworks:Interaction with other features
Limitations
- Column name scope — masks match by column name globally, not per table. You cannot mask
emaildifferently inusersvs.contacts. This is a deliberate tradeoff: simplicity and predictability over per-table granularity. - SQL aliases — if a query uses
SELECT email AS contact_email, the result column is namedcontact_email, and theemailmask will not apply. The AI could theoretically use aliases to bypass masking. Mitigate this with a dedicated read-only database role that restricts access to sensitive columns at the PostgreSQL level. - Aggregations —
SELECT COUNT(DISTINCT email)returns an integer count, not email values. Masking does not interfere with aggregations since the masked column is not in the result set. - WHERE clauses — masking does not affect query filters.
SELECT id FROM users WHERE email = 'alice@example.com'executes against the real data. The AI can still filter by masked columns — it just cannot see the values in results.
Tips
- Start with
redact— it’s the safest default for PII columns. - Use
hashwhen the AI needs to detect patterns — GROUP BY, JOIN, or COUNT DISTINCT still work on hashed values. - Use
partialfor phone numbers and account numbers — the last 4 digits help the AI understand the format without exposing the full value. - Use
nullfor columns that should be completely invisible — salaries, SSNs, medical data. - Masking + business descriptions work together — the AI sees
"description": "Primary email address"alongside"***", so it knows what the column is without seeing the values. - Mask liberally, describe generously — err on the side of masking more columns. The AI writes better SQL when it understands the schema (via descriptions) than when it sees raw data.
- Think about JOINs — if you mask
emailin one table, mask it in all tables. Isthmus enforces this consistency at startup. - Combine with database-level controls — for maximum protection, mask columns in the policy YAML and revoke
SELECTon those columns for the database role. This provides defense in depth.