Outbound scrubbing
sable_scrub: true on a chat request makes the gateway redact known
secret/PII shapes from the outbound prompt before it reaches any vendor.
The redaction happens in memory at the egress shim (after the sealed
request is opened, before the upstream call), and each match is replaced with
a typed marker like [redacted:email] so the model can still reason about the
text's structure.
It is opt-in, per request, and works on both the OpenAI surface
(POST /v1/chat/completions) and the
Anthropic Messages API (POST /v1/messages):
curl https://api.buildsable.com/v1/chat/completions \
-H "Authorization: Bearer $SABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "sable",
"sable_scrub": true,
"messages": [{"role":"user","content":"Summarize this log dump"}]
}'What gets redacted
The filter is deliberately conservative and pattern-based. It catches the shapes that are near-certainly secrets or contact identifiers:
| Shape | Matches | Becomes |
|---|---|---|
| Email address | Matcheslocal@domain.tld | Becomes[redacted:email] |
| EVM address / tx hash | Matches0x + 40 or 64 hex characters | Becomes[redacted:evm] |
| API keys & tokens | MatchesCommon prefixes (sk-, sk_, ghp_, github_pat_, xoxb-, AKIA, AIza, eyJhbGciOi, …) and Bearer … values, 20+ chars | Becomes[redacted:key] |
| Long bare hex | Matches32+ contiguous hex characters (hashes, raw private keys, session tokens) | Becomes[redacted:hex] |
| Card number | Matches13 to 19 digits, unbroken or in groups of 3+ separated by spaces or hyphens, that pass the Luhn checksum | Becomes[redacted:card] |
| Phone number | MatchesA + followed by 8 to 15 digits (+31 6 1234 5678); a number with a leading 0 or 00 and 10 to 15 digits (06 12345678, 020 7946 0958); or the North American 3-3-4 grouping ((555) 123-4567, 555.123.4567) | Becomes[redacted:phone] |
Where it runs: the §3 story
The privacy contract is unchanged by this feature, and the scrub is built inside it, not next to it:
- The request is sealed on ingress as always. The scrub runs after unsealing, inside the egress frame, immediately before the upstream call, the same short window where plaintext already exists.
- Nothing scrubbed or unscrubbed is ever stored or logged. The only new thing that reaches a log is the redaction count, which is content-free.
- The scrubbed text goes to the vendor; the original never leaves the frame.
The receipt proof
A scrubbed request's signed receipt carries
"scrubbed": true, proof that the filter ran on that request. The field is
present only when you opted in.
Honest limits
Read these before relying on it:
- Pattern-based, not NER. It catches secret shapes: emails, keys,
wallet addresses, long hex, card and phone numbers. It does not catch names,
addresses in prose, or any free-text PII. A number glued to a word or inside
a URL is left alone, and a bare ten-digit number without a leading
0is not treated as a phone (Unix timestamps have that shape), so a phone number written that way passes through. A long number that happens to pass the card checksum is redacted even when it is not a card. An opt-in redactor that silently mangled prose would be worse than none, so it doesn't try. - This is redaction, not anonymization. The surrounding content still reaches the vendor in plaintext (on standard-tier routing, the host sees the prompt; see the privacy ladder). Scrubbing narrows what leaves; it does not change which rung you're on.
scrubbed: truemeans the filter executed, not that the prompt contained nothing sensitive afterward.
When to use it
The threat model is the agent stack handling untrusted input: a
prompt-injected agent pastes whatever it has into a model call, and whatever
it has often includes keys, wallets, and tokens. With sable_scrub: true on
the call (or baked into your agent's request wrapper), those shapes never
reach the upstream vendor at all, and the receipt proves the filter was on.