The first thing most teams do with a free coding agent is point it at a real repository. Why not? The tokens are free, the server is free, and the repo is right there. That instinct is exactly the one worth interrogating before you press the sync button.
I am not going to claim anyone's agent leaked anything. I have no such evidence, and vague fear is useless. What I can give you is a boundary you can measure yourself: decoy credentials, a classification pass, and an outbound capture point that tells you whether a string you planted crossed a line you can observe.
The four boundaries nobody labels
A remote coding agent moves your code across at least four trust boundaries. Most threat models collapse them into one, which is why the conversation goes nowhere.
B1 B2 B3 B4
laptop --> workspace sync --> model endpoint --> provider logs
| | | |
files remote FS prompt/body retention
.env? tmp dirs? tool output? policy?
B1 is what you choose to send. B2 is what the workspace keeps after the run. B3 is what actually enters a request body. B4 is what survives the session. You control B1 fully, B3 partially (if you can proxy), and B2/B4 mostly by contract and by deletion discipline.
So the useful question is not "is this tool safe?" It is: which boundary do I have evidence about, and which one am I guessing at?
Step 1 — Classify before you sync
Start with a file-level gate. This is the cheapest control and the one people skip. The script below is a template I wrote for this article and have not run against your repo; run it, read the output, and adjust the patterns to your stack.
#!/usr/bin/env python3
# preflight.py - classify repo files before a remote agent sees them.
import hashlib, json, re, sys
from pathlib import Path
DENY = [
(r"(^|/)\.env(\..*)?$", "env-file"),
(r"(^|/)id_(rsa|ed25519|ecdsa|dsa)$", "private-key"),
(r"\.(pem|p12|pfx|jks|keystore)$", "key-material"),
(r"(^|/)\.(aws|ssh|gnupg|kube)/", "credential-dir"),
(r"(^|/)(secrets?|credentials?)\.(ya?ml|json|toml|ini)$", "secret-config"),
(r"(^|/)terraform\.tfstate(\.backup)?$", "state-with-secrets"),
]
REVIEW = [
(r"(^|/)fixtures?/", "fixture-may-hold-real-data"),
(r"\.(sql|dump|bak|zip|tar|gz)$", "archive-or-dump"),
(r"(^|/)docker-compose\.ya?ml$", "may-inline-env"),
]
def classify(rel: Path):
p = rel.as_posix()
for pat, label in DENY:
if re.search(pat, p): return "deny", label
for pat, label in REVIEW:
if re.search(pat, p): return "review", label
return "allow", ""
def sha256(path: Path) -> str:
h = hashlib.sha256(); h.update(path.read_bytes()); return h.hexdigest()
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
files, counts = [], {"allow": 0, "review": 0, "deny": 0}
for p in sorted(root.rglob("*")):
if not p.is_file() or ".git" in p.parts:
continue
rel = p.relative_to(root)
verdict, label = classify(rel)
counts[verdict] += 1
files.append({"path": rel.as_posix(), "verdict": verdict,
"label": label, "sha256": sha256(p)})
json.dump({"root": str(root), "counts": counts, "files": files},
open("manifest.json", "w"), indent=2)
print(counts)
Run it and look at the deny list explicitly:
python3 preflight.py .
python3 - <<'PY'
import json
m = json.load(open("manifest.json"))
print("\n".join(f["path"] for f in m["files"] if f["verdict"] == "deny"))
PY
The manifest is the second half of the value: hashes plus an upload decision, so a later "what exactly did we send?" question has an answer that is not a memory.
Step 2 — Plant a decoy that must never leave
Classification tells you what you intended to exclude. A decoy tells you whether the intent held. Generate one unique marker per run, keep it outside the repo, and plant it only inside a fixture that a careless agent would happily read.
#!/usr/bin/env bash
# tools/decoys/make.sh - one decoy per run; marker never enters git.
set -euo pipefail
OUT="${DECOY_STORE:-$HOME/.local/share/agent-decoys}"
mkdir -p "$OUT"
RUN="$(date -u +%Y%m%dT%H%M%SZ)"
CANARY="sk-decoy-$(openssl rand -hex 16)"
printf '%s\n' "$CANARY" > "$OUT/$RUN.canary"
mkdir -p fixtures/agent-decoys
cat > fixtures/agent-decoys/staging.env <<EOF
# decoy value, not a real credential, nothing to rotate
STAGING_API_KEY=$CANARY
EOF
echo "canary: $OUT/$RUN.canary"
Two rules make this honest. First, the decoy is shaped like a secret but is worthless, so a hit is embarrassing, not an incident. Second, fixtures/ shows up as review in the manifest — which is exactly right, and proves your classifier is not asleep.
Step 3 — Watch the outbound body
Now the interesting part. If you can route the agent's traffic through a proxy you control, you can check B3 directly. Here is a mitmproxy addon that reads a canary file and reports any request body containing the marker.
# tripwire.py - mitmdump -s tripwire.py ; CANARY_FILE=/path/to/run.canary
import os, sys
from mitmproxy import http
CANARIES = [l.strip() for l in open(os.environ["CANARY_FILE"]) if l.strip()]
class Tripwire:
hits = 0
def request(self, flow: http.HTTPFlow) -> None:
body = flow.request.get_text(strict=False) or ""
for c in CANARIES:
if c in body:
Tripwire.hits += 1
print(f"HIT {flow.request.host}{flow.request.path}", file=sys.stderr)
def done(self) -> None:
print(f"tripwire hits: {Tripwire.hits}", file=sys.stderr)
addons = [Tripwire()]
Wire it up. Replace <agent-cli> with whatever entrypoint your tool ships — I am deliberately not naming one, because the point is the capture point, not the client.
mitmdump -q -s tripwire.py --set confdir=/tmp/mitm &
CANARY_FILE="$HOME/.local/share/agent-decoys/20261026T090000Z.canary" \
HTTPS_PROXY=http://127.0.0.1:8080 \
<agent-cli> run "add a health endpoint to fixtures/agent-decoys"
A hit means the marker reached a request body you could see. Zero hits means the marker did not cross that boundary during that run. It is not a privacy certificate, and reading it as one is how teams talk themselves into bad defaults.
Where a free tier actually fits
This is where free model access and a free server option become genuinely useful — not as a production platform, but as a disposable surface you can test boundaries on without spending budget. That is the honest framing.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is an open-source AI development platform, and per the current outreach brief it offers free model access (10 million tokens) and a free server option. Treat those as operator-supplied claims with moving parts: models, quotas, hardware, and expiry can change without notice, so read the current terms on the signup page before you build a workflow on them. What matters for this article is that a free workspace is a good place to run the decoy test, precisely because you should assume you will throw it away.
| Data class | Free remote workspace? | Control |
|---|---|---|
| Public OSS clone, no history rewrite | Yes | Pin commit SHA in the manifest |
| Internal source, no secrets | Yes, with review | Decoy run + tripwire before real work |
.env, key material, tfstate |
No | Deny in preflight, keep local |
| Customer data, dumps, fixtures from prod | No | Synthesize fixtures instead |
| Incident timelines, auth logs | No | Summarize by hand, not by paste |
| Throwaway reproduction of a public CVE | Yes | Ideal case for a free tier |
That last row is the one worth defending. Reproducing a public failure in a scratch workspace is the highest-value, lowest-risk use of somebody else's compute.
Prevent, detect, recover
| Layer | Mechanism | Evidence it worked |
|---|---|---|
| Prevent |
preflight.py deny list + repo allowlist |
manifest.json with hashes |
| Prevent | Decoy stored outside the tree | Canary file exists only locally |
| Detect |
tripwire.py on outbound bodies |
Hit line naming host and path |
| Detect | Post-run grep of the workspace | Marker absent after deletion |
| Recover | Rotate anything the decoy resembled, not the decoy | Rotation ticket referencing the run ID |
Note the recover row carefully. If a hit ever involves a real credential, the decoy is irrelevant — you rotate the real thing on the assumption it left, and you do it in dependency order rather than clicking the first "rotate" button you find.
Limitations I will not paper over
TLS pinning or a client that ignores HTTPS_PROXY will break the capture step, and then you are back to guessing about B3. Body matching only sees text; multipart uploads, gzip streams, and base64 blobs will slip past a naive in check. The decoy test says nothing about B4 retention, which is a policy question, not a packet question. And a single run is a sample, not a distribution — run it again after every agent or CLI upgrade, because the code path that assembles a prompt is exactly the kind of thing that changes quietly.
Who should skip this? Solo developers working on public repos with no credentials in the tree gain little. Teams under a compliance regime that forbids third-party inference entirely should not be testing the boundary at all; they should be enforcing an egress deny and stopping there.
What I would actually ship
Two files in CI, one habit. preflight.py fails the build if a deny file is staged for a synced workspace. tripwire.py runs before the first real task on any new workspace, with a fresh canary each time. The habit is asking, before every sync: which of the four boundaries am I about to trust without evidence?
So here is the boundary question I keep coming back to. The decoy check belongs in CI, where it is cheap and repeatable. The deny list belongs in the repo, where reviewers can argue about it. But the decision about what a workspace retains after the run — that one is not enforceable by you at all, is it? Which layer do you think should own it?
Top comments (0)