Dev.to Security 🔐 Cybersecurity 👁 0 📖 8 min read

A Secret in the Prompt Is Already Shared

If a secret reaches a remote model, treat that secret as copied, retained, and no longer yours. You cannot recall a context window the way you revoke a token after a leak. The practical move is to classify files before a

If a secret reaches a remote model, treat that secret as copied, retained, and no longer yours. You cannot recall a context window the way you revoke a token after a leak. The practical move is to classify files before any assistant reads them, not after a worrying paste. This walkthrough maps trust boundaries around logs and secrets, then adds a local preflight gate.

Think of the assistant as a courier who photocopies every page you hand over at the door. Your laptop is building A, while the model and its logs live across town in building B. Trust stops at the process that builds the prompt, because later hops follow someone else's retention. You should decide what may cross that door while the files still sit quietly on disk.

A workable picture of the path has four rooms, and each room keeps a different kind of memory. The workspace holds source, environment files, crash dumps, and shell history you never filed as documentation. The client then gathers those bytes into a prompt, a tool trace, or an attachment that looks routine. Remote inference parses the payload, after which operator logs and support tooling may keep another durable copy.

You already avoid dumping raw private keys, yet the risky files usually look like ordinary engineering work. Application logs reprint authorization headers after a failed request, long after the original incident feels closed. Test fixtures replay production payloads with real customer identifiers because someone needed a failing case to pass. Heap dumps and profiler output embed connection strings simply because the process still had them in memory.

Transcript files from yesterday's debugging session are delayed pastes waiting for the next seemingly innocent model call. Continuous integration artifacts often stitch together secrets, branch names, and internal hostnames in a single text blob. Editor swap files and forgotten debug.json dumps sit beside the code and get swept into "include the repo" habits. If you cannot explain why a file is safe to publish, it is not safe to place in a prompt.

Before you open an assistant on a repository, name the classes of data that must never leave the machine. Credentials and tokens form the first class, including cloud keys, session cookies, and private signing material. Personal and customer data form the second class, even when they appear inside a failing unit test. Operational residue is the third class, covering logs, core files, environment files, and anything stored under SSH directories.

A fourth class is contractual or regulated text that your company already forbids sending to any outside vendor. The question's cleverness does not change that classification, because the model cannot unlearn a paragraph later. Keep the matrix below next to the repository as a preflight check, not as a substitute for legal review.

Data class Typical location Cross the remote boundary?
Credentials and signing material .env, *.pem, CI variables, cloud key files Never
Customer or staff records fixtures, CSV exports, ticket dumps Never
Operational residue logs/, crash dumps, shell history, .ssh/ Not until a human redacts it
Ordinary source without secrets src/, public docs, tests with fake data Maybe, after the local gate
Counsel-flagged or regulated text anything already under a legal hold Never

The gate below is a proposed local filter, not a certified control, so treat every finding as advisory guidance. It walks a tree, skips deny-name patterns, scans remaining text for key-shaped tokens, and writes two artifacts. One artifact is a redacted question file you might later send if the remaining problem is still answerable. The other artifact is an audit of what would have crossed the boundary if you had shared the tree blindly.

#!/usr/bin/env python3
"""Proposed preflight gate. Advisory only. Not a compliance control."""
from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

DENY_NAMES = {
    ".env",
    ".env.local",
    ".env.production",
    "id_rsa",
    "id_ed25519",
    "credentials.json",
    "service-account.json",
}
DENY_SUFFIXES = {".pem", ".p12", ".pfx", ".key", ".kdbx"}
DENY_PARTS = {"secrets", ".ssh", "kube", "terraform.tfstate"}
SKIP_DIRS = {".git", "node_modules", "dist", "build", ".venv", "__pycache__"}

KEY_SHAPED = re.compile(
    r"(?i)(api[_-]?key|secret|token|password|authorization|bearer)\s*[:=]\s*\S+"
)
PEM_BLOCK = re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")


def denied_path(path: Path) -> str | None:
    name = path.name.lower()
    if name in DENY_NAMES or path.suffix.lower() in DENY_SUFFIXES:
        return "deny-name"
    for part in path.parts:
        if part.lower() in DENY_PARTS:
            return "deny-path"
    return None


def scan_text(text: str) -> list[str]:
    hits: list[str] = []
    if PEM_BLOCK.search(text):
        hits.append("pem-block")
    if KEY_SHAPED.search(text):
        hits.append("key-shaped-assignment")
    return hits


def redact_line(line: str) -> str:
    line = PEM_BLOCK.sub("[REDACTED_PEM]", line)
    return KEY_SHAPED.sub(lambda m: m.group(1) + "=[REDACTED]", line)


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
        yield path


def main() -> int:
    parser = argparse.ArgumentParser(description="Proposed workspace preflight gate")
    parser.add_argument("root", type=Path)
    parser.add_argument("--out", type=Path, required=True)
    args = parser.parse_args()
    root = args.root.resolve()
    out = args.out
    out.mkdir(parents=True, exist_ok=True)

    audit_lines: list[str] = []
    clean_chunks: list[str] = []
    blocked = 0

    for path in iter_files(root):
        rel = path.relative_to(root)
        reason = denied_path(path)
        if reason:
            blocked += 1
            audit_lines.append(f"BLOCK {rel} ({reason})")
            continue
        try:
            text = path.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            audit_lines.append(f"SKIP {rel} (binary)")
            continue
        hits = scan_text(text)
        if hits:
            blocked += 1
            audit_lines.append(f"BLOCK {rel} ({','.join(hits)})")
            continue
        redacted = "\n".join(redact_line(line) for line in text.splitlines())
        clean_chunks.append(f"# FILE {rel}\n{redacted}\n")
        audit_lines.append(f"ALLOW {rel}")

    (out / "audit.txt").write_text("\n".join(audit_lines) + "\n", encoding="utf-8")
    (out / "question.txt").write_text(
        "Answer from the allowed files only. Do not ask me to paste blocked paths.\n\n"
        + "\n".join(clean_chunks),
        encoding="utf-8",
    )
    print(f"wrote {out}/audit.txt and {out}/question.txt")
    print(f"blocked={blocked}")
    return 1 if blocked else 0


if __name__ == "__main__":
    sys.exit(main())

Run the script from the repository root so relative paths in the audit match the files you almost shared. A non-zero exit means the script believes something sensitive still sits inside the candidate set you nearly exported. Read the audit before you negotiate with the findings, because deleting evidence is not the same as reducing risk.

python3 context_gate.py . --out /tmp/assistant-preflight
echo "exit=$?"
sed -n '1,40p' /tmp/assistant-preflight/audit.txt
wc -l /tmp/assistant-preflight/question.txt

You can tighten the deny names for your stack without pretending the script is a general secret scanning product. Teams that keep Terraform state, Kubernetes secret manifests, or dumped JWTs in scratch folders should add those paths. The default should remain "do not send" unless a file earns a place in a short, reviewed allow set. An allow set that includes src/ and docs/ is easier to defend than a deny set you update under pressure.

Command output from the assistant is another doorway for the same classes, so keep a gate on the way back. If you let a tool run environment dumps, git diffs against production config, or shell history, the trust boundary expands immediately. A safer pattern wraps risky commands with an allowlist, then forwards only truncated standard output into later prompts. The snippet below is an example you must adapt; it is not a drop-in security product for mixed fleets.

#!/usr/bin/env bash
# Proposed command wrapper. Example only. Adapt before any real use.
set -euo pipefail
ALLOW_REGEX="${ALLOW_REGEX:-^(ls|pwd|git status|git log --oneline -5)$}"
cmd="$*"
if [[ ! "$cmd" =~ $ALLOW_REGEX ]]; then
  printf 'refusing command not in allowlist: %s\n' "$cmd" >&2
  exit 2
fi
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
bash -lc "$cmd" >"$tmp" 2>&1 || true
python3 - "$tmp" <<'PY'
from pathlib import Path
import re, sys
text = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
pat = re.compile(r"(?i)(api[_-]?key|secret|token|password|authorization)\s*[:=]\s*\S+")
for line in text.splitlines()[:80]:
    print(pat.sub(lambda m: m.group(1) + "=[REDACTED]", line)[:240])
PY

Notice that the wrapper refuses to forward lines that look like secret assignments, even when the command seemed boring. Treat the model like a journalist, and treat your shell like an unedited interview tape sitting on the table. You do not hand over the tape because a single question sounded technical and the deadline felt close. You hand over a transcript you already reviewed, with names, tokens, and internal hosts already taken out on purpose.

Once the gate writes a clean question file, you may still want a remote second opinion on the remaining puzzle. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option that can host that hop after local redaction is done. You can send the already cleaned question file there if a remote pass is still useful to you.

That does not make the remote side trusted; it only means you are sending already-sanitized text instead of the raw workspace. If your policy forbids any remote inference, stop at the audit file and keep the remaining work on the machine. A cleaned file can still overshare architecture details that your threat model never classified as a secret. Re-read the allow set with that in mind before any client process attaches the file to a remote request.

Limitations matter, because a regular-expression gate will miss secrets that refuse to look like secrets in isolation. A password in a comment, a customer name in a screenshot, or a token split across two lines will pass. Entropy checks punish legitimate long hashes in lockfiles and can lull you into deleting the wrong kind of evidence. Free remote capacity is still somebody else's machine with logs, and this workflow is not a confidential enclave.

You should not use this approach when you need a compliance boundary rather than a careful personal habit. Regulated workloads, air-gapped product code, and incident data from real users belong in tools your security team approved. Do not treat a blog script as a substitute for data loss prevention, a vendor review, or an internal model. If your question cannot be asked without the secret, change the system so the secret is no longer required.

The core conclusion stays simple when the assistant, the tools, and the logs all start talking at once. Classify first, redact second, and only then decide whether a remote model belongs anywhere in the loop. The context window will keep a copy of whatever you allow across that door, including jokes that contain tokens. Your deny list is the last control that still lives entirely on your side of the wall.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.