Skip to the content.

How to redact PII before sending prompts to an LLM

Calling a model API sends a request body off your machine. Whatever is in that body reaches the provider: names, emails, card numbers, the contents of files your code read, and anything an agent put in a tool call. This page is about removing that before it leaves, and about how to check that it actually did.

PrivAiTe is one of the three options below. It is not the right answer for everyone, and the page says where it is not.

First, know where PII actually hides

Most tools scan messages[].content and stop there. In an application that only chats, that is enough. In anything that uses tools, it is not, because the same values live in two more places:

Whatever you pick below, check it against all three. A tool that scores well on plain text can still forward everything in the other two.

Option 1: do it in your own code

Call Microsoft Presidio (or any detector) on the text before you build the request. Free, no new component to run, and you keep full control of what counts as sensitive.

It works well when there is exactly one place in your code that talks to the model. It stops working when there are several: every new call site is a path someone has to remember to route through the scrubber, and the structured fields above are easy to forget because they do not look like user text. There is also no restore step, so the model’s reply comes back full of placeholders and your application has to put the real values back itself.

Pick this if your surface is small and you want no extra infrastructure.

Option 2: a guardrail inside a gateway you already run

If you already route model traffic through a gateway, it probably has a hook for this. LiteLLM ships a Presidio guardrail, Kong’s AI Gateway calls any HTTP guardrail service, and the cloud gateways have their own.

The advantage is that there is no new hop: the thing is already on the path, so you configure rather than deploy. The limits are the guardrail’s own. Check whether it covers tool-call arguments, and whether it restores values on the way back or only deletes them.

Pick this if a gateway is already in your path.

Option 3: a proxy in front of the provider

Put an OpenAI-compatible proxy between your client and the provider, and point the client’s base URL at it. Nothing in your application changes, every call site is covered because they all go through the same address, and clients you do not control (a chat UI, a coding agent) are covered too.

This is what PrivAiTe does. What it adds over the two options above:

The cost is a component to run and latency on every request. Detection runs locally, so nothing is sent anywhere for analysis.

Other proxies exist in this shape. Occludra Gateway is simpler and also blocks prompt injection, but per its documentation it replaces values irreversibly and covers message text only. LLM Guard was the reference open-source scanner suite and is archived as of July 2026. A detailed side-by-side, including where PrivAiTe loses, is on the comparison page.

Pick this if several clients talk to the model, or if any of them is an agent.

None of this is a guarantee

Detection is statistical. PrivAiTe’s default preset fully masks 72.7% of direct identifiers on the 3 public corpora of privaite-bench that PrivAiTe’s own model never trained on, with the harness open so the number can be rerun. Per type it ranges from 88% to 92% on person names, government and network IDs down to 64% on contacts, 62% on bank and card numbers and 9% on record numbers; the opt-in minilm preset reaches 82.9% on the same corpora. Any tool that tells you nothing gets through is either not measuring or not telling you.

Treat the result as pseudonymisation and transfer minimisation, not anonymisation, and decide per type what must never come back at all. In PrivAiTe that is entity_overrides with redact or mask, and block_entities for values that must not leave even as a placeholder.

Check it on the wire, not in the reply

The common way to check is to send a test prompt and read the answer. That shows what came back, which is not what went out. A span that covers only part of a value looks clean in a restored reply and still put the rest of it on the wire.

Capture the outbound request body instead. With PrivAiTe:

privaite verify

It runs a throwaway provider on 127.0.0.1, sends the same agent-shaped request straight to it and then through a real proxy, and prints both request bodies. It exits non-zero if a planted value reached the wire. With any other tool, point it at a local endpoint that logs what it receives and compare the two bodies yourself. More ways to audit, including on your own data: verification.