DEV Community

Riley Lin
Riley Lin

Posted on

The Hostname in Your Diff Is a Clue

You can scrub every password from a prompt and still leak a working map of your company. Quasi-identifiers such as hostnames, ticket keys, fixture emails, and private IP ranges ride along with ordinary diffs. A free coding model will treat those leftovers as useful context rather than as accidental disclosure. Classify outbound text before it leaves the laptop, and redact that middle class as firmly as secrets.

Think of a git patch as a photocopy of your desk instead of a clean excerpt of source. The code sits in the center, but the sticky notes remain visible around the margins of every hunk. A path under /srv/payments-prod, a comment citing INC-1842, and a fixture using maya@client-acme.test all travel together. None of those strings is a credential, yet together they describe customers, production, and the incident you are still debugging.

When the destination is a free model or a free shared server, that photocopy leaves a building you do not control. MonkeyCode offers free model access and a free server option for coding sessions, which makes the outbound habit more important, not less. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Treat any remote assistant as a system that will keep paths, names, and tickets you did not mean to donate.

Most teams already hunt for keys and tokens, then relax once the scanner is quiet. That relaxation is the gap this workflow tries to close, because operational metadata is not a secret in the vault sense. It is still enough for a stranger to sketch your topology, your naming scheme, and sometimes a real person on the other side of a fixture. Public code can travel; neighborhood detail should stay on the machine that produced the patch.

A workable scheme uses three outbound classes and refuses to invent a fourth under time pressure. Secret means material that authenticates or decrypts, including passwords, tokens, and private keys that earlier filters already catch. Quasi-identifier means material that points at a place, a person, a customer, or an incident without opening the lock. Public-code means language, algorithms, and interfaces that would be reasonable on a public tracker after a boring review.

Deleted lines are the envelope most people forget to open. Git shows a minus prefix when you drop a hostname or a fixture email, and the model still reads that line as context. File headers are another quiet channel, because --- a/deploy/prod/us-east/payments.yaml already names a region and a domain. Stack traces behave the same way when a request id, a pod name, and an internal URL appear in the same paste. If you only scan added lines, you are proofreading the new sticky notes and mailing the old ones.

The artifact below is a proposed local classifier you can run on a staged diff or on a buffer you were about to paste. It is heuristic Python with the standard library, not a compliance product, and it should fail closed on unmatched path headers. Pipe git diff --cached into it, read the report on stderr, and copy only the redacted stdout if the remaining text still makes sense. Label this as an unproven filter until you have tried it on your own repository's last week of patches.

#!/usr/bin/env python3
"""outbound_classify.py — proposed local filter for diffs headed to a model.

Reads a unified diff or plain text on stdin. Writes a redacted body to stdout
and a line-oriented report to stderr. Heuristics only; do not treat silence
as proof that a snippet is safe to send.
"""
from __future__ import annotations

import re
import sys
from dataclasses import dataclass

SECRET_RE = re.compile(
    r"(?i)(api[_-]?key|secret|passwd|password|private[_-]?key|bearer\s+[a-z0-9._\\-]+)\s*[=:]\s*\S+"
)
QUASI_PATTERNS = (
    ("rfc1918", re.compile(r"\b(?:10\.\d{1,3}|192\.168|172\.(?:1[6-9]|2\d|3[0-1]))\.\d{1,3}\.\d{1,3}\b")),
    ("internal_host", re.compile(r"\b[\w.-]+\.(?:internal|corp|lan|local)\b", re.I)),
    ("ticket", re.compile(r"\b(?:INC|JIRA|PROD|SEC)[-_]\d{2,}\b")),
    ("email", re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.I)),
    ("kube", re.compile(r"\b(?:namespace|ns)=[a-z0-9-]+\b", re.I)),
    ("prod_path", re.compile(r"/(?:prod|production|live|payments-prod)(?:/|\b)", re.I)),
)
PATH_RE = re.compile(r"^(?:\\+\\+\\+|---)\\s+[ab]/(?P<path>.+)$")

@dataclass
class Hit:
    kind: str
    label: str
    line_no: int
    excerpt: str

def classify_line(line_no: int, line: str) -> list[Hit]:
    hits: list[Hit] = []
    if SECRET_RE.search(line):
        hits.append(Hit("secret", "credential_shape", line_no, line.strip()[:120]))
    for label, cre in QUASI_PATTERNS:
        if cre.search(line):
            hits.append(Hit("quasi", label, line_no, line.strip()[:120]))
    return hits

def redact(line: str) -> str:
    out = SECRET_RE.sub("[REDACTED_SECRET]", line)
    for label, cre in QUASI_PATTERNS:
        out = cre.sub(f"[REDACTED_{label.upper()}]", out)
    return out

def main() -> int:
    raw = sys.stdin.read().splitlines(keepends=True)
    hits: list[Hit] = []
    redacted: list[str] = []
    for i, line in enumerate(raw, start=1):
        path_m = PATH_RE.match(line.rstrip("\\n"))
        if path_m:
            path = path_m.group("path")
            if re.search(r"(?i)(prod|secret|customer|payment)", path):
                hits.append(Hit("quasi", "sensitive_path", i, path))
                prefix = line[: line.find("/")] 
                line = prefix + "/[REDACTED_PATH]\\n" if line.endswith("\\n") else prefix + "/[REDACTED_PATH]"
        hits.extend(classify_line(i, line))
        redacted.append(redact(line))
    for h in hits:
        sys.stderr.write(f"{h.kind}\t{h.label}\tL{h.line_no}\t{h.excerpt}\\n")
    sys.stderr.write(f"summary\tsecrets={sum(h.kind=='secret' for h in hits)}\\t"
                     f"quasi={sum(h.kind=='quasi' for h in hits)}\\n")
    sys.stdout.write("".join(redacted))
    return 2 if any(h.kind == "secret" for h in hits) else 0

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

A realistic dry run starts from the index, not from memory of what you think you typed. Stage the files you actually intend to discuss, then send only that patch through the filter so unstaged neighborhood files never enter the buffer. The commands below are ordinary git and python; adjust the interpreter if your workstation uses a different layout. Keep the report in a local file if you want to compare what the model would have seen against what you finally pasted.

chmod +x outbound_classify.py
git diff --cached > /tmp/outbound.patch
python3 outbound_classify.py < /tmp/outbound.patch > /tmp/outbound.redacted 2> /tmp/outbound.report
echo "exit was $?"
cat /tmp/outbound.report
less /tmp/outbound.redacted
Enter fullscreen mode Exit fullscreen mode

Suppose a cached hunk rewrites a fixture and also drops an old hostname. The report should mention email on the plus side and internal_host on the minus side, even though you already deleted the living machine name. The redacted file should still show the test structure, which is usually what you wanted the model to reason about. If the remaining patch is nonsense because every identifier vanished, that is a signal to describe the problem in invented names instead of pasting production residue.

Stack traces deserve a second pass because they look like evidence and therefore feel legitimate to share. A single Java or Go dump can carry a pod name, an RFC1918 address, and a customer-shaped id in adjacent frames, which is plenty of neighborhood for a model that is trying to be helpful. The tiny helper below is a proposed wrapper that classifies the trace as text, then refuses to print stdout when secrets appear. You still have to read the quasi hits; the tool will not know which request ids are public.

# Proposed: classify a captured trace before it enters a chat box.
python3 outbound_classify.py < /tmp/request-1842.trace > /tmp/trace.redacted 2> /tmp/trace.report
if grep -q '^secret' /tmp/trace.report; then
  echo "Refusing to copy a trace that still looks like a credential." >&2
  exit 1
fi
# Read quasi hits, then paste only /tmp/trace.redacted if the story survives.
Enter fullscreen mode Exit fullscreen mode

Limitations are not decorative here, because regex cannot see intent, encoding tricks, or screenshots. A hostname split across two lines, a base64 blob that decodes into a customer email, or an image of a dashboard will walk past this script without a sound. False positives will annoy you on public docs that mention example.local, and false negatives will appear the first time someone invents a ticket format the patterns do not know. The classifier also does not judge license, so copied vendor code can still be a problem after every hostname is gone.

You should not use this approach as a substitute for a real DLP program when the buffer contains regulated patient data, payment card dumps, or anything under legal hold. Air-gapped shops that already forbid hosted models do not need a friendlier paste path; they need the network policy they already wrote. If your threat model includes a determined insider, a local Python filter is the wrong control, because it runs with the same privileges as the person it is supposed to restrain. In those cases the honest move is to keep the assistant offline or not to use one.

The order of operations is the entire method, and it stays cheap once the script exists. Classify the staged diff, read the quasi hits like a short code review, then ask the model only about the redacted remainder. Free remote capacity does not change that sequence, and it does not make a hostname less informative. If the redacted patch cannot explain the bug, rewrite the question with synthetic names until a stranger could not find your building on a map.

Top comments (0)