Blog
Anonymize your PII data before sending it to an LLM
This blog post describes why and how you should anonymize your PII data before sending your prompt text to an LLM.
🤔 Why?
In today’s data-driven world, organizations are increasingly leveraging the power of large language models (LLMs) like ChatGPT, Gemini, or Claude for a wide range of use cases — from generating customer insights to summarizing emails.
While these tools offer immense benefits, using them responsibly, especially when dealing with personal data, is non-negotiable.
One critical step in preserving privacy and maintaining compliance is anonymizing Personally Identifiable Information (PII) before sending any data to an LLM. Let’s explore why this is not just best practice, but a necessity.
🔒 What is PII and why it’s sensitive
Personally Identifiable Information (PII) includes any data that can be used to identify an individual, such as names, addresses, phone numbers, social security numbers, email addresses, and even IP addresses. Mishandling PII could lead to privacy breaches, identity theft, and legal consequences.
When LLMs process data with embedded PII, there’s always a risk, no matter how small, that this information may be exposed during inference, stored inadvertently, or reconstructed through model outputs in some contexts — particularly if your data is processed through third-party services or APIs.
🕵️♂️ Why anonymization is essential
Privacy by design
Anonymizing PII aligns with the principle of “privacy by design”, a concept embedded in privacy regulations such as GDPR. This approach ensures privacy risks are mitigated before data processing even begins, reducing the attack surface and safeguarding the identities of individuals.
Regulatory compliance
Laws such as the Algemene Verordening Gegevensbescherming (AVG), the General Data Protection Regulation (GDPR), the California Consumer Privacy Act (CCPA), and the Health Insurance Portability and Accountability Act (HIPAA) impose strict obligations on how personal data is handled, stored, and transferred. Anonymizing PII helps organizations avoid unintentional violations by ensuring that no identifiable data is passed to external services.
For example, GDPR requires that personal data transferred outside the European Union has adequate protection. Anonymizing the data at the source removes much of the regulatory burden while maintaining the usefulness of the data for analysis by language models.
Minimized risk in data breaches
Anonymization significantly reduces the impact and liability in the event of a breach. Since the data no longer ties directly to specific individuals, it becomes far less useful (or harmful) in the wrong hands. Even if anonymized datasets are exposed, the information can’t easily be traced back to specific people.
Ethical AI usage
As AI becomes more deeply embedded in decision-making processes, the ethical use of data is critical. Anonymizing PII helps ensure that organizations are not exploiting or mishandling personal data, thereby maintaining trust with customers and upholding high ethical standards in AI deployment.
Protecting against model memorization
While many leading LLM providers implement rigorous data retention and privacy policies, there remains a theoretical risk of “model memorization,” where models could unintentionally reproduce sensitive data encountered during training or inference. Although the likelihood is low, especially if models are not being fine-tuned with your data, anonymization adds a important safeguard.
🛡️ How to anonymize PII effectively
Anonymization goes beyond simply removing names and (email)addresses. Because removing this data from the prompt could make it impossible to handle the output from the prompt correctly.
For example when you are using an LLM for analyzing multiple resumes for the best match on a job description, it makes not sense to just remove the names from the candicates because in this way, there is no possible solution anymore to relate the response from the LLM to the question in the prompt because all person names are gone.
So the correct way to anonymize the PII data should includes the following two main steps:
- Identification: The PII data should be detected and categorized.
- Anonymization: The identified PII data should now be anonymized.
This can be done in several ways:
– Redact: This just removes all detected PII tokens. Note that this solution limits the possibility to relate the input prompt with the generated answer.
– Replace: This just replaces PII with a fixed categorize-defined placeholder token (e.g., A detected person name is replaced by “[NAME]”).
– Mask: Modify the data so that it resembles the original, but is no longer traceable / detectable. It’s partial redaction. (e.g., A detected US Driver’s license is replaced as “AC43****”)
– Hash: Use a hash algorithm like SHA256 or SHA512 to hash the PII token. This makes the PII tokens unique but non-reversable.
– Encrypt: Use a cryptographic key to encrypt the detected PII token. This makes the PII tokens unique and reversable (it can be decrypted by using the same key). 🔺
🔺 Note
When providing an encrypted value (e.g. WvuyUkUygbvjoVy0-OeMoExioIE) and you want to use this in your prompt and be able to decrypt it again from the LLM response, make sure to instruct the LLM (via the prompt) to not modify or change any text related to this format.
This happens for a few reasons:
- Tokenization limits: LLMs like GPT-4 use a tokenizer that splits text into tokens. Random strings don’t have typical word patterns, so the tokenizer might break them into unexpected pieces, which can lead to slight changes when reconstructing.
- Autocorrection/bias towards plausible text: because random-looking text doesn’t have meaning, the LLM may “correct” or “normalize” the string to something it thinks is more plausible, even if that’s not desired.
- Noise: the model may generate slightly different versions of the text because it’s sampling from a probability distribution. With random-looking text, there’s no “context” to anchor it, so it might introduce small errors.
Possible solutions are:
- Use explicit instructions: in your prompt, you can say: “Please copy and paste this string exactly:
`WvuyUkUygbvjoVy0-OeMoExioIE`. Do not change any character.” - Wrap the text in code fences: using backticks (
`) or code fences ('''/```) helps the LLM recognize it as a literal block to preserve.
🛠️ Tools
There are several tools and libraries which can do this, in this blogpost I’ll zoom into two options.
Azure AI Foundry Language services
SAAS
💡 TIP
When you create a Azure AI Foundry project, you need to define the region, so choose the correct Azure region depending on your GDPR requirements if you are going to use it as a SAAS.
With the Azure AI Language, you can the ‘Extract PII from text’ functionality to detect, extract and anonymize PII data like person-names, emails and other PII data.
In order to do this, you need to create a Azure AI project and connect from your code to the Azure AI Language endpoint (e.g. https://my-project.cognitiveservices.azure.com/)
You can also use the Azure AI Language playground to quickly test the PII capabilities:
– Select “Extract PII from text”

– Test some text

Local Docker
It’s also possible to host the PII detection API as a Docker Container on your own infrastructure.
This is especially important when you have security or data governance requirements that remote PII detection as SAAS can’t support or is not allowed. With this solution all PII data will be analyzed and anonymized before it’s send to an LLM.
The requirements to use these Docker containers are:
- Docker (Linux Containers) installed on a host computer / local infrastructure. Note that Kubernetes is also an option.
- Docker must be configured to allow the containers to connect with and send billing data to Azure.
- Azure AI Language resource should be in place.
See the next examples on how to start the Language and PII Docker images and how you can analyze and anonymize the text using the Microsoft SDK.
First make sure the LANGUAGE_URL(1) and the LANGUAGE_KEY(2) are available as environment variables, you can find these in Azure AI Foundry:

Then make sure that both Docker containers are started:
docker run --rm -it -p 5000:5000 --memory 4g --cpus 1 mcr.microsoft.com/azure-cognitive-services/textanalytics/language Eula=accept Billing=%LANGUAGE_URL% ApiKey=%LANGUAGE_KEY% docker run --rm -it -p 5004:5000 --memory 8g --cpus 1 mcr.microsoft.com/azure-cognitive-services/textanalytics/pii:latest Eula=accept Billing=%LANGUAGE_URL% ApiKey=%LANGUAGE_KEY%
Example C# code to analyze a text for PII items is like:
var text =
"""
Geachte heer/mevrouw,
Op 10 maart 2024 heb ik een oven gekocht.
Helaas werkt de oven sinds 25 maart 2024 niet naar behoren, de oven wordt niet warm en geeft een foutmelding op het display.
Ik zie uw reactie met belangstelling tegemoet.
Peter Jansen
Dorpsstraat 10, 5678 CD Utrecht
peter.jansen@email.com
""";
// 0. Define key and endpoints
var azureKeyCredential = new AzureKeyCredential(Environment.GetEnvironmentVariable("LANGUAGE_KEY")!);
var languageEndpoint = new Uri("http://localhost:5000");
var personallyIdentifiableInformationEndpoint = new Uri("http://localhost:5004");
// 1. Detect the language
var languageClient = new TextAnalyticsClient(languageEndpoint, azureKeyCredential);
var lang = (await languageClient.DetectLanguageAsync(text)).Value.Iso6391Name;
// 2. Analyze and anonymize the PII in the text
var personallyIdentifiableInformationClient = new TextAnalyticsClient(personallyIdentifiableInformationEndpoint, azureKeyCredential);
var entities = (await personallyIdentifiableInformationClient.RecognizePiiEntitiesAsync(text, lang)).Value;
Console.WriteLine($"Redacted Text: {entities.RedactedText}");
Console.WriteLine($"Recognized {entities.Count} PII entit{(entities.Count > 1 ? "ies" : "y")}:");
foreach (PiiEntity entity in entities)
{
Console.WriteLine($"Text: {entity.Text}, Category: {entity.Category}, SubCategory: {entity.SubCategory}, Confidence score: {entity.ConfidenceScore}");
}
The redacted text looks like this:
Redacted Text: Geachte ****/*******, Op ************* heb ik een oven gekocht. Helaas werkt de oven sinds ************* niet naar behoren, de oven wordt niet warm en geeft een foutmelding op het display. Ik zie uw reactie met belangstelling tegemoet. ************ ******************************* **********************
And the 7 detected entities are:
– Text: heer, Category: PersonType, SubCategory: , Confidence score: 0,97
– Text: mevrouw, Category: PersonType, SubCategory: , Confidence score: 0,86
– Text: 10 maart 2024, Category: DateTime, SubCategory: Date, Confidence score: 1
– Text: 25 maart 2024, Category: DateTime, SubCategory: Date, Confidence score: 0,99
– Text: Peter Jansen, Category: Person, SubCategory: , Confidence score: 1
– Text: Dorpsstraat 10, 5678 CD Utrecht, Category: Address, SubCategory: , Confidence score: 0,99
– Text: peter.jansen@email.com, Category: Email, SubCategory: , Confidence score: 0,8
Microsoft Presidio tool
>> Also with this solution all PII data will be analyzed and anonymized in your locally running Docker container before it’s send to an LLM.
The name Presidio origins from Latin praesidium (protection, garrison) and helps to ensure sensitive data is properly managed and governed. It provides fast identification and anonymization modules for private entities in text such as credit card numbers, names, locations, social security numbers, bitcoin wallets, US phone numbers, financial data and more.
This is a Data Protection and De-identification SDK (written in Python and available in a Docker image with a REST interface) to analyze and protect data.
It’s context aware, pluggable and customizable PII de-identification service which internally uses spaCy (Natural Language Processing toolkit and models).
Presidio contains two applications:
- analyzer
- anonymizer + deanonymizer
Analyzer

Anonymizer / Deanonymizer

Both tools are highly configurable and customizable to your needs.
For example, it’s possible to use different NLP Engines for the languages you want to analyze.
Also the existing PII token recognizers can be modified, extended or enabled/disabled according your use-case scenario.
Another important feature is that these tools are available as a default and customizable Docker images which exposes the API via a REST interface, which means that you can run the Docker image locally or in your own data-cetre and you can use any programming language to access the REST interface.
For C#, I did create a NuGet package which supports the complete API for the analyzer and anonymizer / deanonymizer, see the Presidio.SDK project.
Example:
var text =
"""
Geachte heer/mevrouw,
Op 10 maart 2024 heb ik een oven gekocht.
Helaas werkt de oven sinds 25 maart 2024 niet naar behoren, de oven wordt niet warm en geeft een foutmelding op het display.
Ik zie uw reactie met belangstelling tegemoet.
Peter Jansen
Dorpsstraat 10, 5678 CD Utrecht
peter.jansen@email.com
""";
// Analyze text for PII
var analyzeRequest = new AnalyzeRequest
{
Text = text,
Language = "nl",
AdHocRecognizers =
[
new PatternRecognizer
{
Name = "Dutch Postcode recognizer",
SupportedEntity = "NL_POSTCODE",
SupportedLanguage = "nl",
GlobalRegexFlags = RegexFlags.Multiline | RegexFlags.DotAll,
Patterns =
[
new Pattern
{
Name = "Dutch Postcode",
Regex = @"\b[1-9][0-9]{3}\s?(?!SA|SD|SS)[A-Z]{2}\b",
Score = 1
}
],
Context = ["postcode"]
}
]
};
var analysisResults = await analyzerService.AnalyzeAsync(analyzeRequest);
As you can see, a powerful feature from the presidio-analyzer is that you can provide your own Regular Expression based recognizers, in this example an additional recognizer for a Dutch Postcode is added.
In this example, the analysisResults contains an array of all the PII types and the location in the original text. Example:
AnalysisResults: [
. . .
{
"Start": 245,
"End": 257,
"Length": 12,
"Score": 0.85,
"EntityType": "PERSON",
"RecognitionMetadata": {
"RecognizerName": "SpacyRecognizer",
"RecognizerIdentifier": "SpacyRecognizer_140220366258256"
}
},
. . .
When this information is used to anonymize the request:
// Step 2a: Anonymize the detected PII
var anonymizeRequest = new AnonymizeRequest
{
Text = text,
Anonymizers = new Dictionary
{
[PIIEntityTypes.PERSON] = new Hash(),
[PIIEntityTypes.DATE_TIME] = new Mask { MaskingChar = "*", CharsToMask = 99 },
[PIIEntityTypes.EMAIL_ADDRESS] = new Encrypt { Key = "3t6w9z$C.F)J@NcR" }
},
AnalyzerResults = analysisResults.Select(r => new RecognizerResult
{
Start = r.Start,
End = r.End,
Score = r.Score,
EntityType = r.EntityType
}).ToArray()
};
var anonymizeResponse = await anonymizerService.AnonymizeAsync(anonymizeRequest);
The anonymizeResponse returns the text as:
Geachte heer/mevrouw, Op ************* heb ik een oven gekocht. Helaas werkt de oven sinds ************* niet naar behoren, de oven wordt niet warm en geeft een foutmelding op het display. Ik zie uw reactie met belangstelling tegemoet. ddf4c15bf8217f6c9a1ed0bf03e0324a3bd404d764053359b6f6f9eb3a153ba5 , WvuyUkUygbvjoVy0yhwBNH9MfLzEyZCQjjAvP2hyPrY5f2sQL249Z-OeMoExioIE
As you can see:
- All references to a person (Peter Jansen) are hashed.
- All references to a Date or DateTime are replaced by
***. - All references to an email-address are encrypted.
- All other references are by default replaced by
<EntityType>
Deanonymizing in code can be done like this:
var deanonymizeRequest = new DeanonymizeRequest
{
Text = anonymizeResponse.Text,
Deanonymizers = new Dictionary
{
[PIIEntityTypes.EMAIL_ADDRESS] = new() { Key = "3t6w9z$C.F)J@NcR" }
},
AnonymizerResults = anonymizeResponse.Items
.Where(r => r.Operator == Operators.encrypt)
.ToArray()
};
var deanonymizeResponse = await anonymizerService.DeanonymizeAsync(deanonymizeRequest, cancellationToken);
Note that when deanonymizing, it’s only possible to revert the anonymization from encrypted PII data (if the same key is used).
So for this example, the output will be:
Geachte heer/mevrouw, Op ************* heb ik een oven gekocht. Helaas werkt de oven sinds ************* niet naar behoren, de oven wordt niet warm en geeft een foutmelding op het display. Ik zie uw reactie met belangstelling tegemoet. ddf4c15bf8217f6c9a1ed0bf03e0324a3bd404d764053359b6f6f9eb3a153ba5 , peter.jansen@email.com
⚖️ Finding the Balance
Anonymization doesn’t mean sacrificing utility. Done correctly, it maintains the core context and meaning of the data while removing the component that could compromise privacy. For example, “John Smith filed a complaint on January 3rd” becomes “[CUSTOMER_NAME] filed a complaint on [DATE]” — preserving the semantics that LLMs need to perform their tasks, while stripping out sensitive identifiers.
📌 Conclusion
Sending original PII to an LLM is a risk no organization should take lightly. Anonymizing data before it interacts with a language model is not just a technical precaution, it’s a foundational component of responsible, ethical, and legal AI use. In a world where data privacy is important, anonymization is a “must-do” first step toward using the full potential of LLMs safely, correctly and effectively.
I hope this blog post did provide some details on why it’s required and what tools can be used to make sure PII data is handled correctly when using an LLM.
🌐 Links
- Microsoft Presidio – Home page
- GitHub Project – .NET REST Presidio.SDK
- GitHub Project – My own Docker version including extra
nl-language support
📝 Notes
Some content in this blog is created with the help of a LLM. I did review and revise the content where needed.
Written by: Stef Heyenrath
Stef started writing software for the Microsoft .NET framework in 2007. Over the years, he has developed into a Microsoft specialist with experience in: backend technologies such as .NET, NETStandard, ASP.NET, Ethereum, Azure, and other cloud providers. In addition he worked with several frontend technologies such as Blazor, React, Angular, Vue.js.
He is the author from WireMock.Net.
Mission: Writing quality and structured software with passion in a scrum team for technically challenging projects.
Want to know more about our experts? Contact us!
