Why do it
When your application sends a prompt to a model provider, any personal data in it travels with it. Under UK and EU data protection law, data minimisation means you should send only what you need. Most prompts do not need a real name, email address or ID number to get a useful answer, so removing them before the request leaves is the simplest way to reduce what you expose.
This is a practical guide, not legal advice. If you handle health, financial or children's data, talk to your data protection officer or a solicitor as well.
What to remove
- Direct identifiers: names, email addresses, phone numbers, postal addresses and postcodes.
- Government and financial numbers: National Insurance, NHS and passport numbers, payment card numbers, IBANs and bank details.
- Secrets: API keys, passwords, tokens and connection strings. These are often pasted into prompts by accident, especially by developers asking for help with code.
- Internal references: customer, policy and case numbers that identify a person inside your own systems.
Leave harmless context alone. Cities, company names and general dates usually make the answer better and identify nobody on their own.
Do it yourself with Presidio
Microsoft's open-source Presidio library finds many types of personal data using patterns and a language model. This is the whole of a minimal version:
pip install presidio-analyzer presidio-anonymizer
python -m spacy download en_core_web_lg
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
def redact(text: str) -> str:
found = analyzer.analyze(text=text, language="en")
return anonymizer.anonymize(text=text, analyzer_results=found).text
safe = redact("Email jane.doe@example.com about the claim for John Smith, phone 020 7946 0321.")
# -> "Email <EMAIL_ADDRESS> about the claim for <PERSON>, phone <PHONE_NUMBER>."
Then send the redacted text instead of the original:
from openai import OpenAI
client = OpenAI()
reply = client.chat.completions.create(
model="YOUR_MODEL",
messages=[{"role": "user", "content": safe}], # send the redacted text, never the original
)
This works for a prototype. Before you rely on it, know what it does not cover: it does not know your own internal ID formats, it misses some UK-specific numbers and street addresses, and it replaces values with blank labels such as <PERSON>, which can make replies less useful (see the next section).
Use tokens, not blanks
If a prompt contains two people, <PERSON> and <PERSON> loses the difference between them. Number the replacements instead, such as [PERSON_1] and [PERSON_2], so the model can still reason about who is who. Keep a private map from each token back to the real value, and put the real values back into the model's reply before you show it to your user. The model never sees the originals, and the user never sees the tokens.
Keep that map in memory for the length of one request. Do not write it, or the original prompt, to your logs.
Test it before you trust it
Write 50 to 100 realistic examples that include the kinds of data your users actually type, and note which values are sensitive and which words are harmless. Run your redaction over them and count two things: how many sensitive values still appear (the serious error) and how many harmless words were hidden (which costs you answer quality). Keep a second set that you never tune against, so you do not fool yourself.
For an example of reporting this honestly, including the misses, see how Jnvoy measures itself.
Or use a gateway
If you would rather not maintain this yourself, Jnvoy does the steps above as a service you can also self-host. It numbers the tokens, restores the real values in the reply, records only counts of what it found (never the text), and covers UK, EU and other national identifiers. Scan a piece of text:
curl -X POST https://www.jnvoy.com/v1/scan \
-H "x-api-key: YOUR_JNVOY_KEY" -H "Content-Type: application/json" \
-d '{"text": "My name is John Smith and my NI number is AB123456C"}'
Or send a protected request to a model. You supply your own provider key, which Jnvoy uses for that one request and never stores:
curl -X POST https://www.jnvoy.com/v1/proxy \
-H "x-api-key: YOUR_JNVOY_KEY" -H "x-provider-key: YOUR_OPENAI_KEY" \
-H "Content-Type: application/json" \
-d '{"provider": "openai", "model": "YOUR_MODEL",
"messages": [{"role": "user", "content": "My email is john@example.com. Say hello."}]}'
Hosted keys are issued on request through the homepage. The code is on GitHub under the Business Source License 1.1 (source-available, not open source), and you can run it on your own servers.
What can go wrong
- No tool catches everything. Unusual names, informal addresses and company-specific identifiers are the usual misses. Treat redaction as one control among several.
- Context can identify a person on its own. "The CEO of our only London client" may point to someone without naming them.
- Too much redaction hurts answers. If you hide every place and date, the model has nothing to work with.
- Redacting the prompt is not enough if you log it. Check what your own application, proxy and monitoring tools write to disk.