Nobody decides to send a postal address to a language model. It arrives like this:
var prompt = $"Summarise this support ticket:\n{JsonSerializer.Serialize(ticket)}";One line. It works. It took eight seconds to write. And ticket has a Customer navigation property, which has an Address, and Customer has a PaymentMethods collection that Entity Framework happened to load because something earlier in the request touched it.
The alternative looked like eleven lines and a new type, so nobody wrote it. That is the entire mechanism. Not negligence, not ignorance, just the relative friction of two options at the moment someone was trying to finish a feature.
The fix has to be cheaper than the leak, and fortunately it is.
public sealed record TicketForSummary(
string Subject,
string Body,
string Category,
int AgeInDays);
public static TicketForSummary ForSummary(this Ticket t) => new(
t.Subject,
t.Body,
t.Category,
(int)(DateTime.UtcNow - t.CreatedAt).TotalDays);var prompt = $"Summarise this support ticket:\n{JsonSerializer.Serialize(ticket.ForSummary())}";That is the control. A record, an extension method, and a call site that looks almost identical to the one it replaces.
What it bought you: the customer's name, email, phone, address, account number and payment methods are now structurally incapable of reaching the model from this code path. Not filtered, not detected, not masked. Absent. No detector runs, so no detector can miss them. No token is created, so no restore can fail. The provider retains nothing, because nothing was sent.
Compare that to the alternative you were considering, which was a detector averaging 0.48 F1 on cross-domain text, called over a network, with a failure mode you have to design for.
Don't detect what you can refuse to send.
An extension method is a convention, and conventions decay. The next developer writes a new endpoint and serialises the entity again, because the entity is right there.
Make the compiler enforce it.
// Marker for types explicitly reviewed and approved for prompt inclusion.
public interface IPromptSafe;
public sealed record TicketForSummary(...) : IPromptSafe;
public sealed class PromptBuilder
{
public string Build<T>(string instruction, T payload) where T : IPromptSafe =>
$"{instruction}\n{JsonSerializer.Serialize(payload)}";
}Now Build(instruction, ticket) does not compile. The developer has to define a projection and think, for about thirty seconds, about which fields belong in it. Thirty seconds at the right moment is worth more than any amount of runtime filtering.
This is the pattern's real value. It moves the decision from "did anyone remember to redact" to "you cannot express the unsafe version", and it does so without a runtime dependency, a container, or a latency budget.
The second half of minimisation. Sometimes the model needs to refer to a person without needing to know who they are.
The instinct is to put the identifier in the prompt so that the answer can reference it. You do not need to.
// The model works on a reference. Your code resolves it.
var prompt = $"""
Draft a reply to the customer for ticket {ticket.Reference}.
Address them as {{CUSTOMER_NAME}}.
Issue: {ticket.Body}
""";
var draft = await _model.CompleteAsync(prompt, ct);
// Resolution happens here, on your side, after the model is done.
var reply = draft.Replace("{{CUSTOMER_NAME}}", customer.FullName);The model composes a letter with a slot in it. Your code fills the slot. The name never enters the prompt, never enters the provider's logs, never enters your prompt cache, and cannot be recovered from anything the model touched.
This generalises well. Any value the model needs to place rather than reason about can be a slot: names, account numbers, dates of appointments, amounts, links. Values the model needs to reason about cannot, and Chapter 10 is about telling the difference.
A caution: the slot marker must be something the model reproduces verbatim and will not helpfully expand. Double-brace tokens work because they are common in templating and models leave them alone. If the model starts filling in slots itself, tighten the instruction and validate the output.
Download the full PDF for free?
Free download — no account required