Hiding names from an AI is easy. Keeping it useful is the hard part.

redacted-context-mcp lets a coding agent work with all of your private notes, and write emails and documents that name the right people, without those names ever leaving your machine. How it works, what the first version got wrong, and why it redacts at the source.

Contents

I kept seeing people use AI tools in ways they never should, pasting in everything they had: personal data, access tokens, client identifiers, mac address of their devices etc. I built redacted-context-mcp hoping to make people more careful about what they hand to an AI, without making them give up what it is good at.

The first version was a basic MCP server and a CLI that let a coding agent list, search and read a folder of private notes. Before any text reached the agent, the server replaced names, email addresses, hostnames and other sensitive values with placeholders. The agent never opened the folder itself. It called the server’s tools and got redacted text back. That part of the design hasn’t changed.

One redaction boundaryThe coding agent sends a tool call to redctx-mcp. The server reads raw files, redaction terms and the vault salt from the private folder, redacts every field, and returns only redacted text and opaque ids. Direct file access from the agent to the private folder must fail.Agent workspaceredctx-mcp, local processPrivate folderCoding agentneutral workspaceno raw filesresolve @p_ id, readredact every fieldthe only way outraw filesredaction termsvault salttool callreadraw textterms, saltredacted text+ @p_ idsshell or file tools: must fail (separate OS user or container)
The agent works in its own folder and only talks to the MCP server. Raw text exists only inside the server process.

It is MIT licensed and has no runtime dependencies. pip install redacted-context-mcp installs one package, and the MCP protocol, the redactor and the CLI are all in it. There is no hosted service, no account and no API key. Redaction runs in the server process on your machine, and the server makes no network calls of its own. The only exceptions are opt-in: the GitHub issue tools talk to GitHub, and redctx discover talks to Ollama, which it refuses to reach over plain HTTP anywhere but localhost unless you pass --allow-remote-endpoint. The Presidio and GLiNER detectors are optional extras and run locally too.

That matters more for a privacy tool than for most software, because every package it imports runs in the process that holds your raw text. Presidio installs 57 packages and LLM Guard 83, with PyTorch and a model from Hugging Face; the comparison further down has the numbers. redacted-context-mcp is one package and about 12,000 lines of Python.

The first version#

Generic patterns caught email addresses, URLs and phone numbers. Client names, people and project codes went into a local TOML file, which redctx discover could draft from the notes with a local model through Ollama.

Placeholders were numbered in the order the server met the values. The first organisation became [ORG_01], the second [ORG_02]. That was consistent within a session, but the numbering restarted with every process, so the same client could be [ORG_01] in one session and [ORG_03] in the next.

File names were hidden behind ids such as @p_1a2b3c4d5e6f, the first 12 hex characters of a SHA-256 of the relative path. The hash had no key, so anyone could hash a guess like clients/acme/kickoff.md and compare it with an id the agent had printed.

Neither scheme survived the first week.

What the agent sees now#

Each placeholder is now an HMAC-SHA256 of the category and the normalised value, keyed with a 256-bit salt that stays on the local machine and cut to 128 bits. File ids use a keyed hash as well. The same value gets the same placeholder in every file, every session and after every restart, and nobody without the salt can compute one. There is no mapping table to store, either. When values have to be restored, the server rescans the folder with the same salt and rebuilds the map.

This is a fictional note from the project’s demo and what the agent receives for it.

Raw, stays on your machine

# Harbor Lantern Brief

Example Lantern Labs asked Fictional Harbor Studio to review the local search
prototype for PROJECT-LANTERN-042.

The prototype uses Python and SQLite. Its fictional test service is available
at https://portal.example/projects/lantern, and its internal host,
search.internal.example, is reachable only from the fictional test network.

Avery Example owns the review. Contact avery@example.com or call
+1 202-555-0147 during the fictional support window.

What the agent receives

# [PERSON_5943f8ed…]

[ORG_a5481015…] asked [ORG_48c7850e…] to review the local search
prototype for [SENSITIVE_d5020227…].

The prototype uses Python and SQLite. Its fictional test service is available
at [URL_7a5e862c…] and its internal host,
[DOMAIN_2fb528bc…], is reachable only from the fictional test network.

[PERSON_31ce220d…] owns the review. Contact [EMAIL_31ce220d…] or call
[PHONE_299f846f…] during the fictional support window.
redctx --mode balanced on the project’s fictional demo data.

Each placeholder keeps its type, so the agent can still tell an organisation from a person or a URL. Fictional Harbor Studio maps to the same placeholder in both demo files, and the agent can follow it across documents without learning the name.

One value, one placeholderFictional Harbor Studio appears in two files in the private folder. In both cases the server computes HMAC-SHA256 keyed with the vault salt over the category ORG and the value, keeps the first 128 bits, and the agent reads the same placeholder, ORG_48c7850e, in both files. The file names are replaced with opaque ids.Private folderredctx-mcp, local processWhat the agent readsharbor-lantern-brief.mdFictional Harbor Studiocontacts.mdFictional Harbor StudioHMAC-SHA256key: vault salt, 256 bitsmessage: "ORG" + valuekeep the first 128 bits@p_1ca0a50bc134[ORG_48c7850e…]@p_00dbf32a3737[ORG_48c7850e…]
The same organisation in the two demo files. The salt stays on this machine, so nothing else can compute the placeholder.

The title is redacted as a person, because consecutive capitalised words look like a name to the detector. Strict mode, the default, also removes the word “Contact” from the last line.

Writing back with the real names#

The agent can also write. It can draft an email, a reply to a client or write documentation. But we all know already that they do a better job when they have valuable context. For a while I had three bad options. Give the agent nothing and watch it guess. Paste everything and send client data to a model provider. Or clean up documents by hand, which is slow and easy to get wrong. redacted-context-mcp is the fourth option, it is solving this issue at source before before it even reaches the agent and without taking away the agents’ pens. On writes it is using placeholders, the same ones it got when reading your - redacted - sources. So wherever a name belongs it will just fill it with an identifier, so the real values can be rehydrated locally. The finished text addresses the right person by name, and the provider has only ever seen the placeholders. And the only person who can know those names are you.

What the agent wrote

Hi [PERSON_31ce220d…],

The review of the local search prototype for [SENSITIVE_d5020227…] is done.
I'll send the findings to [ORG_a5481015…] on Friday, with a copy to [EMAIL_8eb768ff…].

Thanks,
[ORG_48c7850e…]

What is restored locally

Hi Avery Example,

The review of the local search prototype for PROJECT-LANTERN-042 is done.
I'll send the findings to Example Lantern Labs on Friday, with a copy to avery@example.com.

Thanks,
Fictional Harbor Studio
redctx rehydrate on a reply the agent drafted, against the project’s fictional demo data.

There are two ways back, and neither returns the restored text to the agent. redctx rehydrate is a CLI command, not an MCP tool, and it refuses to run without --allow-raw-output. It rebuilds the map by rescanning the private folder with the same salt and config, so a redacted file on its own is not enough to restore anything. Inside an agent session, a server started with --enable-writes exposes redctx_submit_doc. The agent submits redacted text, and the server restores the values and saves the result in a configured subdirectory. Before saving, it redacts the restored text a second time, and if any restored value would survive, the write is refused.

Writing backThe agent sends redacted text. The server checks that the path is allowed (inside the write subdirectory, not a protected file, no colon) and that every placeholder resolves. It restores the real values, redacts the result again, and checks that every restored value is gone. Only then does it write the file atomically. Any failed check refuses the write and nothing is written.redctx-mcp, local processAgent sendsredacted textPath allowed?write subdir,not protected,no colonPlaceholdersresolve?Restore,redact againValues goneagain?AtomicwriteyesyesyesRefused, nothing writtennonono
The write tool, simplified from the README. It only exists when the server is started with --enable-writes, and the first failed check refuses the write.

One person, several spellings#

Rehydration exposed the next problem. A stakeholders.md names Avery Example as the product owner, and an email in the same folder is signed “Thank you, Avery”. The full name and the first name are different strings, so they got unrelated placeholders, [PERSON_31ce220d…] and [PERSON_aa2cc330…], and the agent had no way to tell that the product owner wrote the email. The email address was a third, unrelated placeholder.

Alias groups fix that. In the local config, a name can list the other forms it appears in:

toml .agent-context-redactor.toml
[redaction.aliases]
"Avery Example" = ["Avery", "avery@example.com"]

The full name keeps its placeholder. Each alias gets the same entity id followed by a keyed suffix for the form, so the agent sees one person written three ways, and searching for the entity id finds every form.

Raw, stays on your machine

# Stakeholders

Avery Example is the product owner for PROJECT-LANTERN-042.

----

Hi team,

The review is booked for Friday. Questions to avery@example.com.

Thank you,
Avery

What the agent receives

# Stakeholders

[PERSON_31ce220d…] is the product owner for [SENSITIVE_d5020227…].

----

Hi team,

The review is booked for Friday. Questions to [EMAIL_31ce220d….1ca7e6be].

Thank you,
[PERSON_31ce220d….abb2f1cf]
Two files from a fictional folder in balanced mode, with the alias group above.

The suffix is also what keeps rehydration exact. A draft signed [PERSON_31ce220d….abb2f1cf] comes back signed “Avery”, not “Avery Example”. The aliases live in the same local config as the names, which the server never serves, so linking adds no new store of raw values. A config that puts an alias on the allow list, or in two groups, is refused, and the error names the group by its position, never by its value. redctx discover can also draft the groups from the notes. It proposes a first name only when no other known person shares it, and an email address only when it spells exactly one person’s name. Every link still goes past a human before it is used, because a wrong one would tell the agent that two people are one.

Avery in three tools#

To my knowledge there is no another open-source redaction tool that links name variants, so I put the same notes through redacted-context-mcp and the two I’d expect people to reach for, Microsoft Presidio and LLM Guard. This time the folder has a second person, Anna Smith, who leads the review and is the one the email is addressed to.

All three got the same list of names: the redctx config with an alias group for each person, a deny list for Presidio, and hidden_names for LLM Guard. Without the list, Presidio’s spaCy model didn’t recognise either name and passed both through, and LLM Guard redacted “Avery” but left “Example”. For Presidio I used the encrypt operator, its reversible mode, and for LLM Guard one vault for both files, the way a proxy would see them in one session.

Raw, stays on your machine

# Stakeholders

Avery Example is the product owner for PROJECT-LANTERN-042.
Anna Smith leads the review.

----

Hi Anna,

The review is booked for Friday. Questions to avery@example.com.

Thank you,
Avery

redacted-context-mcp

# Stakeholders

[PERSON_31ce220d…] is the product owner for [SENSITIVE_d5020227…].
[PERSON_2f953128…] leads the review.

----

Hi [PERSON_2f953128….9bd68b43],

The review is booked for Friday. Questions to [EMAIL_31ce220d….1ca7e6be].

Thank you,
[PERSON_31ce220d….abb2f1cf]

Presidio 2.2

# Stakeholders

qKnBgY8t… is the product owner for 3BqFsF4U….
OJL5SE1q… leads the review.

----

Hi mfEXrlFA…,

The review is booked for Friday. Questions to LfjFfdrC….

Thank you,
F0_FbdTw…

LLM Guard 0.3

# Stakeholders
[REDACTED_PERSON_1][REDACTED_CUSTOM_1] is the product owner for [REDACTED_CUSTOM_2].[REDACTED_PERSON_1][REDACTED_CUSTOM_3] leads the review.

----

Hi [REDACTED_PERSON_2],

The review is booked for Friday. Questions to [REDACTED_EMAIL_ADDRESS_1].

Thank you,[REDACTED_PERSON_3]
The same two notes, as each tool hands them to the model. Colours show who each value really is: blue is Avery Example, violet is Anna Smith. Only in the redacted-context-mcp pane can the model see that too, because the forms share an entity id. Presidio’s tokens are cut to 8 characters.

Presidio’s output has six unrelated tokens for two people. The encryption is also randomised, so the same name gets a different token every time a file is read, and the agent can’t even match “Avery Example” in one session with “Avery Example” in the next. Restoring them needs the key, which the caller has to keep somewhere.

LLM Guard’s model tagged the line break in front of each name as a person, so [REDACTED_PERSON_1] stands for a newline, and it now sits in front of both Avery’s and Anna’s names. The line breaks it swallowed also glued the two stakeholder lines together. The vault restarts its numbering with every new process.

Writing the reply#

Then I gave each version to Claude Haiku 5.5 and asked it to write a reply, three times per tool, and restored the placeholders with each tool’s own restore step. The prompt said I was the reviewer the email was addressed to, and asked for a reply to whoever sent the email, thanking them for booking the review, addressed by name, mentioning their role on the project and signed with my name. If the notes didn’t say what the role was, it was supposed to say so instead of guessing.

The role is the part that needs the link. The notes only say what Avery Example does, and the email is signed “Avery”.

redacted-context-mcp

Hi Avery,

Thank you for booking the review for Friday. As product owner for PROJECT-LANTERN-042, your input will help keep the review focused on what matters most. I'll see you then.

Thank you,
Anna

Presidio 2.2

The notes don't say what role Avery has on the project.

Hi Avery,

Thank you for booking the review for Friday. I look forward to it. If you have any questions beforehand, please send them to avery@example.com.

Best regards,
Anna

LLM Guard 0.3

The notes don't say what role Anna has on the project.

Hi Anna,

Thank you for booking the review for Friday. Please send any questions to avery@example.com.

Thank you,

Avery
One of the three replies per tool, after each tool restored its placeholders. Claude Haiku 5.5, same prompt for all three.
Out of 3 runsredacted-context-mcpPresidioLLM Guard
Reply to the sender, right names320
…and knew the sender’s role300
Reply to the product owner, right names330

With redacted-context-mcp the model worked out every time that the person who signed the email is the product owner, and the reply came back as “Hi Avery”, the form the email used. With Presidio it got the people right but said each time that the notes didn’t give a role, and once it signed the email “[Your name]”. With LLM Guard it replied to Anna and signed as Avery in all three runs, the wrong way round. I also ran a simpler prompt that asked for a reply to “the product owner” directly. Presidio managed that, since the stakeholder line names the owner. LLM Guard got it backwards twice, and the third reply was restored as “Hi”, a line break and a comma, because the placeholder it used was the newline.

Cost of running it#

Last, the cost of each tool. I timed a clean install with uv and no cache, then redacted 200 generated notes (49 KB, ten people with their emails and first names, four project codes) in a fresh process, three times per tool. The numbers are medians, on a Ryzen 9 3900X with 64 GB of RAM, Windows 11 and Python 3.12.

redacted-context-mcpPresidio 2.2LLM Guard 0.3
Install1.0 s15.5 s42.8 s, then a 713 MB model download on first run
Packages installed15783
On disk1 MB675 MB2.0 GB + 713 MB
Start-up0.23 s4.4 s11.8 s (21 s on the first run)
Per note0.47 ms12.8 ms125 ms
Peak memory31 MB725 MB1.8 GB
CPUone coreone core21 of 24 cores
Names, emails and codes left in the output000

With the names configured, all three caught every planted value. The difference is in what they cost and in what the model can still work out from the output. LLM Guard spent 530 CPU-seconds on 49 KB of text. redacted-context-mcp matches the config and its built-in patterns, which is why it is fast and small, and also why it misses a name that is in neither. The model-based detectors described further down can be plugged in for that.

The leaks around the text#

A lot of the work after the first version went into places where a raw value could leave the server without being part of a note.

The salt was one. A copy of it pasted into a note would have been readable, so strings in the salt’s format are redacted as secrets. The config and the term files list every protected name in plain text, so they are never served. A review then found that a term file named in the config but created after the server started was missing from the protected list. The write subdirectory is excluded from the map used for restoring values, so the agent cannot feed values back into it, and the same review found that on Windows and macOS a folder called Incoming slipped past a check for incoming.

After that the leaks were in error messages. A failed GitHub request printed the name of the environment variable holding the token, and that name can contain a client’s name. A regex detector rule that is too slow gets rejected at startup, and the rejection names the rule by its number, since the text of the rule may be the name it exists to hide. One was a colon. On NTFS, terms.txt:notes addresses an alternate data stream of terms.txt, and the path checks had treated it as a different file.

Some information reaches the agent by design. It sees file sizes, line counts and timing, and GitHub issue searches are sent to GitHub exactly as typed.

Finding the names#

The built-in detectors and the config can be extended with pluggable ones: regex rules, Microsoft Presidio or GLiNER. A plugged detector can add redactions but cannot remove one. A value that none of them recognises, and that is not in the config, reaches the agent unchanged.

What gets caughtFour values from a private note. avery@example.com is caught by the built-in email detector and becomes an EMAIL placeholder. PROJECT-LANTERN-042 is in the config terms and becomes a SENSITIVE placeholder. SQLite is on the allow list and stays as it is. The codename lantern, written in lowercase, matches no detector and is not in the config, so it reaches the agent as plain text. Plugged detectors can add matches but cannot remove one.Private noteRedactorWhat the agent readsavery@example.combuilt-in detector: email[EMAIL_8eb768ff…]PROJECT-LANTERN-042config: terms[SENSITIVE_d5020227…]SQLiteconfig: allow listSQLitelanternno detector, not in configlanternPlugged detectors (regex, Presidio, GLiNER) can add matches here. They can't remove one.
Four values from one note. Balanced and strict mode treat these the same way.

Compared with the alternatives#

Proxies such as LLM Guard or LiteLLM with Presidio sit between the agent and the model API and scrub each outgoing request. They cover everything the agent sends, which is more than this project covers, and some of them restore the original values in the replies. They also have to decide what is sensitive inside source code, diffs and tool definitions, and by the time they run, the raw text is already in the agent’s process and its local session history. When a proxy restores names in a reply, those names go back into the agent’s context, and every later request depends on the proxy catching them again.

Redaction tools exposed over MCP, such as the text tools in Tonic Textual’s server, take text from the agent and return a redacted copy. To call them, the agent has to have read the raw text first.

Read hooks, such as the noirdoc plugin for Claude Code, are the closest to this design. They redact a protected file before the agent reads it. A hook belongs to one agent’s hook system, while an MCP server works the same way in Claude Code, Codex and any other MCP client.

Linking name variants is where this project stands alone among open-source tools. None of the open-source redactors I looked at does it, and the two I tested above don’t. The only tool I found that does is Limina, formerly Private AI, a commercial product whose coreference resolution links mentions such as “Robert”, “Bob” and “R. Johnson”. But even that only works within a single request, and restoring values requires a mapping the caller keeps. Alias groups are declared rather than inferred, but they hold across files, sessions and restarts without any mapping.

The difference I care about most is how values come back. The agent can produce emails, messages, replies and documentation that name the right owner, in the form the name was written, and that name never leaves the machine. The restored text is written locally and never returns to the agent’s context, and the map behind it is rebuilt from the private folder each time instead of being kept anywhere. You keep the benefits of a capable model without handing the provider the names, addresses and identifiers in your project.

I also think the source is the right place to redact. Only the private folder is redacted, so the codebase the agent edits stays exactly as it is. Placeholders are computed from the value and a local key, so they stay stable across files, sessions and restarts without a mapping table anywhere. File names, search results, GitHub issues and error messages go through the same redactor as file contents, and writes back into the folder are checked before anything is saved. The redaction itself runs locally, with no runtime dependencies and no hosted service, and all of it is MIT licensed.

The shell#

All of this assumes that the agent can reach the folder only through the server, along the solid lines in figure 1. A coding agent that can run shell commands as the same OS user that owns the folder can take the dashed line and read the raw files directly. In that setup the redaction layer is only an instruction-level guardrail. Claude Code deny rules for the folder help, but they only block the commands they list. A hard boundary requires running the agent as a separate OS user, or in a container that cannot see the folder.

Try it#

The demo uses only fictional data and needs no model or credentials:

shell
pipx install redacted-context-mcp
git clone https://github.com/zoltan0803/redacted-context-mcp.git
cd redacted-context-mcp
redctx --root examples/quickstart/context \
  --config examples/quickstart/context/demo-redaction-config.toml audit

Then point an agent at your own folder. For Claude Code, put this in the agent workspace’s .mcp.json:

json .mcp.json
{
  "mcpServers": {
    "redacted_context": {
      "type": "stdio",
      "command": "redctx-mcp",
      "args": ["--root", "/absolute/path/to/private-context"]
    }
  }
}

Codex and other stdio clients work the same way, and the server speaks both the stateless 2026-07-28 MCP protocol and the older initialize-based one.