The safest habit with an AI coding assistant is to treat every attached file as a data export. You are not brainstorming in a private notebook; you are packing a crate that may travel off-box. Once tokens leave your workstation, you cannot unsay a customer fixture, a staging hostname, or an unfinished auth flow. Classification before you attach is cheaper than writing an incident timeline after a careless paste.
Think of the context window as airport luggage rather than a scratch pad on your desk. Security does not inspect your whole house; it inspects what you chose to put on the belt. An agent with file tools widens that belt, because the model can pull files you never meant to show. Your job is to decide, in advance, which repository paths are allowed to board.
Information disclosure is the dominant threat, and it rarely looks like a cinematic leak. It looks like a test fixture with real emails, a screenshot of a dashboard, or a .env.example that is not actually an example. Tampering shows up when a tool call reads parent directories, lockfiles, or editor history you did not attach. Repudiation follows when the session transcript is the only record, and that transcript lives on someone else's disk.
Spoofing matters when a helper extension claims to stay local while posting prompts to a remote endpoint. Elevation of privilege is quieter: the agent runs a shell command, then pastes stdout that contains a token you forgot. You do not need a full STRIDE workshop to use this. You need a preflight that answers one question: what class of data is about to leave, and who receives it?
A useful preflight is a four-way decision, not a yes-or-no secret scan. Secrets are only one class, and you already know not to paste a production password. The misses that hurt teams are adjacent: customer-shaped fixtures, internal URLs, license-incompatible snippets, and comments that name unreleased products. Treat those as export classes with different gates, the way a company treats public docs versus restricted source.
| Data class | Typical clues in a repo | Default gate | Why the gate exists |
|---|---|---|---|
| Credential material |
.env, PEM armor, AKIA, ghp_, BEGIN PRIVATE
|
Hold on device | Remote context is a copy you cannot reliably erase |
| Personal data | Emails, phone patterns, government IDs in fixtures | Redact or synthesize | Assistants do not inherit your retention or deletion policy |
| Internal topology | Corp hostnames, RFC1918 plus service names, VPN paths | Review, then strip | Topology helps an attacker even without a password |
| Privileged commentary |
TODO: bypass auth, lawsuit notes, unreleased names |
Hold or rewrite | Comments are often more sensitive than the code they decorate |
| License-bound code | Vendor headers, All rights reserved, copied SDK samples |
Do not attach | A model reply can launder restricted text into your branch |
| Ordinary implementation | Public algorithms, your own UI copy, dummy schemas | Allow | This is the cargo the assistant is actually for |
You should not memorize that table during a late debugging session. You should rehearse the export the same way you rehearse a deploy: with a command that fails closed. The script below is a local classifier you point at the files you plan to attach, or at a manifest the agent would be allowed to read. It is a rehearsal, not a DLP product, and it will miss encoded blobs, screenshots, and cleverly named files. Run it anyway, because most leaks are boring and textual.
Save the following as prompt_export_classifier.py in a tools directory you actually commit. Keep the pattern lists short on purpose so you can read every rule before you trust it. Extend the tuples when your company adds a hostname suffix or a ticket prefix that should never ride along with a stack trace.
#!/usr/bin/env python3
"""Rehearse an AI-coding prompt export. Local workflow example, not production DLP."""
from __future__ import annotations
import argparse
import re
from dataclasses import dataclass
from pathlib import Path
MAX_BYTES = 1_000_000
SKIP_SUFFIXES = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.pdf', '.zip', '.wasm', '.exe'}
@dataclass(frozen=True)
class Rule:
data_class: str
gate: str # HOLD | REDACT | REVIEW
pattern: re.Pattern[str]
RULES = (
Rule('credential', 'HOLD', re.compile(r'BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY')),
Rule('credential', 'HOLD', re.compile(r'(?i)(api[_-]?key|secret|passwd|password)\s*[=:]\s*\S+')),
Rule('credential', 'HOLD', re.compile(r'\b(AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|xox[baprs]-)')),
Rule('personal', 'REDACT', re.compile(r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b', re.I)),
Rule('personal', 'REDACT', re.compile(r'\b(?:\+?\d{1,3}[-.\s])?(?:\(?\d{3}\)?[-.\s])?\d{3}[-.\s]\d{4}\b')),
Rule('topology', 'REVIEW', re.compile(r'\b(?:10|127)(?:\.\d{1,3}){3}\b')),
Rule('topology', 'REVIEW', re.compile(r'\b(?:staging|internal|corp|intranet)\.[A-Za-z0-9.-]+\b', re.I)),
Rule('commentary', 'REVIEW', re.compile(r'(?i)\b(todo|fixme|hack|bypass auth|do not ship)\b')),
Rule('license', 'HOLD', re.compile(r'(?i)all rights reserved|not for redistribution|proprietary and confidential')),
)
GATE_RANK = {'ALLOW': 0, 'REVIEW': 1, 'REDACT': 2, 'HOLD': 3}
def classify_text(text: str) -> tuple[str, list[str]]:
gate = 'ALLOW'
hits: list[str] = []
for rule in RULES:
if rule.pattern.search(text):
hits.append(f'{rule.data_class}:{rule.gate}')
if GATE_RANK[rule.gate] > GATE_RANK[gate]:
gate = rule.gate
return gate, hits
def iter_targets(root: Path, paths: list[str]) -> list[Path]:
out: list[Path] = []
for raw in paths:
p = (root / raw).resolve() if not Path(raw).is_absolute() else Path(raw)
if p.is_dir():
out.extend(sorted(x for x in p.rglob('*') if x.is_file()))
elif p.is_file():
out.append(p)
return out
def main() -> int:
parser = argparse.ArgumentParser(description='Classify files before they enter a model context.')
parser.add_argument('--root', default='.', help='Repository root used to resolve relative paths')
parser.add_argument('--paths', nargs='*', default=[], help='Files or directories you plan to attach')
parser.add_argument('--manifest', help='Text file with one relative path per line')
args = parser.parse_args()
root = Path(args.root).resolve()
planned = list(args.paths)
if args.manifest:
planned.extend(Path(args.manifest).read_text(encoding='utf-8').splitlines())
planned = [p.strip() for p in planned if p.strip() and not p.strip().startswith('#')]
if not planned:
raise SystemExit('Provide --paths or --manifest so the rehearsal has a cargo list.')
worst = 'ALLOW'
print(f'root\t{root}')
for path in iter_targets(root, planned):
rel = path.name
try:
rel = str(path.relative_to(root))
except ValueError:
rel = str(path)
if path.suffix.lower() in SKIP_SUFFIXES:
print(f'REVIEW\t{rel}\tbinary-or-media (inspect separately)')
worst = 'REVIEW' if GATE_RANK['REVIEW'] > GATE_RANK[worst] else worst
continue
data = path.read_bytes()[:MAX_BYTES]
try:
text = data.decode('utf-8')
except UnicodeDecodeError:
print(f'REVIEW\t{rel}\tnon-utf8')
worst = 'REVIEW' if GATE_RANK['REVIEW'] > GATE_RANK[worst] else worst
continue
gate, hits = classify_text(text)
detail = ','.join(hits) if hits else 'ordinary'
print(f'{gate}\t{rel}\t{detail}')
if GATE_RANK[gate] > GATE_RANK[worst]:
worst = gate
print(f'BUNDLE_GATE\t{worst}')
return 0 if worst in {'ALLOW', 'REVIEW'} else 2
if __name__ == '__main__':
raise SystemExit(main())
A small fixture proves the rehearsal fails closed before you trust it on a real branch. Create tmp_export_fixtures/ok.ts with a public helper, and tmp_export_fixtures/bad.env with PASSWORD=please-do-not-ship. Then run the two commands below from the repository root and read the BUNDLE_GATE line like a CI job, not like a suggestion.
printf 'export function add(a: number, b: number) { return a + b }\n' > tmp_export_fixtures/ok.ts
printf 'PASSWORD=please-do-not-ship\n' > tmp_export_fixtures/bad.env
python3 prompt_export_classifier.py --root . --paths tmp_export_fixtures/ok.ts; echo exit:$?
python3 prompt_export_classifier.py --root . --paths tmp_export_fixtures; echo exit:$?
The first command should print ALLOW and return zero, because the helper is ordinary implementation. The second command should print HOLD on the env file, set BUNDLE_GATE to HOLD, and return two. If you wire this into a wrapper around your assistant, treat a non-zero exit as a refused boarding pass. Do not let the agent expand the manifest with .. walks after the check has already passed.
Agent tool loops make that last sentence load-bearing. A paste you curated is a closed suitcase; a read_file or workspace search is an open wardrobe. When you enable tools, classify the readable root, not the three files in the chat bubble. Put the allowed root in the manifest, keep secrets outside that root, and refuse to debug production with an agent that can shell out. The model cannot respect a boundary you never drew in the filesystem.
Remote inference changes the impact, not the method. A local dry run still uses the same classifier; a remote call simply means the crate really leaves the building. MonkeyCode is an open-source coding assistant with free model access and a free server option, which can be enough for a classification rehearsal against a live completion. Disclosure: This article was prepared as part of MonkeyCode's product outreach. You still generate the report on your machine first, because a free endpoint does not become a permitted export path for customer fixtures.
Limitations are not decorations here, and you should read them before you automate the gate. Regular expressions do not understand business context, so a public docs email address can look identical to a patient address. The script ignores most binaries, so a PNG of a payroll dashboard will skate through as REVIEW at best. Encoded secrets, chunked tokens, and files outside the manifest are invisible, which is why this is a rehearsal sitting in front of human judgment. If your threat model includes a determined insider, you need actual DLP and legal review, not a Python file in tools/.
You should not use this approach as a green light for regulated data, whatever the model vendor promises in a UI checkbox. Healthcare, payments, and employment records need a program that already decided whether any remote model is allowed. People under legal hold should not create extra copies in chat transcripts, even redacted-looking ones. If your company requires air-gapped source, skip remote free servers entirely and keep the assistant on a machine that cannot open an outbound socket.
Used narrowly, the habit is almost dull, which is the point of a good export control. You name the cargo, you run the rehearsal, and you attach only what the table would let through an ordinary data-sharing review. If you try a free remote model after that gate, keep the classifier in the repo and treat its report as a boarding pass, not as a souvenir from the session.
Top comments (0)