A Little More Privacy for Your LLM Calls, Please.

10 min read Original article ↗

Deepanwadhwa

I was recently watching a friend demo a new hiring tool he was prototyping. A chatbot, with some job openings on the side bar, where a user can upload their resume to the tool and the tool will shortlist the jobs and will also converse with you about how to get the job you are interested in. The tool would also suggest courses that the user can buy etc. The underlying LLM was one of OpenAI models. He had also performed some optimizations such as batch processing of jobs to reduce the costs. The tool appeared fast and seamless and I asked a few questions such as, “Who will pay for this etc”, the usual stuff.

But I forgot to ask something so so important…

In the time it took to get that initial list of jobs, the entire resume — candidate’s name, phone number, personal email, their private job-hunt status — left the laptop and landed on OpenAI’s servers. Just like that. My friend wasn’t being malicious but he was just moving fast, as developers do. But the ease with which it happened was a bit unsettling, I think? It made me wonder, why don’t we ever slow down long enough to mask the sensitive bits before we hit “Send”?

Press enter or click to view image in full size

The Terms of the agreement will fork you hard one day.

Public-ish Isn’t the Same as Public

Now, you might be thinking, “So what? Most of that stuff is on LinkedIn anyway.” And that’s a fair point, but I think it misses the forest for the trees. The bottom line is that the data itself is only half the story.

First, there’s the whole issue of context. A LinkedIn profile just sits there, saying, “Here’s my professional history for anyone interested in networking.” A resume sent for a job application says something much more private: “I am privately applying for this specific job right now.” That context changes everything. It’s a whole different ball game.

Then you’ve got consent. The user is putting their info on LinkedIn for recruiters to see, not for an application developer to forward to a third-party AI vendor. The moment you send that data on, you’ve created a new digital footprint that your user, the owner of the data, has zero control over. A tough pill to swallow.

And the real kicker? Aggregation. A formal resume bundles everything together — public info, private contact details, non-public work history — into one neat, vulnerable package that is far more sensitive than its individual parts.

The Search for a Practical Solution

I had run into the issue of Quasi identifiers of privacy earlier this year and was looking to Privacy Enhancing Technologies (PETs) anyway. But now I wanted to build some sort of tooling for this. There is a ton of research that has been done and is going on. There is this wonderful survey that you can read if you are interested, but I will also summarize(poorly) it a bit here:

You’ve got Differential Privacy (DP), which adds mathematical “noise” to data to protect individuals. Sounds great, right? The catch is, that “noise” can really mess with your model’s performance. Often, the more privacy you add, the less useful the results get. It’s a constant, frustrating trade-off.

Then you have the cryptographic heavyweights like Homomorphic Encryption (HE), which lets you compute on encrypted data. The problem? It is, to put it mildly, slow. We’re talking computationally expensive to the point of being impractical for the kind of real-time API calls most of us are making. HE is also mathematically lagging for any practical purposes, so far you can perform additions on encrypted bytes but no multiplications yet. I might be oversimplifying here but that’s the gist.

But what about Federated Learning (FL)? It’s a brilliant idea where a model gets trained across many different user devices without the raw data ever leaving them. But here’s the thing — that doesn’t solve this problem. FL is for training your own model collaboratively. It does nothing to stop your application from sending a single, sensitive prompt to a third-party, pre-existing API like OpenAI’s. It’s the wrong tool for this specific job.

And the most direct approach of all: Anonymization. The idea is simple: find the sensitive stuff and remove it. A recent academic review laid out a useful distinction: de-identification is about removing the obvious things like names and addresses , while anonymization is the bigger goal of removing anything that could indirectly point back to a person.

On the surface, this sounds like the perfect, practical solution. The problem is, it’s notoriously leaky. The same paper reminded me of the classic Netflix prize story, where researchers famously de-anonymized a dataset of “anonymous” movie ratings just by cross-referencing it with public IMDb data. Anonymization is vulnerable to re-identification if an attacker can get their hands on other public datasets. It’s not a silver bullet.

The bottom line was that the perfect, all-in-one solution didn’t seem to exist but I still wanted to try to build something.

Meet ZINK

It’s a small, open-source Python library designed to do one job well: give your data a disguise before it leaves home.

Think of it as a little checkpoint that sits between your code and any external API. It finds the sensitive stuff, scrubs or swaps it out, and then puts it all back together when the response comes home. You get to decide how you want to play it.

For starters, you can just black out the sensitive bits. Simple.

Python

import zink as zn
text   = "John works as a doctor and plays football after work and drives a Toyota."
labels = ("person", "profession", "sport", "car")
print(zn.redact(text, labels).anonymized_text)
# → person_REDACTED works as a profession_REDACTED ...

But sometimes, that breaks the flow of a sentence. So, what if you swapped the real data with fake-but-plausible data instead? ZINK can do that, too.

Python

import zink as zn
text   = "Patient, 33 years old, was admitted with chest pain."
labels = ("age", "medical condition")
print(zn.replace(text, labels).anonymized_text)
# → Patient, 78 years old, was admitted with Diabetes Mellitus.

And if you need total control, you can bring your own pool of pseudonyms to the party.

Python

import zink as zn
text   = "Melissa works at Google and drives a Tesla."
labels = ("person", "company", "car")
custom = {
"person": "Alice",
"company": "OpenAI",
"car": ("Honda", "Toyota")
}
print(zn.replace_with_my_data(text, labels, user_replacements=custom).anonymized_text)
# → Alice works at OpenAI and drives a Honda.

Catching the Tricky Cases

Every developer knows that feeling when you find an annoying edge case that breaks your beautiful logic. For ZINK, that was discovering that the underlying NER model would sometimes correctly flag a full name like “John Doe” but then completely miss a later, standalone mention of just “John.”

To fix that, ZINK does a little multi-pass trick behind the scenes:

  • First, it does a quick pass with a normal confidence setting to grab the obvious stuff.
  • Then, it temporarily blanks out those findings in memory.
  • Finally, it runs more passes with a higher confidence threshold on what’s left over.

This lets it zero in on those harder-to-find partial repeats without second-guessing its initial, high-quality finds. A simple trick. But effective.

The One-Line Solution: The @zink.shield Decorator

After building all that logic, the final piece of the puzzle was making it dead simple to use. I mean, what’s the point of a privacy tool if it’s a pain to implement?

Enter the @zink.shield decorator.

It automates the whole anonymization-re-identification cycle. You just wrap any function that makes an external API call, and ZINK handles the rest. Here’s a real play-by-play of it in action with a logic puzzle full of names.

Plaintext


from google import genai
import json
import os
from google.genai import types # Keep this import
import zink as zn

# --- Configuration and Constants ---

api_key = os.environ.get("GEMINI")

# Initialize the client object
try:
client = genai.Client(api_key=api_key)
except AttributeError:
print("Client object structure not found, trying genai.configure...")
raise ImportError("Detected potential older google-generativeai library structure or initialization issue. Please update the library or adjust the client initialization.")
except Exception as init_err:
print(f"Error initializing the GenAI client: {init_err}")
raise

@zn.shield(target_arg="prompt", labels = ["person-name","address"])
def call_gemini(prompt: str):
"""
Generates structured JSON using the client.models.generate_content method.
Uses gemini-2.0-flash-latest model.
"""
# Define the model name to use
print(50 * "-")
print("Prompt being sent to Google : ", prompt)
model_name = "models/gemini-2.0-flash" # Use the full model path
gen_config = types.GenerateContentConfig(
temperature=0.8,
system_instruction="You are a helpful assistant."
)

response = client.models.generate_content(
model=model_name,
contents=prompt, # Pass the dynamically constructed prompt
config=gen_config # Keep using 'config' param name as requested, passing the gen_config object
# NO generation_config parameter (Comment kept as requested)
)

raw_text = response.text.strip()
print(50 * "-")
print("Raw response from Gemini : ", raw_text)

return raw_text

example_prompt = "Hello. How are you?. My name is John Doe. I live at 123 Main St, Springfield."
example_prompt2 = "Harry and John are brothers. Harry has a sister named Mary. Mary is older than Harry but younger than John. Who is the oldest sibling?"
print("Example prompt: ", example_prompt2)
print(50 * "-")
print("Zinked Response from Gemini : ", call_gemini(prompt = example_prompt2))

Example prompt:  Harry and John are brothers. Harry has a sister named Mary. Mary is older than Harry but younger than John. Who is the oldest sibling?
--------------------------------------------------
Prompt being sent to Google : person-name_6834_REDACTED and person-name_1904_REDACTED are brothers. person-name_6834_REDACTED has a sister named person-name_1793_REDACTED. person-name_1793_REDACTED is older than person-name_6834_REDACTED but younger than person-name_1904_REDACTED. Who is the oldest sibling?
--------------------------------------------------
Raw response from Gemini : Here's the breakdown:
* person-name_6834_REDACTED and person-name_1904_REDACTED are brothers.
* person-name_1793_REDACTED is their sister.
* person-name_1793_REDACTED is older than person-name_6834_REDACTED but younger than person-name_1904_REDACTED.
Therefore, person-name_1904_REDACTED is the oldest sibling.
--------------------------------------------------
Zinked Response from Gemini : Here's the breakdown:
* Harry and John are brothers.
* Mary is their sister.
* Mary is older than Harry but younger than John.
Therefore, John is the oldest sibling.

The decorator did all the work. It anonymized the outgoing prompt, let the LLM solve the puzzle using only placeholders, and then seamlessly patched the real names back into the final response. All of it, invisible to the rest of the code.

This doesn’t mean every API call needs to be anonymized. A trivia question about a public figure like Jimmy Carter, for instance, would be needlessly obscured for the LLM. ZINK is intended for situations where you or your users are sending genuinely private information. In those cases, passing the data through ZINK first might provide a crucial privacy filter and it might help you sell your product better to your clients.

What ZINK Can’t Promise

Now, let’s get real for a second. Is this a silver bullet? Absolutely not. And I

think it’s important to be upfront about that.

  • It can still miss an edge case. No NER model is perfect.
  • A really sophisticated model might be able to re-identify someone from context clues alone.
  • And if you’re up against a determined attacker, all bets are off.

Think of ZINK as a decent lock on your door, not a bank vault. But here’s the thing: locks matter. They stop casual leaks. They raise the bar. They force the bad actors to actually have to work for it.

At the end of the day, this isn’t about some grand, abstract debate on AI ethics. It’s about a simple, practical step we can all take as developers.

The next time an LLM demo feels like magic, just pause. Ask yourself if that prompt contains anything you wouldn’t want in a public GitHub issue. If the answer is yes, give it a mask first.

Move fast, by all means. We all have to. But maybe add a little more privacy to your LLM calls, Please.