Picture the paste sitting in a ticket comment, not in a threat report. The failing request is POST /internal/billing/replay. It returns 500. Under it, someone has already copied DATABASE_URL=postgres://app:EXAMPLE_NOT_A_SECRET@db.internal/billing and a customer email.
That text is still on the laptop. The trust boundary breaks when it is submitted to a model. I do not get the secret back. A cheerful answer is not a deletion receipt.
I want one invariant. No incident text leaves the workstation until a local gate prints class=shareable and a short hash.
Sounds fussy? Good. Fussy is cheaper than a rotation call.
The boundary I am actually defending
I am not trying to make a model polite. I am trying to keep three things on the near side of the wire: secrets, customer identifiers, and names that map the internal network.
A remote model is a new processor. A free server is also a new processor until I know who runs it, where the logs land, and how long a prompt sticks. A different hostname does not shrink that boundary.
So the useful question is not which model can explain this 500. It is which bytes are allowed to leave.
Logs are not a prompt format
Most bad pastes start as logs. An access line, an app traceback, a support export, a dashboard shot. Someone calls that context. I call it an export.
Would you hand a stranger the raw file just because the status code is interesting? I would not. A log line is an event record. It was not written as a document I consented to ship off the machine.
Query strings hide emails. Upstream URLs hide userinfo. Cookie jars and signature headers are not debugging hints. They are credentials with extra steps.
A WAF audit log is the same class of export as an app log. Timestamps and status codes can be rewritten as fields. Header tokens cannot.
I copy fields. I do not attach the file. This rewrite is the only shape I would even consider sharing:
ts=2026-10-10T00:00:00Z method=POST route=/internal/billing/replay status=500 code=E_UPSTREAM_TIMEOUT
No host. No email. No password in the URL. If I cannot rebuild the symptom from fields like that, I am not ready to ask a model. I am ready to read the code.
Screenshots are logs too. This template reads text only. A PNG does not become shareable because I forgot to OCR it. The manual rule is blunt. Images stay on the laptop.
What I refuse to send
If I cannot name the class, I do not paste the blob. A sentence that says please ignore any secrets is a wish, not a control.
| Class | Example shape (fake) | Default |
|---|---|---|
| Connection URL | postgres://app:EXAMPLE_NOT_A_SECRET@db.internal/billing |
block |
| Env assignment | WEBHOOK_SECRET=EXAMPLE_NOT_A_SECRET |
block |
| Private key banner | -----BEGIN PRIVATE KEY----- |
block |
| Customer email |
ada@example.com inside a ticket |
block |
| Internal hostname | billing.internal |
block |
| Stack frame, no secret | File app/replay.py, line 40 |
human review |
| Synthetic health failure | GET /healthz -> 503 E_UPSTREAM_TIMEOUT |
shareable |
The examples are canaries. They are not live credentials. They are not a claim that I found them in a product.
Four zones, then a tool
I draw the zones before I pick a model. Otherwise the tool picks the boundary for me, and that is backwards.
[1 laptop] --gate--> [2 shareable text]
|-- [3 remote free model]
\-- [4 free server option]
blocked and review stay in zone 1
Zone 1 is the only zone I fully control in this walkthrough. Zones 3 and 4 can both retain prompts. Both can log.
Both may be operated by someone who is not me. I do not treat the word free as a synonym for private.
What actually crosses the gate? A classified file, a 12-character SHA-256 prefix, and the hit list. What stays home? The original ticket, the env file, the HAR, the kubectl dump, the dashboard shot.
Would I send a redacted diff of a function that is already public? Yes, after the gate. Would I send the production traceback so a model can see the real path? No. I rewrite the path to app/replay.py, then I classify again.
Step-by-step: the paste gate
This is an unexecuted template. It targets Python 3.12 and the standard library only. I have not run it for this draft. Expected lines are the contract I want, not observed output. Nothing here is a vulnerability report.
1. Pin the runner
I pin the interpreter in the command, not in a slide.
python3.12 --version
# contract: Python 3.12.x
# fixture: paste-gate-v1
If the runner is not 3.12, I stop. I will not trust an exit code from whatever python happens to be on PATH.
2. Keep the classifier offline
The script reads one file. It prints one line. It exits 0, 2, or 3. There is no socket import on purpose.
If a gate can open a network connection, it is no longer a gate. It is another exporter.
#!/usr/bin/env python3
# paste_gate.py - unexecuted template. Python 3.12, stdlib only.
import hashlib
import re
import sys
from pathlib import Path
FIXTURE_VERSION = "paste-gate-v1"
BLOCK = [
("connection_url", re.compile(r"\b(?:postgres|mysql|mongodb|redis)://\S+", re.I)),
("env_secret", re.compile(r"\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY)\s*=\s*\S+")),
("pem", re.compile(r"-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----")),
("email", re.compile(r"\b[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}\b")),
("internal_host", re.compile(r"\b[\w.-]+\.(?:internal|corp|local)\b", re.I)),
]
REVIEW = [
("stack_path", re.compile(r"File .+, line \d+")),
]
def classify(text: str) -> tuple[str, list[str]]:
hits = [name for name, pat in BLOCK if pat.search(text)]
if hits:
return "blocked", hits
review = [name for name, pat in REVIEW if pat.search(text)]
if review:
return "review", review
return "shareable", []
def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: paste_gate.py <file>", file=sys.stderr)
return 1
raw = Path(argv[1]).read_text(encoding="utf-8")
klass, hits = classify(raw)
digest = hashlib.sha256(raw.encode()).hexdigest()[:12]
print(
f"version={FIXTURE_VERSION} class={klass} "
f"sha256_12={digest} hits={','.join(hits) or '-'}"
)
return {"shareable": 0, "blocked": 2, "review": 3}[klass]
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Why a hash prefix? So a review note can point at a file without pasting the file into chat again. Why not build the whole design around one header name? Header names change. A connection URL and a private-key banner should fail closed anyway.
The email pattern also flags userinfo in a URL, because EXAMPLE_NOT_A_SECRET@db.internal looks like a mailbox. That overlap is intended. Fail closed.
3. Add a positive fixture and a negative one
Positive means must not leave. Negative means may leave. I want both. A gate with only scary samples becomes a panic button, and nobody runs panic buttons.
fixtures/block_db_url.txt is the positive fixture:
POST /internal/billing/replay -> 500
DATABASE_URL=postgres://app:EXAMPLE_NOT_A_SECRET@db.internal/billing
WEBHOOK_SECRET=EXAMPLE_NOT_A_SECRET
customer=ada@example.com
fixtures/pass_healthz.txt is the negative fixture:
GET /healthz -> 503
code=E_UPSTREAM_TIMEOUT
note=synthetic fixture, no identifiers
fixtures/review_stack.txt is the middle, and the middle is where people cheat:
File "app/replay.py", line 40, in replay
raise TimeoutError("upstream")
A path can be harmless. It can also be a map of the repo. That is why review is not a quiet pass.
4. Write the expected failure down
I put the contract in a comment. I do not invent a hash and pretend I executed the file.
python3.12 paste_gate.py fixtures/block_db_url.txt; echo exit:$?
# UNEXECUTED contract: class=blocked hits=connection_url,env_secret,email,internal_host exit 2
python3.12 paste_gate.py fixtures/pass_healthz.txt; echo exit:$?
# UNEXECUTED contract: class=shareable hits=- exit 0
python3.12 paste_gate.py fixtures/review_stack.txt; echo exit:$?
# UNEXECUTED contract: class=review hits=stack_path exit 3
If a later run prints class=shareable on the block file, the gate is wrong. I do not ask a model why. I fix the pattern, and I keep the failing file in the tree.
5. Pick a destination only after the exit code
Only exit 0 may be copied. Exit 3 stays in zone 1 until I delete the sensitive lines and re-run. Exit 2 never gets a second chance in the same bytes. I redact into a new file, then I classify that new file.
| Destination | Exit 0 synthetic | Exit 3 stack | Exit 2 bundle |
|---|---|---|---|
| Remote free model | copy allowed | no | no |
| Free server I do not operate | copy allowed, assume retention | no | no |
| Server I operate and can wipe | copy allowed | only after rewrite and a new exit 0 | redact first, then re-gate |
| Laptop | always | always | always |
I do not edit the blocked file in place and forget which version I hashed. The hash is the point. If the bytes change, the prefix changes, and the old approval is dead.
6. Rewrite, then classify the rewrite
Before the gate, I rebuild the log into four fields. Time. Method. A route template, not a raw path if the path embeds an id. Status, plus a stable error code.
That is the whole shareable object. If the bug lives in a header, I describe the header name and the failure mode. I do not copy the header value. If the bug lives in a query parameter, I copy the parameter name and a fake value of the same shape, like id=REDACTED_INT.
Then the gate runs on the rewrite, not on the original. Two files. Two hashes. One approval.
Prevent, detect, recover
| Phase | Control | Evidence I want |
|---|---|---|
| Prevent | Local gate, no socket, deny by default | Exit 2 on the block fixture |
| Detect | CI runs the three fixtures when the pattern list changes | Contract comments still match |
| Recover | Treat a bad paste as already disclosed | Rotate the secret, tell the owner, do not paste the same line into a cleanup prompt |
Recovery is the step I see skipped. If the URL already left the laptop, redacting the next prompt does not rewind the first one. Rotate the credential. Then add a fixture so the next URL cannot match shareable.
Asking the same model to forget the previous message is not recovery. It is a second export.
Where free access actually fits
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is an open-source project. The operator supplied two availability claims for this draft: free model access, and a free server option. I am not pinning a token quota, a model name, a hardware size, or a duration. Those figures were not checked against a primary source here, and an uncited number goes stale. Read the project page before you plan a workload around a limit.
I would still use the same gate.
A free model is a fair place for fixtures/pass_healthz.txt, and for a redacted function that is already public. It is a bad place for the billing paste, even when the allowance looks large. A large allowance is not a trust boundary.
A free server option can be the better zone when the prompt needs to stay on infrastructure I operate. Can be is not is. Until I have read how that server isolates tenants, what it logs, and who can open the disk, zone 4 sits behind the same exit codes as zone 3. If I do operate it, I still do not disable the gate. Local classification is cheaper than a forensic argument later.
If you already keep a MonkeyCode workspace, run this gate on the paste before the first completion. Let the model answer a shareable file. Do not let it decide what is shareable.
Who should not use this
Regex is a sieve. It misses a sentence that describes a firewall rule in plain English. It misses a secret split across two lines. It false-positives on a public note that contains an email, which is why example.com still blocks here. I would rather review a false positive than explain a leaked URL.
This script does not encrypt anything. It does not delete remote logs. It does not prove a provider retention setting. Exit 0 means this file matched my allow shape. It does not mean a vendor forgot the prompt.
Skip this approach if you need a real data-loss control plane, a legal hold, or a customer contract that forbids this class of export. Skip it if a green check would talk you out of rotating a secret you already pasted. Skip it as evidence of a product flaw. The fixture was not executed against a target, and it found nothing.
The question I would actually put in CI
Which invariant belongs in CI, and which layer should enforce it?
I would put the three fixtures in CI, on the repo that owns the gate. I would not put production tickets in CI just to test a model. The laptop gate enforces the paste. The CI job enforces the gate. The model enforces neither.
Top comments (0)