You should treat every coding-agent prompt as a network egress event, not as a private scratchpad. Secrets, customer dumps, and internal hostnames leave your control the moment they enter a context window. A cheap redaction pass before the call is more reliable than hoping a vendor will forget. The rest of this article turns that rule into a repeatable threat model and a small sanitizer you can run locally.
Coding assistants now sit inside the same loop where you reproduce bugs and paste stack traces. That convenience quietly collapses the old habit of keeping production clues on an isolated jump box. The model feels like a colleague on your laptop, which is why people paste .env files without flinching. Your threat model has to assume the opposite: the model is a processor you do not operate.
Picture four rooms with locked doors between them, even if the UI draws them as one chat. Room one is your workstation, including the repo, the shell history, and local secret stores. Room two is the agent runtime, which can read files, run commands, and assemble a context bundle. Room three is the model endpoint, and room four is whatever logs or eval traces that endpoint still retains.
Anything that walks from room one into room three has crossed a trust boundary you cannot reverse. Remote assistants make that crossing obvious, because the weights and the disks are visibly not yours. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option, which helps you rehearse the same workflow without pretending the bytes stayed local.
You do not send live credentials, session cookies, or private keys, even when the bug only reproduces with them. You do not send customer records, health data, or payroll extracts just to get a prettier SQL rewrite. You do not send internal architecture diagrams that name unreleased hostnames, jump boxes, or VPN paths. You also do not send other people's unreleased source, because a context window is not a license grant.
The quieter leak is not the prompt you remember; it is the transcript you forget. Agent products write session logs, tool traces, and sometimes prompt caches onto disk you do not inspect. If your company later issues a legal hold, that chat history can become a document you never meant to keep. Rotate the habit: assume every turn is written down, then decide whether that sentence should exist.
You cannot prompt your way out of a data-flow problem, because instructions are not a boundary. Telling the model to forget a key still placed the key in the request body. Vendors may train, debug, or cache that body under terms you have not read this week. The only reliable denial is omission, which means you never put that byte in the packet.
There is a time-of-check failure that many agent workflows quietly ignore when a deadline is close. You redact a copy, then the agent uses a file tool and reads the live path anyway. The trust boundary failed because the runtime still had permission to enter room one. Pin the agent to the redacted path, or disable file tools until the brief is clean.
The practical control is boring on purpose, which is why it still works under deadline pressure. You stage a context file, you run a local redactor, and only then do you let the agent read the file. The script below is an example you should adapt, not a compliance product and not a guarantee. It writes a receipt of every hit so you can see what almost left the building.
#!/usr/bin/env python3
"""Example context redactor. Adapt before use. Not a DLP product."""
from __future__ import annotations
import re
import sys
from pathlib import Path
PATTERNS = [
("aws_akid", re.compile(r"AKIA[0-9A-Z]{16}")),
("jwt", re.compile(r"eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}")),
(
"pem",
re.compile(
r"-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----"
),
),
("bearer", re.compile(r"(?i)(authorization:\s*bearer\s+)\S+")),
(
"url_cred",
re.compile(
r"(?i)((?:https|postgres|mysql|mongodb|redis)://)[^/\s:]+:[^@\s]+@"
),
),
(
"rfc1918",
re.compile(
r"\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|"
r"192\.168\.\d{1,3}\.\d{1,3}|"
r"172\.(?:1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3})\b"
),
),
("email", re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.I)),
(
"env_secret",
re.compile(
r"(?im)^([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD))\s*=\s*.+$"
),
),
]
def redact(text: str) -> tuple[str, list[str]]:
receipt: list[str] = []
out = text
for name, rx in PATTERNS:
def repl(match: re.Match[str], label: str = name) -> str:
receipt.append(f"{label}:{match.start()}")
if match.lastindex:
return match.group(1) + f"<REDACTED:{label}>"
return f"<REDACTED:{label}>"
out = rx.sub(repl, out)
return out, receipt
def main() -> None:
if len(sys.argv) != 2:
print("usage: redact_context.py <file>", file=sys.stderr)
sys.exit(2)
src = Path(sys.argv[1])
text = src.read_text(encoding="utf-8", errors="replace")
cleaned, receipt = redact(text)
out = Path(str(src) + ".redacted")
rec = Path(str(src) + ".receipt")
out.write_text(cleaned, encoding="utf-8")
rec.write_text(("\n".join(receipt) + "\n") if receipt else "", encoding="utf-8")
print(f"wrote {out} with {len(receipt)} hits; receipt {rec}")
if __name__ == "__main__":
main()
Label the next block as an unexecuted example test, then run it only against synthetic fixtures you created. The point is not coverage theater; it is proving the redactor actually deletes the shapes you think it deletes. If this test fails on your laptop, you are not ready to brief an agent with a staging dump.
# Example test. Do not paste production tokens into this file.
def test_redact_jwt_and_env() -> None:
sample = (
"Authorization: Bearer eyJhbGciOiJub25lIn0.e30.fakesig\n"
"STRIPE_SECRET_KEY=sk_live_example_not_real\n"
"postgres://app:hunter2@10.0.4.12:5432/orders\n"
)
cleaned, receipt = redact(sample)
assert "fakesig" not in cleaned
assert "hunter2" not in cleaned
assert "sk_live_example_not_real" not in cleaned
assert "10.0.4.12" not in cleaned
assert any(item.startswith("bearer:") for item in receipt)
assert any(item.startswith("env_secret:") for item in receipt)
Run the example against a staging dump before any agent call, and keep the receipt beside the ticket. If the receipt is empty, you probably pointed the redactor at the wrong input file. If the receipt is huge, you are briefing the model with material that should have stayed in room one. Either outcome remains useful, because both results beat pasting a raw production log into chat.
A second local gate belongs at the repository edge, so tracked secrets never become default context. These ordinary commands stay on your machine and they do not phone home by themselves. They will not replace a vault, but they catch the cases that make incident reviews painful.
# Example local gate. Pin the agent to the redacted path only.
set -euo pipefail
src="${1:?usage: gate_context.sh <file>}"
python3 redact_context.py "$src"
redacted="${src}.redacted"
receipt="${src}.receipt"
if [ ! -s "$receipt" ]; then
echo "receipt empty; confirm the redactor scanned the file you intended" >&2
fi
git grep -nE 'AKIA[0-9A-Z]{16}|BEGIN .*PRIVATE KEY|eyJ[A-Za-z0-9_-]+\.' -- \
':*.env' ':*.yml' ':*.yaml' ':*.log' ':*.md' || true
# Refuse to hand the live path to an agent file tool.
echo "agent may read: $redacted"
echo "agent must not read: $src"
When a bug needs a token to reproduce, mint a disposable fixture instead of laundering the real one. Replace customer names with stable aliases, then replace account IDs with obviously fake numeric ranges. Keep a small translator on your side of the boundary so you can map the model's advice back. That translator is part of the threat model, because it stops you from pasting the real roster just this once.
# Example policy function. Proposal only, not legal advice and not a control.
def may_send(kind: str, has_prod_ids: bool, has_secrets: bool) -> str:
if has_secrets:
return "block: strip credentials or recreate the case with fixtures"
if kind in {"customer_dump", "hr_export", "medical"}:
return "block: keep the bytes in room one"
if kind == "stack_trace" and has_prod_ids:
return "rewrite: replace hostnames, tokens, and correlation ids"
if kind in {"unit_test", "public_repro"}:
return "allow: still run the redactor on the bundle"
return "review: default deny until a human looks"
Think of that function as a customs officer who cannot be sweet-talked by a clever system prompt. Stack traces can travel after you rewrite identifiers, because the control flow is often the actual question. Customer tables cannot travel, because the model does not need a real person to rearrange a query. Secrets cannot travel even in comments, because comments still sit inside the same request body.
Regex scanners will miss novel secret formats, unicode lookalikes, and screenshots that never become text. A local model still writes swap files, core dumps, and IDE chat databases you might later back up to the cloud. This workflow is a seatbelt, not an airbag, and it will not satisfy a PCI or HIPAA auditor by itself. If you need a real control, you want DLP, a vendor review, and a written data processing agreement.
Do not use this approach as cover for sending production data to a hobby endpoint. Do not use it if your employer already forbids unaudited model providers, because a redactor does not create permission. Do not use it as a substitute for fixtures when the right move is to synthesize a failing test. Teams that already handle export-controlled or classified source should stay on their approved air-gapped path.
If you want a remote place to practice the redacted workflow, MonkeyCode's free model access and free server option are one such room three. Keep the sanitizer in room one, send only the .redacted file, and store the receipt with the ticket. The agent can still help you refactor the code once those secrets have left the brief. That is the whole method: you draw the boundary, then you talk across it on purpose.
Top comments (0)