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.

What one SELECT * puts in the agent’s context
  • 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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 wire
prod-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.

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