Audit Agent File Reads Before a Remote Model Sees Your kubeconfig A developer demonstrates how AI coding agents can inadvertently leak secrets such as database passwords and kubeconfig keys by reading local files and sending them to remote models. The lab walkthrough highlights the trust boundary between local workspace and remote model context, proposing a scanner to filter sensitive content before it reaches the model. I asked an agent one question: "why won't this stack start?" It called read file on docker-compose.yml . Then it read .env . Then both blobs went into a remote completion request. The model answered. The database password went with the prompt. That is not a novel exploit. It is a default tool loop. Local disk and a remote model are different trust domains, free endpoint or not. If your harness can read the workspace, you already moved a boundary. Did you notice? This walkthrough is a lab. I am not claiming a CVE, a production incident, or a named vendor leak. I am claiming something dumber and more common: file-read tools collapse the workspace into model context unless you enforce an invariant before the HTTP body is built. Here is the event chain I keep seeing in traces, including MCP filesystem servers and homegrown read file tools: docker-compose.yml , then .env , then a kubeconfig "just to check the port". messages . client-key-data , and MYSQL ROOT PASSWORD . Step 3 is where people argue about product features. Step 5 is the actual trust-boundary violation. The model never needed the secret to explain a bind-mount typo. So why was the secret in the prompt? Treat the remote model as an unmanaged log sink. Free does not mean on-box. Paid does not mean your DPA covers every paste. Self-hosted still has operators, disks, and debug dumps. The invariant is the same: secrets do not cross the harness → model boundary. Assets: compose passwords, .env values, kubeconfig user keys, SSH private keys, cloud access keys, customer identifiers that happen to sit in fixtures. Actors: a developer, an IDE agent, an MCP filesystem server, a remote completion API, and whoever can read conversation logs later. Trust boundaries: | Boundary | What crosses it | Default control | Failure mode | |---|---|---|---| | Workspace → agent tool | Raw file bytes | None, if read file is unconstrained | Agent opens .env because the user said "debug boot" | | Tool result → messages | Full file, no redaction | Maybe a token cap | Secret survives truncation if it sits near the top | | Harness → remote model | HTTPS JSON | TLS only | Provider, proxy, or support log now holds the secret | | Model → your logs | Echoed values, diffs, "fixed compose" | Debug logging | You re-leak the secret into CI artifacts | Positive fixture should be allowed into context : a README that says "set the database password in your secrets manager". Negative fixture must be denied : a compose file with a real-looking password, or any kubeconfig users .user.client-key-data . If your scanner cannot tell those two apart, you do not have a gate. You have a word filter. Pinned for this lab: Python 3.12 , stdlib only. Unexecuted against your laptop until you run it. Label it that way in review comments. Create a tiny workspace: lab-agent-context/ README.md positive docker-compose.yml negative kubeconfig.canary negative notes.txt positive scan agent context.py test scan agent context.py README.md : lab-agent-context Set the database password in your secrets manager. Do not commit .env files. notes.txt : Bind mount is ./api:/app. Restart the api service after changing ports. docker-compose.yml canary only — rotate if you ever paste this into a real chat : services: db: image: postgres:16 environment: MYSQL ROOT PASSWORD: "canary-compose-password-not-for-prod" DATABASE URL: "postgres://app:canary-db-url-not-for-prod@db:5432/app" Yes, the image is Postgres and the variable says MySQL. Agents copy that kind of mess into prompts all the time. That is the point. kubeconfig.canary fake key material, still treated as secret-shaped : apiVersion: v1 kind: Config clusters: - name: lab cluster: server: https://127.0.0.1:6443 users: - name: lab-admin user: client-key-data: Y2FuYXJ5LWt1YmUtY2xpZW50LWtleS1ub3QtZm9yLXByb2Q= bash /usr/bin/env python3 """Pre-flight gate for agent file reads. Lab fixture, not a secret scanner product.""" from future import annotations import re from pathlib import Path DENY NAMES = { ".env", ".env.local", "id rsa", "id ed25519", "credentials", } DENY SUFFIXES = { ".pem", ".p12", ".kubeconfig", } DENY BASENAME SUBSTR = "kubeconfig", CONTENT RULES = "private key", re.compile r"BEGIN ?:OPENSSH |RSA ?PRIVATE KEY" , "aws access key", re.compile r"\bAKIA 0-9A-Z {16}\b" , "github pat", re.compile r"\bgh pousr A-Za-z0-9 {20,}\b" , "client key data", re.compile r"\bclient-key-data\s :\s \S+" , "jdbc or pg url", re.compile r"\b ?:postgres|mysql|mongodb :// ^\s +", re.I , "assignment password", re.compile r" ?im ^\s ?:password|passwd|mysql root password|secret key \s := \s \S+" , , DOC PASSWORD = re.compile r"set ?:the |your ?. password|do not commit|secrets manager", re.I, def path denied path: Path - str | None: name = path.name.lower if name in DENY NAMES: return f"deny-name:{name}" if path.suffix.lower in DENY SUFFIXES: return f"deny-suffix:{path.suffix}" if any s in name for s in DENY BASENAME SUBSTR : return f"deny-kubeconfig-name:{name}" return None def content denied text: str - str | None: Allow documentation that talks about passwords without embedding one. if DOC PASSWORD.search text and not re.search r" := \s \S+", text : return None for label, rx in CONTENT RULES: if rx.search text : return f"deny-content:{label}" return None def audit file path: Path - str | None: hit = path denied path if hit: return hit text = path.read text encoding="utf-8", errors="replace" return content denied text def audit paths paths: list Path - list tuple str, str : findings = for path in paths: hit = audit file path if hit: findings.append str path , hit return findings if name == " main ": import sys targets = Path p for p in sys.argv 1: findings = audit paths targets if not findings: print "PASS: no denied files in proposed agent context" raise SystemExit 0 print "FAIL: refused to copy these paths into model context" for path, reason in findings: print f" {path}: {reason}" raise SystemExit 1 Expected failure evidence when you pass the negative files run it; do not take my word : python3.12 scan agent context.py README.md notes.txt PASS: no denied files in proposed agent context python3.12 scan agent context.py README.md docker-compose.yml kubeconfig.canary FAIL: refused to copy these paths into model context docker-compose.yml: deny-content:assignment password kubeconfig.canary: deny-kubeconfig-name:kubeconfig.canary If the second command prints PASS , your gate is dead. Do not "fix" that by sending the files to a model to ask why. python from pathlib import Path from scan agent context import audit file, audit paths ROOT = Path file .parent def test positive readme is allowed : assert audit file ROOT / "README.md" is None def test positive notes are allowed : assert audit file ROOT / "notes.txt" is None def test negative compose is denied : hit = audit file ROOT / "docker-compose.yml" assert hit == "deny-content:assignment password" def test negative kubeconfig is denied : hit = audit file ROOT / "kubeconfig.canary" assert hit.startswith "deny-kubeconfig-name:" def test batch fails closed : findings = audit paths ROOT / "README.md", ROOT / "docker-compose.yml", ROOT / "notes.txt" assert p for p, in findings == str ROOT / "docker-compose.yml" python3.12 -m pytest test scan agent context.py -q Wire the same function into the harness before you append a tool result to messages . After the HTTPS call is the wrong layer. You cannot un-send a prompt. I use self-hosted and remote coding models the same way I use CI runners: assume the job can see only what the gate allowed. A free completion endpoint does not move kubeconfig into a safer domain. It moves it into someone else's. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source AI development platform. The operator-supplied facts I will use, and only those, are that it currently offers free model access and a free server option. I am not pinning model names, token quotas, hardware, uptime, or duration here — those change, and stale numbers are worse than no numbers. If you need a box to run this fixture against a throwaway workspace, that free server option is one place to park the gate. It is not a substitute for the gate. Remote-free and self-hosted-free share a rule: the model may see the repo subset you explicitly allow, not the repo subset the agent felt like opening. What I would still refuse to send, even to a server I started myself: ::add-mask:: failures. / was the glob. Free tokens do not change classification. They change your bill. | Layer | Prevent | Detect | Recover | |---|---|---|---| | Paths | Deny .env , id , kubeconfig , .pem in the tool allowlist | CI test above on a canary tree | Delete the file from agent memory stores; you still rotate | | Content | Strip PASSWORD= , URLs with userinfo, client-key-data | Fail closed on first hit; no "ask the model if this is secret" | Rotate the canary and every sibling credential in that file | | Harness | Cap tool fan-out; no recursive $HOME | Log tool names and paths, never values, at info level | Treat the conversation id as compromised storage | | Model | Prefer on-box models for anything that touched infra files | Red-team with the negative fixtures on every harness change | Provider deletion requests are not cryptographic erasure | Notice the empty cell you wanted: there is no recover step that makes a remote prompt un-seen. Rotation is the recovery. Logging the secret again in the incident ticket is a second leak. I have watched that happen. Have you checked your last "agent debug" gist? This regex gate is a regression fixture. It will miss unstructured secrets, miss encodings, and false-positive on some docs if you loosen DOC PASSWORD . It will not replace vaults, IAM, or a real secret scanner in CI. Do not use this approach as your only control if you are in a regulated environment that forbids sending repo contents to a third party at all. Do not grant an MCP filesystem server ~ and then hope the deny list is complete. Do not paste the failing compose file into a public model to "see if it notices". That is how the lab becomes a leak. Also do not confuse self-hosting with secrecy. Root on the box still reads GPU swap, still reads chat SQLite, still reads nginx access logs if you were sloppy with URLs. The invariant belongs in the harness and in CI, not in a banner that says the model is free. Which check is an enforceable invariant in CI — "compose files with assignment-shaped passwords never enter messages " — and which layer should own it: the MCP tool, the agent harness, or the model gateway? If your answer is "the model will be careful", you do not have a boundary. You have a wish.