A redaction rule that has never rejected a payload is a rule you cannot trust, because you have never watched it work. Most teams write a short policy about what not to send to a model, then paste context into a chat window and hope the policy is real. The thing that actually protects you is a small program sitting on the path, failing closed, and carrying a test suite full of canaries that prove it still fires after every refactor.
The front page is loud this week with new AI features and with honest fatigue about them, and both reactions are reasonable. The quieter engineering question underneath is what leaves your machine when an agent, a chat window, or a CI job ships a payload outward. You can answer that with three trust boundaries, one choke point, and roughly sixty lines of code you can read in a single sitting.
Three boundaries, three different failure modes
Your first boundary is the shell you type in, where a stray cat .env turns into an outgoing payload before you notice. Your second boundary is whatever collects and forwards context, which is usually a CLI wrapper, a proxy, or a CI step that nobody reviewed after the first week. Your third boundary is the remote endpoint itself, where you control neither retention nor logging nor who reads the request later.
The first two boundaries are yours to instrument, and the third one is the reason the first two matter. You cannot audit what a provider keeps, so the only defensible move is to decide what it ever receives. That decision is cheap to encode as a deny list of shapes: private keys, cloud access keys, provider tokens, internal hostnames, RFC1918 addresses, email addresses, and full KEY=value assignment lines.
Notice what is missing from that list: customer records, unredacted production logs, incident timelines with real names, and any source file under an NDA. Those do not have a reliable shape, which is exactly why the gate must be paired with a human rule instead of pretending to be complete. A gate that catches the mechanical leaks buys you the attention to think about the semantic ones.
Build one choke point that fails closed
The gate below rejects a payload when it recognizes a shape and exits non-zero, printing only the rule name and a short prefix. It deliberately never echoes the matched secret, because your terminal history and CI logs are themselves a place data leaks.
// egress-gate.mjs — inspect a payload before it reaches any model endpoint.
import { readFileSync } from "node:fs";
const RULES = [
{ name: "private-key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
{ name: "cloud-key", re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/ },
{ name: "provider-token", re: /\b(sk|ghp|glpat|xox[baprs])-[A-Za-z0-9_-]{16,}/ },
{ name: "internal-host", re: /\b[a-z0-9-]+\.(internal|corp|lan|svc)\./ },
{ name: "rfc1918", re: /\b(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\b/ },
{ name: "email", re: /\b[^\s@]+@[^\s@]+\.[a-z]{2,}\b/i },
{ name: "env-assignment", re: /^\s*[A-Z0-9_]{3,}=.{8,}/m },
];
export function findLeaks(text) {
const hits = [];
for (const rule of RULES) {
const match = text.match(rule.re);
if (match) hits.push({ rule: rule.name, prefix: match[0].slice(0, 6) + "..." });
}
return hits;
}
if (process.argv[2] === "check") {
const payload = readFileSync(0, "utf8");
const hits = findLeaks(payload);
if (hits.length) {
for (const hit of hits) console.error(`blocked: ${hit.rule} (${hit.prefix})`);
process.exit(1);
}
console.log(`pass: ${Buffer.byteLength(payload)} bytes clean`);
}
The important design choice is that the default outcome is refusal, and the only way forward is to remove the offending shape from the payload. You can always over-ride a gate deliberately, but you cannot over-ride it by accident while tired.
Prove the gate with canaries, and prove it does not cry wolf
A gate with no failing test is a decoration. Build a canary corpus of strings that must be caught, plus a benign sample that must survive untouched, because a redactor that mangles ordinary code trains your team to bypass it.
// gate.test.mjs — run with: node --test
import { test } from "node:test";
import assert from "node:assert/strict";
import { findLeaks } from "./egress-gate.mjs";
const CANARIES = [
["private-key", "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA..."],
["cloud-key", "aws_access_key_id = AKIAIOSFODNN7EXAMPLE"],
["internal-host", "curl http://billing-api.prod.svc.cluster.local/health"],
["env-assignment", "DATABASE_URL=postgres://user:hunter2@10.9.4.2:5432/app"],
];
for (const [rule, sample] of CANARIES) {
test(`catches ${rule}`, () => {
const hit = findLeaks(sample).map((h) => h.rule);
assert.ok(hit.includes(rule), `expected ${rule}, got ${hit.join(",") || "none"}`);
});
}
test("leaves ordinary code alone", () => {
const benign = "const retries = 3;\nfor (let i = 0; i < retries; i++) await ping();";
assert.deepEqual(findLeaks(benign), []);
});
Run node --test and you get a red or green signal about your own policy, not about the vendor you picked this quarter. Every time you find a real leak that slipped through, add it as a canary first and fix the rule second, so the regression is permanent.
Put the gate on the path, not in a wiki
A gate that lives in documentation is advisory, and advisory controls only bind the people who already agreed with them. Wrap your outgoing calls in one script, so every laptop, CI job, and agent session goes through the same door.
#!/usr/bin/env bash
# ask-model.sh — the only sanctioned way to send a payload from this repo.
set -euo pipefail
payload="$(mktemp)"
trap 'rm -f "$payload"' EXIT
cat >"$payload"
node egress-gate.mjs check <"$payload" >&2
curl -sS "${MODEL_ENDPOINT:?set MODEL_ENDPOINT first}" \
-H "content-type: application/json" \
-H "authorization: Bearer ${MODEL_KEY:?set MODEL_KEY first}" \
--data-binary @"$payload"
Now you can pipe a staged diff through it before asking anything about your code, which is the moment leaks usually happen: git diff --cached | ./ask-model.sh. The gate sees the same bytes the endpoint would, and your history keeps only the rule name when it blocks something.
Where free model access and a free server actually fit
A gate tested only against a stub is untested, because the stub never surprises you, so you want a real endpoint in the loop for the full canary suite. This is the narrow place where MonkeyCode's operator-supplied free model access helps: it lets you run the suite against a live endpoint on every pull request instead of rationing calls. The same operator states a free server option and a free allowance of ten million tokens, and you should verify both against the current terms before you build a workflow that depends on them.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. I treat the free tier as a place to rehearse the gate, never as a place to keep secrets. If you run the wrapper on a shared free host, give it a scoped key, keep your real .env off that machine, and rotate the key on a schedule, because the host is another trust boundary you do not control. One reasonable next step is to run your canary suite once against a live endpoint and let the failures tell you which rules your team still needs.
What this gate will not catch, and who should skip it
Pattern gates miss anything without a stable shape: a pasted customer quote, a base64 blob, a screenshot, a paraphrased internal strategy, or a secret format invented last month. They also produce false positives on UUIDs, content hashes, and documentation examples, which is why the benign-code test matters as much as the canary tests. If your work falls under a regime that requires formal DLP tooling with audit trails, a sixty-line script is a supplement and never a substitute.
If you only ever ask a model about public open-source code and never paste anything private, the gate is mostly friction, and you should skip it. Everyone else should start with the boundaries and the canaries, because the useful question was never whether a provider is trustworthy. The useful question is what you decided to send, and whether your own code can prove it refused the rest.
Top comments (0)