DEV Community

Riley Lin
Riley Lin

Posted on

Treat Remote Inference as Untrusted Egress

You should treat every remote coding-model request as data leaving a trust boundary you control. The editor window still looks like a local tool, so stack traces, env files, and customer ids travel with the prompt. A small preflight scan on the bundle you are about to send will stop more leaks than another reminder to be careful.

A coding assistant spans several networks even when the interface is only a sidebar in your editor. Your buffer and language server stay on the laptop, while completion, chat, or agent steps often run remotely. Retries, provider logs, and support traces may persist after the answer returns to your screen. That means the real trust boundary is the outbound request, not the tab that rendered the reply.

Picture the workspace as a sealed workshop and the remote model as a loading dock at the curb. You can roll a cart to that dock, but you do not owe the dock every drawer in the shop. Once a crate sits on the truck, you cannot retrieve one bolt because you later disliked the label. Classification therefore happens before packaging, not after the model has already seen the file.

The dangerous payload is rarely a password you typed into the chat box on purpose. It is the kubeconfig that the agent indexed, or the HAR file you attached to explain a timeout. It is the JWT printed in a failing test, or the customer email sitting in a fixture named happy_path.json. Those objects feel like debugging texture, yet they are live credentials and personal data under another name.

You can make the boundary visible by building a context manifest before any remote call leaves the machine. The manifest lists paths and matched secret patterns, then refuses to proceed when matches remain in the tree. The script below is a labeled proposal you can run locally; it is not a substitute for vendor DLP. Keep the report next to the bundle so later readers can see what you believed was safe to send.

#!/usr/bin/env python3
"""Proposal: refuse to ship workspace context that still matches secret-like patterns."""
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

PATTERNS = [
    ("aws_access_key", re.compile(r"AKIA[0-9A-Z]{16}")),
    ("pem_private_key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")),
    ("bearer_token", re.compile(r"(?i)bearer\s+[A-Za-z0-9._\-]+={0,2}")),
    (
        "generic_secret_assign",
        re.compile(
            r"(?i)(api[_-]?key|secret|token|password)\s*[:=]\s*['\"][^'\"]{8,}['\"]"
        ),
    ),
    ("connection_string", re.compile(r"(?i)(postgres|mysql|mongodb(\+srv)?)://[^\s'\"]+")),
    (
        "email_shaped",
        re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}"),
    ),
]

SKIP_DIRS = {".git", "node_modules", "dist", ".venv", "__pycache__"}
TEXT_SUFFIXES = {
    ".py", ".js", ".ts", ".tsx", ".json", ".yml", ".yaml",
    ".env", ".md", ".txt", ".log", ".http", ".toml", ".har",
}


def iter_files(root: Path):
    for path in root.rglob("*"):
        if not path.is_file():
            continue
        if any(part in SKIP_DIRS for part in path.parts):
            continue
        if path.suffix.lower() not in TEXT_SUFFIXES and path.name not in {".env", ".env.local"}:
            continue
        yield path


def scan(root: Path, allow_emails: bool) -> list[tuple[str, str, int, str]]:
    hits: list[tuple[str, str, int, str]] = []
    for path in iter_files(root):
        try:
            text = path.read_text(encoding="utf-8", errors="replace")
        except OSError:
            continue
        for name, pattern in PATTERNS:
            if name == "email_shaped" and allow_emails:
                continue
            for line_no, line in enumerate(text.splitlines(), start=1):
                if pattern.search(line):
                    hits.append((str(path), name, line_no, line.strip()[:120]))
    return hits


def main() -> int:
    parser = argparse.ArgumentParser(description="Scan a context directory before remote inference.")
    parser.add_argument("root", type=Path, help="Directory you plan to send or attach")
    parser.add_argument("--allow-emails", action="store_true")
    parser.add_argument("--report", type=Path, default=Path("context-egress-report.txt"))
    args = parser.parse_args()
    root = args.root.resolve()
    if not root.is_dir():
        print(f"not a directory: {root}", file=sys.stderr)
        return 2
    hits = scan(root, allow_emails=args.allow_emails)
    lines = [f"root={root}", f"hits={len(hits)}"]
    for path, name, line_no, snippet in hits:
        lines.append(f"{path}:{line_no}:{name}:{snippet}")
    args.report.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"wrote {args.report} with {len(hits)} hit(s)")
    return 1 if hits else 0


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

Save that file as preflight_context.py and point it at the directory you would otherwise zip for an assistant. A non-zero exit code means you still have pattern hits, so you should redact or drop those paths first. The email rule is noisy on purpose, because fixtures often hide production identities inside supposedly fake data. If your sample data is synthetic and documented, you can pass --allow-emails without pretending the other rules are optional.

python3 preflight_context.py ./context-bundle
echo $?
# 0 means this gate found no pattern hits in the tree you named.
Enter fullscreen mode Exit fullscreen mode

That command writes context-egress-report.txt beside your bundle so the decision is an artifact, not a memory. You should commit the report only when it contains no live secrets, because the report reprints matched lines. Treat a hit on a private key or connection string as a failed ship, not as a style warning to ignore. If the gate is clean, copy allowed files into a staging directory and send only that staging tree downstream.

# Proposal: copy only allowlisted paths after a clean preflight.
set -euo pipefail
SRC="./app"
STAGE="./context-stage"
rm -rf "$STAGE"
python3 preflight_context.py "$SRC"
mkdir -p "$STAGE"
# Keep the failing module and its test; leave env files, kubeconfig, and dumps behind.
cp "$SRC/src/billing.py" "$STAGE/"
cp "$SRC/tests/test_billing.py" "$STAGE/"
python3 preflight_context.py "$STAGE"
Enter fullscreen mode Exit fullscreen mode

The snippet above is labeled as a proposal because your allowlist will differ, and copying the whole source tree often reintroduces secrets. Notice the second preflight on the staging directory; a clean scan of the repo root is not a scan of what you send. A path you forgot to copy cannot leak, which is a better default than hoping a giant archive was harmless.

After the staging tree is clean, you still need a model to rewrite tests, explain a stack, or sketch a patch. MonkeyCode provides free model access and a free server option for that mechanical step once the bundle is already redacted. Disclosure: This article was prepared as part of MonkeyCode's product outreach. You still run the preflight on your side, then send the smallest staging tree that can answer the question.

Free hosting does not move the trust boundary back onto your laptop, and it does not erase provider logs. If a later agent mode re-reads the full repository, you must run the same gate against that larger tree too. Logs on a free server are still logs, so you should not treat cost as a privacy control. The model can be useful while remaining firmly on the untrusted side of the dock.

This approach fails closed on regular expressions, which means novel secret formats and obfuscated values will sail through. It also fails open if you point the scanner at a tiny subdirectory while the agent later indexes the whole repository. Do not use this gate as your only control when you handle health data, payments, or government identifiers. Air-gapped teams, and anyone contractually barred from third-party inference, should not send a staging tree at all.

The workflow fits individual developers who already paste context into assistants and want a repeatable stop before egress. It also fits small teams that can agree on a staging folder and keep production dumps off developer laptops. It does not fit a compliance program that needs attested deletion, regional residency, or a signed subprocessors list. Those programs need contracts and architecture, not a Python file that prints interesting lines from disk.

Draw the inference boundary in the same place you draw other egress: before the bytes leave the host. If you try the preflight on a real fixture directory, you will usually find at least one email or token. Redact that hit, rebuild the staging tree, and only then ask the remote model to work on what remains. Keep the model on the far side of the dock, and keep the keys in the workshop where they started.

Top comments (0)