<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Zoltán Nagy</title><link>https://nzoli.com/</link><description>Thoughts and experiments on ai, coding agents, cybersecurity, guardrais and everything else I experiment with.</description><generator>Hugo</generator><language>en-GB</language><copyright>© 2026 Zoltán Nagy</copyright><lastBuildDate>Wed, 14 Oct 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://nzoli.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Hiding names from an AI is easy. Keeping it useful is the hard part.</title><link>https://nzoli.com/redacted-context-mcp/</link><guid isPermaLink="true">https://nzoli.com/redacted-context-mcp/</guid><pubDate>Wed, 14 Oct 2026 00:00:00 +0200</pubDate><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Zoltán Nagy</dc:creator><category>mcp</category><category>privacy</category><category>coding-agents</category><category>python</category><description>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, …</description><content:encoded>&lt;p&gt;I kept seeing people use AI tools in ways they never should, pasting in everything they had: personal data, access tokens, client identifiers, &lt;a href="https://www.bitdefender.com/en-us/blog/hotforsecurity/openai-engineer-arrested-spy-device#:~:text=Investigators%20also%20found%20prompts%20referencing%20specific%20characteristics%20of%20the%20discovered%20device%2C%20including%20its%20custom%20header%20and%20unique%20MAC%20address."&gt;mac address of their devices&lt;/a&gt; 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.&lt;/p&gt;&#10;&lt;p&gt;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&amp;rsquo;s tools and got redacted text back. That part of the design hasn&amp;rsquo;t changed.&lt;/p&gt;&#10;&lt;figure class="diagram numbered"&gt;&#10; &lt;div class="diagram-canvas"&gt;&#10; &lt;svg class="d-svg" viewBox="0 0 760 310" role="img" aria-labelledby="bnd-title bnd-desc" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, monospace" font-size="12"&gt;&#10; &lt;title id="bnd-title"&gt;One redaction boundary&lt;/title&gt;&#10; &lt;desc id="bnd-desc"&gt;The 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.&lt;/desc&gt;&#10;&#10; &lt;!-- Zones. Presentation attributes are fallbacks for feed readers; the site CSS overrides them. --&gt;&#10; &lt;text class="d-title" x="10" y="25" fill="#6f6a60" font-size="10"&gt;Agent workspace&lt;/text&gt;&#10; &lt;rect class="d-zone" x="10" y="38" width="160" height="200" rx="3" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;text class="d-title" x="262" y="25" fill="#6f6a60" font-size="10"&gt;redctx-mcp, local process&lt;/text&gt;&#10; &lt;rect class="d-zone" x="262" y="38" width="236" height="200" rx="3" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;text class="d-title" x="590" y="25" fill="#6f6a60" font-size="10"&gt;Private folder&lt;/text&gt;&#10; &lt;rect class="d-zone d-zone--raw" x="590" y="38" width="160" height="200" rx="3" fill="#f6e0dc" stroke="#b3261e" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;!-- Agent --&gt;&#10; &lt;rect class="d-box d-box--agent" x="26" y="70" width="128" height="140" rx="2" fill="#e2e9f5" stroke="#1f5fbf"/&gt;&#10; &lt;text x="90" y="128" text-anchor="middle" fill="#1d1b17"&gt;Coding agent&lt;/text&gt;&#10; &lt;text class="d-muted" x="90" y="148" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;neutral workspace&lt;/text&gt;&#10; &lt;text class="d-muted" x="90" y="163" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;no raw files&lt;/text&gt;&#10;&#10; &lt;!-- Server --&gt;&#10; &lt;rect class="d-box" x="280" y="70" width="200" height="44" rx="2" fill="#f6f4ef" stroke="#1d1b17"/&gt;&#10; &lt;text x="380" y="96" text-anchor="middle" fill="#1d1b17"&gt;resolve @p_ id, read&lt;/text&gt;&#10;&#10; &lt;rect class="d-box d-box--step" x="280" y="166" width="200" height="44" rx="2" fill="#ebe4f6" stroke="#6a3dbf"/&gt;&#10; &lt;rect class="d-boundary" x="276" y="166" width="4" height="44" fill="#0f607d"/&gt;&#10; &lt;text x="380" y="192" text-anchor="middle" fill="#1d1b17"&gt;redact every field&lt;/text&gt;&#10; &lt;text class="d-muted" x="380" y="229" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;the only way out&lt;/text&gt;&#10;&#10; &lt;!-- Private folder --&gt;&#10; &lt;rect class="d-box d-box--raw" x="606" y="70" width="128" height="36" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="670" y="92" text-anchor="middle" fill="#1d1b17"&gt;raw files&lt;/text&gt;&#10; &lt;rect class="d-box d-box--raw" x="606" y="122" width="128" height="36" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="670" y="144" text-anchor="middle" fill="#1d1b17"&gt;redaction terms&lt;/text&gt;&#10; &lt;rect class="d-box d-box--raw" x="606" y="174" width="128" height="36" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="670" y="196" text-anchor="middle" fill="#1d1b17"&gt;vault salt&lt;/text&gt;&#10;&#10; &lt;!-- Tool call: agent to server --&gt;&#10; &lt;line class="d-line" x1="154" y1="92" x2="272" y2="92" stroke="#4f4b43" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head" points="280,92 272,88 272,96" fill="#4f4b43"/&gt;&#10; &lt;text class="d-muted" x="216" y="84" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;tool call&lt;/text&gt;&#10;&#10; &lt;!-- Read: server to private folder --&gt;&#10; &lt;line class="d-line d-line--raw" x1="480" y1="92" x2="582" y2="92" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="590,92 582,88 582,96" fill="#b3261e"/&gt;&#10; &lt;text class="d-muted" x="544" y="84" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;read&lt;/text&gt;&#10;&#10; &lt;!-- Raw text, terms and salt back into the redactor --&gt;&#10; &lt;line class="d-line d-line--raw" x1="590" y1="188" x2="488" y2="188" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="480,188 488,184 488,192" fill="#b3261e"/&gt;&#10; &lt;text class="d-muted" x="544" y="180" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;raw text&lt;/text&gt;&#10; &lt;text class="d-muted" x="544" y="205" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;terms, salt&lt;/text&gt;&#10;&#10; &lt;!-- Redacted text back to the agent --&gt;&#10; &lt;line class="d-line d-line--safe" x1="276" y1="188" x2="162" y2="188" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="154,188 162,184 162,192" fill="#2f7a3b"/&gt;&#10; &lt;text class="d-muted" x="216" y="180" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;redacted text&lt;/text&gt;&#10; &lt;text class="d-muted" x="216" y="205" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;+ @p_ ids&lt;/text&gt;&#10;&#10; &lt;!-- Direct access: must fail --&gt;&#10; &lt;path class="d-line d-line--blocked" d="M90 210 V272 H670 V248" fill="none" stroke="#b3261e" stroke-width="1.25" stroke-dasharray="4 4"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="670,240 666,248 674,248" fill="#b3261e"/&gt;&#10; &lt;g transform="translate(380 272)"&gt;&#10; &lt;circle class="d-x-bg" r="9" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;path class="d-x-mark" d="M-3.5 -3.5 L3.5 3.5 M3.5 -3.5 L-3.5 3.5" stroke="#b3261e" stroke-width="1.5"/&gt;&#10; &lt;/g&gt;&#10; &lt;text class="d-muted" x="380" y="299" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;shell or file tools: must fail (separate OS user or container)&lt;/text&gt;&#10;&lt;/svg&gt;&#10;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;The agent works in its own folder and only talks to the MCP server. Raw text exists only inside the server process.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;It is MIT licensed and has no runtime dependencies. &lt;code&gt;pip install redacted-context-mcp&lt;/code&gt; 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 &lt;code&gt;redctx discover&lt;/code&gt; talks to Ollama, which it refuses to reach over plain HTTP anywhere but localhost unless you pass &lt;code&gt;--allow-remote-endpoint&lt;/code&gt;. The Presidio and GLiNER detectors are optional extras and run locally too.&lt;/p&gt;&#10;&lt;p&gt;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; &lt;a href="#cost-of-running-it"&gt;the comparison further down&lt;/a&gt; has the numbers. redacted-context-mcp is one package and about 12,000 lines of Python.&lt;/p&gt;&#10;&lt;h2 id="the-first-version"&gt;The first version&lt;a class="anchor" href="#the-first-version" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Generic patterns caught email addresses, URLs and phone numbers. Client names, people and project codes went into a local TOML file, which &lt;code&gt;redctx discover&lt;/code&gt; could draft from the notes with a local model through Ollama.&lt;/p&gt;&#10;&lt;p&gt;Placeholders were numbered in the order the server met the values. The first organisation became &lt;code&gt;[ORG_01]&lt;/code&gt;, the second &lt;code&gt;[ORG_02]&lt;/code&gt;. That was consistent within a session, but the numbering restarted with every process, so the same client could be &lt;code&gt;[ORG_01]&lt;/code&gt; in one session and &lt;code&gt;[ORG_03]&lt;/code&gt; in the next.&lt;/p&gt;&#10;&lt;p&gt;File names were hidden behind ids such as &lt;code&gt;@p_1a2b3c4d5e6f&lt;/code&gt;, 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 &lt;code&gt;clients/acme/kickoff.md&lt;/code&gt; and compare it with an id the agent had printed.&lt;/p&gt;&#10;&lt;p&gt;Neither scheme survived the first week.&lt;/p&gt;&#10;&lt;h2 id="what-the-agent-sees-now"&gt;What the agent sees now&lt;a class="anchor" href="#what-the-agent-sees-now" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;p&gt;This is a fictional note from the project&amp;rsquo;s demo and what the agent receives for it.&lt;/p&gt;&#10;&lt;figure class="compare wide numbered"&gt;&#10; &lt;div class="compare-grid"&gt;&#10; &lt;div class="compare-pane compare-pane--raw"&gt;&lt;p class="compare-label"&gt;Raw, stays on your machine&lt;/p&gt;&lt;pre&gt;# &lt;mark class="pii"&gt;Harbor Lantern Brief&lt;/mark&gt;&#10;&#10;&lt;mark class="pii"&gt;Example Lantern Labs&lt;/mark&gt; asked &lt;mark class="pii"&gt;Fictional Harbor Studio&lt;/mark&gt; to review the local search&#10;prototype for &lt;mark class="pii"&gt;PROJECT-LANTERN-042&lt;/mark&gt;.&#10;&#10;The prototype uses Python and SQLite. Its fictional test service is available&#10;at &lt;mark class="pii"&gt;https://portal.example/projects/lantern&lt;/mark&gt;, and its internal host,&#10;&lt;mark class="pii"&gt;search.internal.example&lt;/mark&gt;, is reachable only from the fictional test network.&#10;&#10;&lt;mark class="pii"&gt;Avery Example&lt;/mark&gt; owns the review. Contact &lt;mark class="pii"&gt;avery@example.com&lt;/mark&gt; or call&#10;&lt;mark class="pii"&gt;+1 202-555-0147&lt;/mark&gt; during the fictional support window.&lt;/pre&gt;&lt;/div&gt;&#10; &lt;div class="compare-arrow" aria-hidden="true"&gt;&lt;/div&gt;&#10; &lt;div class="compare-pane compare-pane--safe"&gt;&lt;p class="compare-label"&gt;What the agent receives&lt;/p&gt;&lt;pre&gt;# &lt;span class="ph"&gt;[PERSON_5943f8ed…]&lt;/span&gt;&#10;&#10;&lt;span class="ph"&gt;[ORG_a5481015…]&lt;/span&gt; asked &lt;span class="ph"&gt;[ORG_48c7850e…]&lt;/span&gt; to review the local search&#10;prototype for &lt;span class="ph"&gt;[SENSITIVE_d5020227…]&lt;/span&gt;.&#10;&#10;The prototype uses Python and SQLite. Its fictional test service is available&#10;at &lt;span class="ph"&gt;[URL_7a5e862c…]&lt;/span&gt; and its internal host,&#10;&lt;span class="ph"&gt;[DOMAIN_2fb528bc…]&lt;/span&gt;, is reachable only from the fictional test network.&#10;&#10;&lt;span class="ph"&gt;[PERSON_31ce220d…]&lt;/span&gt; owns the review. Contact &lt;span class="ph"&gt;[EMAIL_31ce220d…]&lt;/span&gt; or call&#10;&lt;span class="ph"&gt;[PHONE_299f846f…]&lt;/span&gt; during the fictional support window.&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;&lt;code&gt;redctx --mode balanced&lt;/code&gt; on the project&amp;rsquo;s fictional demo data.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;figure class="diagram numbered"&gt;&#10; &lt;div class="diagram-canvas"&gt;&#10; &lt;svg class="d-svg" viewBox="0 0 760 250" role="img" aria-labelledby="ph-title ph-desc" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, monospace" font-size="12"&gt;&#10; &lt;title id="ph-title"&gt;One value, one placeholder&lt;/title&gt;&#10; &lt;desc id="ph-desc"&gt;Fictional 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.&lt;/desc&gt;&#10;&#10; &lt;!-- Zones. Presentation attributes are fallbacks for feed readers; the site CSS overrides them. --&gt;&#10; &lt;text class="d-title" x="10" y="25" fill="#6f6a60" font-size="10"&gt;Private folder&lt;/text&gt;&#10; &lt;rect class="d-zone d-zone--raw" x="10" y="38" width="210" height="196" rx="3" fill="#f6e0dc" stroke="#b3261e" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;text class="d-title" x="282" y="25" fill="#6f6a60" font-size="10"&gt;redctx-mcp, local process&lt;/text&gt;&#10; &lt;rect class="d-zone" x="282" y="38" width="230" height="196" rx="3" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;text class="d-title" x="574" y="25" fill="#6f6a60" font-size="10"&gt;What the agent reads&lt;/text&gt;&#10; &lt;rect class="d-zone" x="574" y="38" width="176" height="196" rx="3" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;!-- Raw files --&gt;&#10; &lt;rect class="d-box d-box--raw" x="26" y="62" width="178" height="62" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text class="d-muted" x="115" y="85" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;harbor-lantern-brief.md&lt;/text&gt;&#10; &lt;text x="115" y="106" text-anchor="middle" fill="#1d1b17"&gt;Fictional Harbor Studio&lt;/text&gt;&#10;&#10; &lt;rect class="d-box d-box--raw" x="26" y="147" width="178" height="62" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text class="d-muted" x="115" y="170" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;contacts.md&lt;/text&gt;&#10; &lt;text x="115" y="191" text-anchor="middle" fill="#1d1b17"&gt;Fictional Harbor Studio&lt;/text&gt;&#10;&#10; &lt;!-- HMAC --&gt;&#10; &lt;rect class="d-box d-box--step" x="300" y="62" width="194" height="70" rx="2" fill="#ebe4f6" stroke="#6a3dbf"/&gt;&#10; &lt;text x="397" y="85" text-anchor="middle" fill="#1d1b17"&gt;HMAC-SHA256&lt;/text&gt;&#10; &lt;text class="d-muted" x="397" y="104" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;key: vault salt, 256 bits&lt;/text&gt;&#10; &lt;text class="d-muted" x="397" y="120" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;message: "ORG" + value&lt;/text&gt;&#10;&#10; &lt;rect class="d-box" x="300" y="160" width="194" height="36" rx="2" fill="#f6f4ef" stroke="#1d1b17"/&gt;&#10; &lt;text x="397" y="182" text-anchor="middle" fill="#1d1b17"&gt;keep the first 128 bits&lt;/text&gt;&#10;&#10; &lt;!-- Agent view --&gt;&#10; &lt;rect class="d-box d-box--agent" x="590" y="62" width="144" height="62" rx="2" fill="#e2e9f5" stroke="#1f5fbf"/&gt;&#10; &lt;text class="d-muted" x="662" y="85" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;@p_1ca0a50bc134&lt;/text&gt;&#10; &lt;rect class="d-ph" x="606" y="94" width="112" height="18" rx="2" fill="#1d1b17"/&gt;&#10; &lt;text class="d-ph-text" x="662" y="107" text-anchor="middle" fill="#f6f4ef" font-size="10.5"&gt;[ORG_48c7850e…]&lt;/text&gt;&#10;&#10; &lt;rect class="d-box d-box--agent" x="590" y="147" width="144" height="62" rx="2" fill="#e2e9f5" stroke="#1f5fbf"/&gt;&#10; &lt;text class="d-muted" x="662" y="170" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;@p_00dbf32a3737&lt;/text&gt;&#10; &lt;rect class="d-ph" x="606" y="179" width="112" height="18" rx="2" fill="#1d1b17"/&gt;&#10; &lt;text class="d-ph-text" x="662" y="192" text-anchor="middle" fill="#f6f4ef" font-size="10.5"&gt;[ORG_48c7850e…]&lt;/text&gt;&#10;&#10; &lt;!-- Both raw values into the HMAC --&gt;&#10; &lt;path class="d-line d-line--raw" d="M204 93 H292" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="300,93 292,89 292,97" fill="#b3261e"/&gt;&#10; &lt;path class="d-line d-line--raw" d="M204 178 H252 V112 H292" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="300,112 292,108 292,116" fill="#b3261e"/&gt;&#10;&#10; &lt;!-- HMAC to truncation --&gt;&#10; &lt;path class="d-line" d="M397 132 V152" fill="none" stroke="#4f4b43" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head" points="397,160 393,152 401,152" fill="#4f4b43"/&gt;&#10;&#10; &lt;!-- Same placeholder into both files --&gt;&#10; &lt;path class="d-line d-line--safe" d="M494 178 H582" fill="none" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="590,178 582,174 582,182" fill="#2f7a3b"/&gt;&#10; &lt;path class="d-line d-line--safe" d="M540 178 V93 H582" fill="none" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="590,93 582,89 582,97" fill="#2f7a3b"/&gt;&#10;&lt;/svg&gt;&#10;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;The same organisation in the two demo files. The salt stays on this machine, so nothing else can compute the placeholder.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;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 &amp;ldquo;Contact&amp;rdquo; from the last line.&lt;/p&gt;&#10;&lt;h2 id="writing-back-with-the-real-names"&gt;Writing back with the real names&lt;a class="anchor" href="#writing-back-with-the-real-names" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;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&amp;rsquo; 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.&lt;/p&gt;&#10;&lt;figure class="compare wide numbered"&gt;&#10; &lt;div class="compare-grid"&gt;&#10; &lt;div class="compare-pane compare-pane--safe"&gt;&lt;p class="compare-label"&gt;What the agent wrote&lt;/p&gt;&lt;pre&gt;Hi &lt;span class="ph"&gt;[PERSON_31ce220d…]&lt;/span&gt;,&#10;&#10;The review of the local search prototype for &lt;span class="ph"&gt;[SENSITIVE_d5020227…]&lt;/span&gt; is done.&#10;I&amp;#39;ll send the findings to &lt;span class="ph"&gt;[ORG_a5481015…]&lt;/span&gt; on Friday, with a copy to &lt;span class="ph"&gt;[EMAIL_8eb768ff…]&lt;/span&gt;.&#10;&#10;Thanks,&#10;&lt;span class="ph"&gt;[ORG_48c7850e…]&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;div class="compare-arrow" aria-hidden="true"&gt;&lt;/div&gt;&#10; &lt;div class="compare-pane compare-pane--raw"&gt;&lt;p class="compare-label"&gt;What is restored locally&lt;/p&gt;&lt;pre&gt;Hi &lt;mark class="pii"&gt;Avery Example&lt;/mark&gt;,&#10;&#10;The review of the local search prototype for &lt;mark class="pii"&gt;PROJECT-LANTERN-042&lt;/mark&gt; is done.&#10;I&amp;#39;ll send the findings to &lt;mark class="pii"&gt;Example Lantern Labs&lt;/mark&gt; on Friday, with a copy to &lt;mark class="pii"&gt;avery@example.com&lt;/mark&gt;.&#10;&#10;Thanks,&#10;&lt;mark class="pii"&gt;Fictional Harbor Studio&lt;/mark&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;&lt;code&gt;redctx rehydrate&lt;/code&gt; on a reply the agent drafted, against the project&amp;rsquo;s fictional demo data.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;There are two ways back, and neither returns the restored text to the agent. &lt;code&gt;redctx rehydrate&lt;/code&gt; is a CLI command, not an MCP tool, and it refuses to run without &lt;code&gt;--allow-raw-output&lt;/code&gt;. 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 &lt;code&gt;--enable-writes&lt;/code&gt; exposes &lt;code&gt;redctx_submit_doc&lt;/code&gt;. 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.&lt;/p&gt;&#10;&lt;figure class="diagram numbered"&gt;&#10; &lt;div class="diagram-canvas"&gt;&#10; &lt;svg class="d-svg" viewBox="0 0 852 222" role="img" aria-labelledby="wb-title wb-desc" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, monospace" font-size="12"&gt;&#10; &lt;title id="wb-title"&gt;Writing back&lt;/title&gt;&#10; &lt;desc id="wb-desc"&gt;The 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.&lt;/desc&gt;&#10;&#10; &lt;!-- Zone. Presentation attributes are fallbacks for feed readers; the site CSS overrides them. --&gt;&#10; &lt;text class="d-title" x="144" y="25" fill="#6f6a60" font-size="10"&gt;redctx-mcp, local process&lt;/text&gt;&#10; &lt;rect class="d-zone" x="144" y="38" width="704" height="174" rx="3" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;rect class="d-box d-box--agent" x="10" y="54" width="112" height="70" rx="2" fill="#e2e9f5" stroke="#1f5fbf"/&gt;&#10; &lt;text x="66" y="85" text-anchor="middle" fill="#1d1b17"&gt;Agent sends&lt;/text&gt;&#10; &lt;text x="66" y="101" text-anchor="middle" fill="#1d1b17"&gt;redacted text&lt;/text&gt;&#10; &lt;rect class="d-box" x="154" y="54" width="112" height="70" rx="2" fill="#f6f4ef" stroke="#1d1b17"/&gt;&#10; &lt;text x="210" y="75" text-anchor="middle" fill="#1d1b17"&gt;Path allowed?&lt;/text&gt;&#10; &lt;text class="d-muted" x="210" y="92" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;write subdir,&lt;/text&gt;&#10; &lt;text class="d-muted" x="210" y="105" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;not protected,&lt;/text&gt;&#10; &lt;text class="d-muted" x="210" y="118" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;no colon&lt;/text&gt;&#10; &lt;rect class="d-box" x="298" y="54" width="112" height="70" rx="2" fill="#f6f4ef" stroke="#1d1b17"/&gt;&#10; &lt;text x="354" y="85" text-anchor="middle" fill="#1d1b17"&gt;Placeholders&lt;/text&gt;&#10; &lt;text x="354" y="101" text-anchor="middle" fill="#1d1b17"&gt;resolve?&lt;/text&gt;&#10; &lt;rect class="d-box d-box--step" x="442" y="54" width="112" height="70" rx="2" fill="#ebe4f6" stroke="#6a3dbf"/&gt;&#10; &lt;text x="498" y="85" text-anchor="middle" fill="#1d1b17"&gt;Restore,&lt;/text&gt;&#10; &lt;text x="498" y="101" text-anchor="middle" fill="#1d1b17"&gt;redact again&lt;/text&gt;&#10; &lt;rect class="d-box" x="586" y="54" width="112" height="70" rx="2" fill="#f6f4ef" stroke="#1d1b17"/&gt;&#10; &lt;text x="642" y="85" text-anchor="middle" fill="#1d1b17"&gt;Values gone&lt;/text&gt;&#10; &lt;text x="642" y="101" text-anchor="middle" fill="#1d1b17"&gt;again?&lt;/text&gt;&#10; &lt;rect class="d-box d-box--safe" x="730" y="54" width="112" height="70" rx="2" fill="#dcebdc" stroke="#2f7a3b"/&gt;&#10; &lt;text x="786" y="85" text-anchor="middle" fill="#1d1b17"&gt;Atomic&lt;/text&gt;&#10; &lt;text x="786" y="101" text-anchor="middle" fill="#1d1b17"&gt;write&lt;/text&gt;&#10;&#10; &lt;path class="d-line" d="M122 89 H146" fill="none" stroke="#4f4b43" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head" points="154,89 146,85 146,93" fill="#4f4b43"/&gt;&#10; &lt;path class="d-line d-line--safe" d="M266 89 H290" fill="none" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="298,89 290,85 290,93" fill="#2f7a3b"/&gt;&#10; &lt;text class="d-muted" x="282" y="81" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;yes&lt;/text&gt;&#10; &lt;path class="d-line d-line--safe" d="M410 89 H434" fill="none" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="442,89 434,85 434,93" fill="#2f7a3b"/&gt;&#10; &lt;text class="d-muted" x="426" y="81" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;yes&lt;/text&gt;&#10; &lt;path class="d-line" d="M554 89 H578" fill="none" stroke="#4f4b43" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head" points="586,89 578,85 578,93" fill="#4f4b43"/&gt;&#10; &lt;path class="d-line d-line--safe" d="M698 89 H722" fill="none" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="730,89 722,85 722,93" fill="#2f7a3b"/&gt;&#10; &lt;text class="d-muted" x="714" y="81" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;yes&lt;/text&gt;&#10;&#10; &lt;rect class="d-box d-box--raw" x="154" y="164" width="544" height="32" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="426" y="184" text-anchor="middle" fill="#1d1b17"&gt;Refused, nothing written&lt;/text&gt;&#10; &lt;path class="d-line d-line--raw" d="M210 124 V156" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="210,164 206,156 214,156" fill="#b3261e"/&gt;&#10; &lt;text class="d-muted" x="218" y="148" fill="#6f6a60" font-size="10.5"&gt;no&lt;/text&gt;&#10; &lt;path class="d-line d-line--raw" d="M354 124 V156" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="354,164 350,156 358,156" fill="#b3261e"/&gt;&#10; &lt;text class="d-muted" x="362" y="148" fill="#6f6a60" font-size="10.5"&gt;no&lt;/text&gt;&#10; &lt;path class="d-line d-line--raw" d="M642 124 V156" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="642,164 638,156 646,156" fill="#b3261e"/&gt;&#10; &lt;text class="d-muted" x="650" y="148" fill="#6f6a60" font-size="10.5"&gt;no&lt;/text&gt;&#10;&lt;/svg&gt;&#10;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;The write tool, simplified from the README. It only exists when the server is started with &lt;code&gt;--enable-writes&lt;/code&gt;, and the first failed check refuses the write.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;h2 id="one-person-several-spellings"&gt;One person, several spellings&lt;a class="anchor" href="#one-person-several-spellings" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Rehydration exposed the next problem. A &lt;code&gt;stakeholders.md&lt;/code&gt; names Avery Example as the product owner, and an email in the same folder is signed &amp;ldquo;Thank you, Avery&amp;rdquo;. The full name and the first name are different strings, so they got unrelated placeholders, &lt;code&gt;[PERSON_31ce220d…]&lt;/code&gt; and &lt;code&gt;[PERSON_aa2cc330…]&lt;/code&gt;, and the agent had no way to tell that the product owner wrote the email. The email address was a third, unrelated placeholder.&lt;/p&gt;&#10;&lt;p&gt;Alias groups fix that. In the local config, a name can list the other forms it appears in:&lt;/p&gt;&#10;&lt;figure class="code" data-lang="toml"&gt;&#10; &lt;figcaption class="code-head"&gt;&#10; &lt;span class="code-lang"&gt;toml&lt;/span&gt;&#10; &lt;span class="code-title"&gt;.agent-context-redactor.toml&lt;/span&gt;&#10; &lt;/figcaption&gt;&#10; &lt;div class="highlight" title=".agent-context-redactor.toml"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;redaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;Avery Example&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Avery&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;avery@example.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;/figure&gt;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;figure class="compare wide numbered"&gt;&#10; &lt;div class="compare-grid"&gt;&#10; &lt;div class="compare-pane compare-pane--raw"&gt;&lt;p class="compare-label"&gt;Raw, stays on your machine&lt;/p&gt;&lt;pre&gt;# Stakeholders&#10;&#10;&lt;mark class="pii"&gt;Avery Example&lt;/mark&gt; is the product owner for &lt;mark class="pii"&gt;PROJECT-LANTERN-042&lt;/mark&gt;.&#10;&#10;----&#10;&#10;Hi team,&#10;&#10;The review is booked for Friday. Questions to &lt;mark class="pii"&gt;avery@example.com&lt;/mark&gt;.&#10;&#10;Thank you,&#10;&lt;mark class="pii"&gt;Avery&lt;/mark&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;div class="compare-arrow" aria-hidden="true"&gt;&lt;/div&gt;&#10; &lt;div class="compare-pane compare-pane--safe"&gt;&lt;p class="compare-label"&gt;What the agent receives&lt;/p&gt;&lt;pre&gt;# Stakeholders&#10;&#10;&lt;span class="ph"&gt;[PERSON_31ce220d…]&lt;/span&gt; is the product owner for &lt;span class="ph"&gt;[SENSITIVE_d5020227…]&lt;/span&gt;.&#10;&#10;----&#10;&#10;Hi team,&#10;&#10;The review is booked for Friday. Questions to &lt;span class="ph"&gt;[EMAIL_31ce220d….1ca7e6be]&lt;/span&gt;.&#10;&#10;Thank you,&#10;&lt;span class="ph"&gt;[PERSON_31ce220d….abb2f1cf]&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;Two files from a fictional folder in balanced mode, with the alias group above.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;The suffix is also what keeps rehydration exact. A draft signed &lt;code&gt;[PERSON_31ce220d….abb2f1cf]&lt;/code&gt; comes back signed &amp;ldquo;Avery&amp;rdquo;, not &amp;ldquo;Avery Example&amp;rdquo;. 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. &lt;code&gt;redctx discover&lt;/code&gt; 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&amp;rsquo;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.&lt;/p&gt;&#10;&lt;h2 id="avery-in-three-tools"&gt;Avery in three tools&lt;a class="anchor" href="#avery-in-three-tools" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;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&amp;rsquo;d expect people to reach for, &lt;a href="https://microsoft.github.io/presidio/"&gt;Microsoft Presidio&lt;/a&gt; and &lt;a href="https://protectai.github.io/llm-guard/input_scanners/anonymize/"&gt;LLM Guard&lt;/a&gt;. This time the folder has a second person, Anna Smith, who leads the review and is the one the email is addressed to.&lt;/p&gt;&#10;&lt;p&gt;All three got the same list of names: the redctx config with an alias group for each person, a deny list for Presidio, and &lt;code&gt;hidden_names&lt;/code&gt; for LLM Guard. Without the list, Presidio&amp;rsquo;s spaCy model didn&amp;rsquo;t recognise either name and passed both through, and LLM Guard redacted &amp;ldquo;Avery&amp;rdquo; but left &amp;ldquo;Example&amp;rdquo;. 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.&lt;/p&gt;&#10;&lt;figure class="panes wide numbered"&gt;&#10; &lt;div class="panes-grid" style="--panes: 4"&gt;&#10; &lt;div class="panes-pane panes-pane--raw"&gt;&#10; &lt;p class="compare-label"&gt;Raw, stays on your machine&lt;/p&gt;&#10; &lt;pre&gt;# Stakeholders&#10;&#10;&lt;span class="ent ent-a"&gt;Avery Example&lt;/span&gt; is the product owner for &lt;mark class="pii"&gt;PROJECT-LANTERN-042&lt;/mark&gt;.&#10;&lt;span class="ent ent-b"&gt;Anna Smith&lt;/span&gt; leads the review.&#10;&#10;----&#10;&#10;Hi &lt;span class="ent ent-b"&gt;Anna&lt;/span&gt;,&#10;&#10;The review is booked for Friday. Questions to &lt;span class="ent ent-a"&gt;avery@example.com&lt;/span&gt;.&#10;&#10;Thank you,&#10;&lt;span class="ent ent-a"&gt;Avery&lt;/span&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;div class="panes-pane"&gt;&#10; &lt;p class="compare-label"&gt;redacted-context-mcp&lt;/p&gt;&#10; &lt;pre&gt;# Stakeholders&#10;&#10;&lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;[PERSON_31ce220d…]&lt;/span&gt;&lt;/span&gt; is the product owner for &lt;span class="ph"&gt;[SENSITIVE_d5020227…]&lt;/span&gt;.&#10;&lt;span class="ent ent-b"&gt;&lt;span class="ph"&gt;[PERSON_2f953128…]&lt;/span&gt;&lt;/span&gt; leads the review.&#10;&#10;----&#10;&#10;Hi &lt;span class="ent ent-b"&gt;&lt;span class="ph"&gt;[PERSON_2f953128….9bd68b43]&lt;/span&gt;&lt;/span&gt;,&#10;&#10;The review is booked for Friday. Questions to &lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;[EMAIL_31ce220d….1ca7e6be]&lt;/span&gt;&lt;/span&gt;.&#10;&#10;Thank you,&#10;&lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;[PERSON_31ce220d….abb2f1cf]&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;div class="panes-pane"&gt;&#10; &lt;p class="compare-label"&gt;Presidio 2.2&lt;/p&gt;&#10; &lt;pre&gt;# Stakeholders&#10;&#10;&lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;qKnBgY8t…&lt;/span&gt;&lt;/span&gt; is the product owner for &lt;span class="ph"&gt;3BqFsF4U…&lt;/span&gt;.&#10;&lt;span class="ent ent-b"&gt;&lt;span class="ph"&gt;OJL5SE1q…&lt;/span&gt;&lt;/span&gt; leads the review.&#10;&#10;----&#10;&#10;Hi &lt;span class="ent ent-b"&gt;&lt;span class="ph"&gt;mfEXrlFA…&lt;/span&gt;&lt;/span&gt;,&#10;&#10;The review is booked for Friday. Questions to &lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;LfjFfdrC…&lt;/span&gt;&lt;/span&gt;.&#10;&#10;Thank you,&#10;&lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;F0_FbdTw…&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;div class="panes-pane"&gt;&#10; &lt;p class="compare-label"&gt;LLM Guard 0.3&lt;/p&gt;&#10; &lt;pre&gt;# Stakeholders&#10;&lt;span class="ph"&gt;[REDACTED_PERSON_1]&lt;/span&gt;&lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;[REDACTED_CUSTOM_1]&lt;/span&gt;&lt;/span&gt; is the product owner for &lt;span class="ph"&gt;[REDACTED_CUSTOM_2]&lt;/span&gt;.&lt;span class="ph"&gt;[REDACTED_PERSON_1]&lt;/span&gt;&lt;span class="ent ent-b"&gt;&lt;span class="ph"&gt;[REDACTED_CUSTOM_3]&lt;/span&gt;&lt;/span&gt; leads the review.&#10;&#10;----&#10;&#10;Hi &lt;span class="ent ent-b"&gt;&lt;span class="ph"&gt;[REDACTED_PERSON_2]&lt;/span&gt;&lt;/span&gt;,&#10;&#10;The review is booked for Friday. Questions to &lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;[REDACTED_EMAIL_ADDRESS_1]&lt;/span&gt;&lt;/span&gt;.&#10;&#10;Thank you,&lt;span class="ent ent-a"&gt;&lt;span class="ph"&gt;[REDACTED_PERSON_3]&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;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&amp;rsquo;s tokens are cut to 8 characters.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;Presidio&amp;rsquo;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&amp;rsquo;t even match &amp;ldquo;Avery Example&amp;rdquo; in one session with &amp;ldquo;Avery Example&amp;rdquo; in the next. Restoring them needs the key, which the caller has to keep somewhere.&lt;/p&gt;&#10;&lt;p&gt;LLM Guard&amp;rsquo;s model tagged the line break in front of each name as a person, so &lt;code&gt;[REDACTED_PERSON_1]&lt;/code&gt; stands for a newline, and it now sits in front of both Avery&amp;rsquo;s and Anna&amp;rsquo;s names. The line breaks it swallowed also glued the two stakeholder lines together. The vault restarts its numbering with every new process.&lt;/p&gt;&#10;&lt;h3 id="writing-the-reply"&gt;Writing the reply&lt;a class="anchor" href="#writing-the-reply" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;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&amp;rsquo;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&amp;rsquo;t say what the role was, it was supposed to say so instead of guessing.&lt;/p&gt;&#10;&lt;p&gt;The role is the part that needs the link. The notes only say what Avery Example does, and the email is signed &amp;ldquo;Avery&amp;rdquo;.&lt;/p&gt;&#10;&lt;figure class="panes wide numbered"&gt;&#10; &lt;div class="panes-grid" style="--panes: 3"&gt;&#10; &lt;div class="panes-pane"&gt;&#10; &lt;p class="compare-label"&gt;redacted-context-mcp&lt;/p&gt;&#10; &lt;pre&gt;Hi &lt;span class="ent ent-a"&gt;Avery&lt;/span&gt;,&#10;&#10;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&amp;#39;ll see you then.&#10;&#10;Thank you,&#10;&lt;span class="ent ent-b"&gt;Anna&lt;/span&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;div class="panes-pane"&gt;&#10; &lt;p class="compare-label"&gt;Presidio 2.2&lt;/p&gt;&#10; &lt;pre&gt;The notes don&amp;#39;t say what role &lt;span class="ent ent-a"&gt;Avery&lt;/span&gt; has on the project.&#10;&#10;Hi &lt;span class="ent ent-a"&gt;Avery&lt;/span&gt;,&#10;&#10;Thank you for booking the review for Friday. I look forward to it. If you have any questions beforehand, please send them to &lt;span class="ent ent-a"&gt;avery@example.com&lt;/span&gt;.&#10;&#10;Best regards,&#10;&lt;span class="ent ent-b"&gt;Anna&lt;/span&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;div class="panes-pane"&gt;&#10; &lt;p class="compare-label"&gt;LLM Guard 0.3&lt;/p&gt;&#10; &lt;pre&gt;The notes don&amp;#39;t say what role &lt;span class="ent ent-b"&gt;Anna&lt;/span&gt; has on the project.&#10;&#10;Hi &lt;span class="ent ent-b"&gt;Anna&lt;/span&gt;,&#10;&#10;Thank you for booking the review for Friday. Please send any questions to &lt;span class="ent ent-a"&gt;avery@example.com&lt;/span&gt;.&#10;&#10;Thank you,&#10;&#10;&lt;span class="ent ent-a"&gt;Avery&lt;/span&gt;&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;One of the three replies per tool, after each tool restored its placeholders. Claude Haiku 5.5, same prompt for all three.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;div class="table-wrap"&gt;&#10; &lt;table&gt;&#10; &lt;thead&gt;&#10; &lt;tr&gt;&#10; &lt;th&gt;Out of 3 runs&lt;/th&gt;&#10; &lt;th&gt;redacted-context-mcp&lt;/th&gt;&#10; &lt;th&gt;Presidio&lt;/th&gt;&#10; &lt;th&gt;LLM Guard&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Reply to the sender, right names&lt;/td&gt;&#10; &lt;td&gt;3&lt;/td&gt;&#10; &lt;td&gt;2&lt;/td&gt;&#10; &lt;td&gt;0&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;&amp;hellip;and knew the sender&amp;rsquo;s role&lt;/td&gt;&#10; &lt;td&gt;3&lt;/td&gt;&#10; &lt;td&gt;0&lt;/td&gt;&#10; &lt;td&gt;0&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Reply to the product owner, right names&lt;/td&gt;&#10; &lt;td&gt;3&lt;/td&gt;&#10; &lt;td&gt;3&lt;/td&gt;&#10; &lt;td&gt;0&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;/tbody&gt;&#10; &lt;/table&gt;&#10;&lt;/div&gt;&lt;p&gt;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 &amp;ldquo;Hi Avery&amp;rdquo;, the form the email used. With Presidio it got the people right but said each time that the notes didn&amp;rsquo;t give a role, and once it signed the email &amp;ldquo;[Your name]&amp;rdquo;. 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 &amp;ldquo;the product owner&amp;rdquo; directly. Presidio managed that, since the stakeholder line names the owner. LLM Guard got it backwards twice, and the third reply was restored as &amp;ldquo;Hi&amp;rdquo;, a line break and a comma, because the placeholder it used was the newline.&lt;/p&gt;&#10;&lt;h3 id="cost-of-running-it"&gt;Cost of running it&lt;a class="anchor" href="#cost-of-running-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Last, the cost of each tool. I timed a clean install with &lt;code&gt;uv&lt;/code&gt; 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.&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10; &lt;table&gt;&#10; &lt;thead&gt;&#10; &lt;tr&gt;&#10; &lt;th&gt;&lt;/th&gt;&#10; &lt;th&gt;redacted-context-mcp&lt;/th&gt;&#10; &lt;th&gt;Presidio 2.2&lt;/th&gt;&#10; &lt;th&gt;LLM Guard 0.3&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Install&lt;/td&gt;&#10; &lt;td&gt;1.0 s&lt;/td&gt;&#10; &lt;td&gt;15.5 s&lt;/td&gt;&#10; &lt;td&gt;42.8 s, then a 713 MB model download on first run&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Packages installed&lt;/td&gt;&#10; &lt;td&gt;1&lt;/td&gt;&#10; &lt;td&gt;57&lt;/td&gt;&#10; &lt;td&gt;83&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;On disk&lt;/td&gt;&#10; &lt;td&gt;1 MB&lt;/td&gt;&#10; &lt;td&gt;675 MB&lt;/td&gt;&#10; &lt;td&gt;2.0 GB + 713 MB&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Start-up&lt;/td&gt;&#10; &lt;td&gt;0.23 s&lt;/td&gt;&#10; &lt;td&gt;4.4 s&lt;/td&gt;&#10; &lt;td&gt;11.8 s (21 s on the first run)&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Per note&lt;/td&gt;&#10; &lt;td&gt;0.47 ms&lt;/td&gt;&#10; &lt;td&gt;12.8 ms&lt;/td&gt;&#10; &lt;td&gt;125 ms&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Peak memory&lt;/td&gt;&#10; &lt;td&gt;31 MB&lt;/td&gt;&#10; &lt;td&gt;725 MB&lt;/td&gt;&#10; &lt;td&gt;1.8 GB&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;CPU&lt;/td&gt;&#10; &lt;td&gt;one core&lt;/td&gt;&#10; &lt;td&gt;one core&lt;/td&gt;&#10; &lt;td&gt;21 of 24 cores&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;Names, emails and codes left in the output&lt;/td&gt;&#10; &lt;td&gt;0&lt;/td&gt;&#10; &lt;td&gt;0&lt;/td&gt;&#10; &lt;td&gt;0&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;/tbody&gt;&#10; &lt;/table&gt;&#10;&lt;/div&gt;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;h2 id="the-leaks-around-the-text"&gt;The leaks around the text&lt;a class="anchor" href="#the-leaks-around-the-text" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;p&gt;The salt was one. A copy of it pasted into a note would have been readable, so strings in the salt&amp;rsquo;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 &lt;code&gt;Incoming&lt;/code&gt; slipped past a check for &lt;code&gt;incoming&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;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&amp;rsquo;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, &lt;code&gt;terms.txt:notes&lt;/code&gt; addresses an alternate data stream of &lt;code&gt;terms.txt&lt;/code&gt;, and the path checks had treated it as a different file.&lt;/p&gt;&#10;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;h2 id="finding-the-names"&gt;Finding the names&lt;a class="anchor" href="#finding-the-names" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The built-in detectors and the config can be extended with pluggable ones: regex rules, &lt;a href="https://microsoft.github.io/presidio/"&gt;Microsoft Presidio&lt;/a&gt; or &lt;a href="https://github.com/urchade/GLiNER"&gt;GLiNER&lt;/a&gt;. 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.&lt;/p&gt;&#10;&lt;figure class="diagram numbered"&gt;&#10; &lt;div class="diagram-canvas"&gt;&#10; &lt;svg class="d-svg" viewBox="0 0 760 290" role="img" aria-labelledby="det-title det-desc" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, monospace" font-size="12"&gt;&#10; &lt;title id="det-title"&gt;What gets caught&lt;/title&gt;&#10; &lt;desc id="det-desc"&gt;Four 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.&lt;/desc&gt;&#10;&#10; &lt;!-- Zones. Presentation attributes are fallbacks for feed readers; the site CSS overrides them. --&gt;&#10; &lt;text class="d-title" x="10" y="25" fill="#6f6a60" font-size="10"&gt;Private note&lt;/text&gt;&#10; &lt;rect class="d-zone d-zone--raw" x="10" y="38" width="190" height="214" rx="3" fill="#f6e0dc" stroke="#b3261e" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;text class="d-title" x="272" y="25" fill="#6f6a60" font-size="10"&gt;Redactor&lt;/text&gt;&#10; &lt;rect class="d-zone" x="272" y="38" width="230" height="214" rx="3" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;text class="d-title" x="574" y="25" fill="#6f6a60" font-size="10"&gt;What the agent reads&lt;/text&gt;&#10; &lt;rect class="d-zone" x="574" y="38" width="176" height="214" rx="3" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10;&#10; &lt;!-- Row 1: built-in detector --&gt;&#10; &lt;rect class="d-box d-box--raw" x="26" y="64" width="158" height="32" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="105" y="84" text-anchor="middle" fill="#1d1b17"&gt;avery@example.com&lt;/text&gt;&#10; &lt;rect class="d-box d-box--step" x="290" y="64" width="194" height="32" rx="2" fill="#ebe4f6" stroke="#6a3dbf"/&gt;&#10; &lt;text x="387" y="84" text-anchor="middle" fill="#1d1b17"&gt;built-in detector: email&lt;/text&gt;&#10; &lt;rect class="d-ph" x="600" y="71" width="124" height="18" rx="2" fill="#1d1b17"/&gt;&#10; &lt;text class="d-ph-text" x="662" y="84" text-anchor="middle" fill="#f6f4ef" font-size="10.5"&gt;[EMAIL_8eb768ff…]&lt;/text&gt;&#10;&#10; &lt;!-- Row 2: configured term --&gt;&#10; &lt;rect class="d-box d-box--raw" x="26" y="112" width="158" height="32" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="105" y="132" text-anchor="middle" fill="#1d1b17"&gt;PROJECT-LANTERN-042&lt;/text&gt;&#10; &lt;rect class="d-box d-box--step" x="290" y="112" width="194" height="32" rx="2" fill="#ebe4f6" stroke="#6a3dbf"/&gt;&#10; &lt;text x="387" y="132" text-anchor="middle" fill="#1d1b17"&gt;config: terms&lt;/text&gt;&#10; &lt;rect class="d-ph" x="590" y="119" width="144" height="18" rx="2" fill="#1d1b17"/&gt;&#10; &lt;text class="d-ph-text" x="662" y="132" text-anchor="middle" fill="#f6f4ef" font-size="10.5"&gt;[SENSITIVE_d5020227…]&lt;/text&gt;&#10;&#10; &lt;!-- Row 3: allow list --&gt;&#10; &lt;rect class="d-box d-box--raw" x="26" y="160" width="158" height="32" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="105" y="180" text-anchor="middle" fill="#1d1b17"&gt;SQLite&lt;/text&gt;&#10; &lt;rect class="d-box d-box--step" x="290" y="160" width="194" height="32" rx="2" fill="#ebe4f6" stroke="#6a3dbf"/&gt;&#10; &lt;text x="387" y="180" text-anchor="middle" fill="#1d1b17"&gt;config: allow list&lt;/text&gt;&#10; &lt;rect class="d-box" x="612" y="160" width="100" height="32" rx="2" fill="#f6f4ef" stroke="#1d1b17"/&gt;&#10; &lt;text x="662" y="180" text-anchor="middle" fill="#1d1b17"&gt;SQLite&lt;/text&gt;&#10;&#10; &lt;!-- Row 4: nothing matches --&gt;&#10; &lt;rect class="d-box d-box--raw" x="26" y="208" width="158" height="32" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="105" y="228" text-anchor="middle" fill="#1d1b17"&gt;lantern&lt;/text&gt;&#10; &lt;rect class="d-zone" x="290" y="208" width="194" height="32" rx="2" fill="none" stroke="#c3bcae" stroke-dasharray="3 4"/&gt;&#10; &lt;text class="d-muted" x="387" y="228" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;no detector, not in config&lt;/text&gt;&#10; &lt;rect class="d-box d-box--raw" x="612" y="208" width="100" height="32" rx="2" fill="#f6f4ef" stroke="#b3261e"/&gt;&#10; &lt;text x="662" y="228" text-anchor="middle" fill="#1d1b17"&gt;lantern&lt;/text&gt;&#10;&#10; &lt;!-- Into the redactor --&gt;&#10; &lt;path class="d-line d-line--raw" d="M184 80 H282" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="290,80 282,76 282,84" fill="#b3261e"/&gt;&#10; &lt;path class="d-line d-line--raw" d="M184 128 H282" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="290,128 282,124 282,132" fill="#b3261e"/&gt;&#10; &lt;path class="d-line d-line--raw" d="M184 176 H282" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="290,176 282,172 282,180" fill="#b3261e"/&gt;&#10; &lt;path class="d-line d-line--raw" d="M184 224 H282" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="290,224 282,220 282,228" fill="#b3261e"/&gt;&#10;&#10; &lt;!-- Out to the agent --&gt;&#10; &lt;path class="d-line d-line--safe" d="M484 80 H592" fill="none" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="600,80 592,76 592,84" fill="#2f7a3b"/&gt;&#10; &lt;path class="d-line d-line--safe" d="M484 128 H582" fill="none" stroke="#2f7a3b" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--safe" points="590,128 582,124 582,132" fill="#2f7a3b"/&gt;&#10; &lt;path class="d-line" d="M484 176 H604" fill="none" stroke="#4f4b43" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head" points="612,176 604,172 604,180" fill="#4f4b43"/&gt;&#10; &lt;path class="d-line d-line--raw" d="M484 224 H604" fill="none" stroke="#b3261e" stroke-width="1.25"/&gt;&#10; &lt;polygon class="d-head d-head--raw" points="612,224 604,220 604,228" fill="#b3261e"/&gt;&#10;&#10; &lt;text class="d-muted" x="380" y="276" text-anchor="middle" fill="#6f6a60" font-size="10.5"&gt;Plugged detectors (regex, Presidio, GLiNER) can add matches here. They can't remove one.&lt;/text&gt;&#10;&lt;/svg&gt;&#10;&#10; &lt;/div&gt;&#10; &lt;figcaption&gt;Four values from one note. Balanced and strict mode treat these the same way.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;h2 id="compared-with-the-alternatives"&gt;Compared with the alternatives&lt;a class="anchor" href="#compared-with-the-alternatives" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Proxies such as &lt;a href="https://protectai.github.io/llm-guard/input_scanners/anonymize/"&gt;LLM Guard&lt;/a&gt; or &lt;a href="https://docs.litellm.ai/docs/tutorials/presidio_pii_masking"&gt;LiteLLM with Presidio&lt;/a&gt; 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&amp;rsquo;s process and its local session history. When a proxy restores names in a reply, those names go back into the agent&amp;rsquo;s context, and every later request depends on the proxy catching them again.&lt;/p&gt;&#10;&lt;p&gt;Redaction tools exposed over MCP, such as the text tools in &lt;a href="https://docs.tonic.ai/textual/tonic-textual-api/textual-mcp-server"&gt;Tonic Textual&amp;rsquo;s server&lt;/a&gt;, take text from the agent and return a redacted copy. To call them, the agent has to have read the raw text first.&lt;/p&gt;&#10;&lt;p&gt;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&amp;rsquo;s hook system, while an MCP server works the same way in Claude Code, Codex and any other MCP client.&lt;/p&gt;&#10;&lt;p&gt;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&amp;rsquo;t. The only tool I found that does is Limina, formerly Private AI, a commercial product whose coreference resolution links mentions such as &amp;ldquo;Robert&amp;rdquo;, &amp;ldquo;Bob&amp;rdquo; and &amp;ldquo;R. Johnson&amp;rdquo;. 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.&lt;/p&gt;&#10;&lt;p&gt;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&amp;rsquo;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.&lt;/p&gt;&#10;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;h2 id="the-shell"&gt;The shell&lt;a class="anchor" href="#the-shell" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;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.&lt;/p&gt;&#10;&lt;h2 id="try-it"&gt;Try it&lt;a class="anchor" href="#try-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The demo uses only fictional data and needs no model or credentials:&lt;/p&gt;&#10;&lt;figure class="code" data-lang="sh"&gt;&#10; &lt;figcaption class="code-head"&gt;&#10; &lt;span class="code-lang"&gt;shell&lt;/span&gt;&#10; &lt;/figcaption&gt;&#10; &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pipx install redacted-context-mcp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/zoltan0803/redacted-context-mcp.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; redacted-context-mcp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redctx --root examples/quickstart/context &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --config examples/quickstart/context/demo-redaction-config.toml audit&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;/figure&gt;&lt;p&gt;Then point an agent at your own folder. For Claude Code, put this in the agent workspace&amp;rsquo;s &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;/p&gt;&#10;&lt;figure class="code" data-lang="json"&gt;&#10; &lt;figcaption class="code-head"&gt;&#10; &lt;span class="code-lang"&gt;json&lt;/span&gt;&#10; &lt;span class="code-title"&gt;.mcp.json&lt;/span&gt;&#10; &lt;/figcaption&gt;&#10; &lt;div class="highlight" title=".mcp.json"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mcpServers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;redacted_context&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;stdio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;redctx-mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/absolute/path/to/private-context&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&lt;/figure&gt;&lt;p&gt;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.&lt;/p&gt;&#10;</content:encoded></item></channel></rss>