Agents never see the PII.
Your agent reads production. The sidecar rewrites sensitive values in the response, in memory, before they reach the client. It never touches the request.
Synthetic data, wrong answers.
SELECT * returns the email, the card number and the national ID, into a context window you do not control.
Real schemas, real volumes, real edge cases make an agent useful, and that is why teams point agents at production. The problem was never the query. It is what comes back.
- emaildana@northwind.example
- card4111 1111 1111 1111
- national ID900-41-2208
All of it lands before anyone reads the reply.
Name the entity, not the column.
A masking rule pairs an entity with a strategy. The sidecar reads every response coming back from your resource and applies the rule before the client sees a byte.
Entities, not columns
You declare what to look for: EMAIL_ADDRESS, US_SSN, CREDIT_CARD. The rule matches the value in the response, so it holds when a schema changes and catches the same data in a column nobody tagged.
Three strategies
Redact removes the value. Partial keeps the last digits. Hash keeps it stable, so a masked column still joins. You pick per rule.
In memory, in the response
The rewrite happens inside the sidecar, on the bytes coming back. Nothing is written anywhere, and your database stays as it is.
Every protocol it fronts
Postgres, SQL Server and HTTP. The same rule covers a query result, an API payload and a shell session reading a file.
The email never arrives.
Pick what the rule names, then read the same response before and after the sidecar.
EMAIL_ADDRESS matches the value, wherever the response carries it.
mask.rules
- {name: emails, entity: EMAIL_ADDRESS, strategy: redact}
SELECT email, ssn, card FROM customers LIMIT 3;prod-db sends
email | ssn | card
------------------------+-------------+---------------------
dana@northwind.example | 900-41-2208 | 4111-1111-1111-1111
marcus@verde.example | 900-73-5512 | 5555-5555-5555-4444
priya@kestrel.example | 900-18-9034 | 3782-822463-10005
agent-07 reads
email | ssn | card
--------------------------+-------------+---------------------
[REDACTED:EMAIL_ADDRESS] | 900-41-2208 | 4111-1111-1111-1111
[REDACTED:EMAIL_ADDRESS] | 900-73-5512 | 5555-5555-5555-4444
[REDACTED:EMAIL_ADDRESS] | 900-18-9034 | 3782-822463-10005
hoop.dev · emails redacted at the wireprod-db sends, row 1 of 3
email dana@northwind.example
ssn 900-41-2208
card 4111-1111-1111-1111
agent-07 reads, row 1 of 3
email [REDACTED:EMAIL_ADDRESS]
ssn 900-41-2208
card 4111-1111-1111-1111
hoop.dev · emails redacted at the wire> pull the customer list for the churn report
● postgres(SELECT name, email FROM customers)
⎿ name email
Ada Lovelace [REDACTED:EMAIL_ADDRESS]
Grace Hopper [REDACTED:EMAIL_ADDRESS]
hoop.dev · emails redacted at the wire
The query runs as written.
Nothing rewrites the statement on the way in. No injected WHERE clause, no swapped column, no proxy guessing at intent. The database plans and executes exactly what the agent asked for.
The rewrite happens on the way back. Counts stay right and joins still resolve. The agent gets a working answer with the sensitive values gone.
Four controls, one sidecar.
Data Masking
Rewrites sensitive values in the response, in memory, before they reach the client. The request itself is never touched.Guardrails
An ordered deny list checked against every statement. Destructive ones never reach the database, and the client reads the reason you wrote.Session Analyzer
An agent scores every action’s intent and syntax for risk before it executes.Human in the Loop
Risky operations pause for a person’s one-off approval, then the agent carries on.
Name what agents never see.
Declare the entities, pick a strategy, start the sidecar. Free and open source under MIT.
Ask an assistant how Data Masking works: Claude, ChatGPT, Grok, Gemini, Perplexity, Copilot.
# config.yaml
pii:
entities: [EMAIL_ADDRESS, US_SSN, CREDIT_CARD]
mask:
enabled: true
rules:
- {name: emails, entity: EMAIL_ADDRESS, strategy: redact}
$ hoop start sidecar --config config.yaml
sidecar running