HomeBlog › SecretNAT: your AI needs the shape of a secret, not its value
Blog

SecretNAT: your AI needs the shape of a secret, not its value

Jul 19, 20266 min readBy the Rysh team

NAT, but for credentials: swap real secrets for reversible look-alike tokens before the request is built. We grepped the wire to prove it.

Every AI coding tool has the same uncomfortable property: to help you, it reads your code. And your code is full of secrets — API keys in a .env, a database URL with the password inline, a bearer token in a curl command you pasted to ask "why is this 401ing?" The moment any of that lands in a prompt, it has left your machine.

One-way redaction handles part of this: scrub the value, send a hole. But a hole has a cost — the model loses context, and no local tool can ever get the value back through the conversation. Sometimes the agent genuinely needs to work with a secret: write it into a config, use it in a migration, thread it into an API call.

So in rysh I built the reversible version. It's called SecretNAT, and the idea is stolen wholesale from networking.

NAT, but for credentials

NAT — Network Address Translation — swaps a private IP for a public one on the way out and reverses the mapping on the way back. Your fridge talks to the internet; the internet never learns your fridge's real address.

SecretNAT does the same thing at the model boundary: real secrets are swapped for reversible, look-alike tokens before the request is built, and the real value is restored only where a local tool actually needs it. The model provider only ever sees the token. It's on by default.

The "look-alike" part is the trick. In semantic mode, tokens keep the shape of the original:

Real What goes over the wire
sk_live_4eC39HqLyjWDarjtT1zdp7dc sk_live_SNAT000001
ghp_AbCdEf…0123456789 ghp_SNAT000002
a 3-segment JWT eyJSNAT000003.SNATPAYLOAD.SNATSIGNATURE
postgres://svc:hunter2@db/app password swapped, the rest intact

Why shape matters: the model's usefulness comes from context. sk_live_ means something — it's a production Stripe key, and a good model will warn you about it, refuse to log it, treat it differently from sk_test_. Replace it with [REDACTED] and that reasoning dies. Replace it with sk_live_SNAT000001 and the model reasons at full fidelity about a credential it does not possess.

There's also a private mode (SECRET_TOKEN_001-style tokens) for when you don't even want the provider to know which vendors you use.

The proof, because you're a skeptic (good)

I don't want "the key never leaves" taken on faith. Here's the test we ran, which you could reproduce: a transparent logging proxy in front of the real Anthropic API, teeing every request body to a file before forwarding. Point rysh at the proxy. Paste a (dummy) live-format Stripe key into a prompt and ask the model about it.

The model answered correctly — it discussed a live Stripe key, what sk_live_ implies, the works. Then, grep the actual bytes that left the machine:

$ grep -c  sk_live_51H8DEMOKEYNOTREAL   wire.log    # the real key
0

$ grep -oE 'sk_live_SNAT[0-9]+'         wire.log    # what the model saw
sk_live_SNAT000001

Zero occurrences of the real key on the wire. What went out was sk_live_SNAT000001 — same prefix, same shape, reversible by rysh, meaningless to anyone else.

My favorite detail: in one run the model's answer analyzed the suffix and called it "a placeholder-looking value." It told on itself. It never saw the real thing.

The reversible half

Tokenizing outbound text is the easy part. What makes it survivable day-to-day is the reverse direction: when a local tool actually needs the real value — writing your .env, running the migration, calling the real endpoint — SecretNAT restores it just-in-time, in memory, on your machine, on a transient copy that never re-enters the conversation. Tool output is then re-sanitized at the source, before it hits the display buffer or the persisted transcript.

That last bit is the hygiene that's easy to get wrong: rysh persists conversations so you can resume them. Sanitizing only at the HTTP boundary would still write real secrets to disk via tool output. Sanitizing at the source means the cleartext value exists for the duration of one tool call, and nowhere else.

The token↔value mapping itself lives only in memory — and the struct's serializer is deliberately built to fail if anything ever tries to persist it. Belt, meet suspenders.

(How you inspect and reveal that mapping locally — ##snat status, ##snat list, ##snat get, and the ##rst alias — is its own article: you keep the keys.)

SecretNAT is not your AI gateway — it's the other half

If your org runs an AI security gateway — egress policy, DLP, prompt-injection scanning, org-wide audit of model traffic — keep it. SecretNAT doesn't compete with it. They guard different halves of the same request:

Run both and you get credentials that can't leak on traffic you can govern. That's actual defense-in-depth, not two products fighting over one job.

What it is NOT (don't let me imply otherwise)

Poke holes in the threat model in the comments. "The provider never receives the value" is a claim I want stress-tested, not applauded.


rysh is in private beta. If your team has ever said "we can't paste our keys into a model," this feature exists because of that sentence — and our design-partner program (free access, direct founder line, roadmap influence) is how you get it: apply here.

Related: ##snat list and ##snat get: you keep the keys · Client-side secret redaction

Try Rysh

Every pane is a shell and an AI agent — install takes one command.

Get started free →