You should treat a free inference host as a foreign room, not as a harmless sandbox on your desk. Free access lowers the price of a trial, but it does not move that room inside your trust boundary. Secrets, raw logs, and customer identifiers still cross a network when you paste them into a prompt. If you cannot name the room and the exit, you are not ready to send the packet.
Picture your laptop as a house that has three working rooms and one locked porch outside. The first room holds source that you may discuss in abstract terms after you strip identifiers. The second room holds logs, tickets, and traces that still carry names and live account numbers. The porch is the remote host, and a free server waits there like any other vendor machine.
A coding agent is the courier who walks between those rooms while carrying a large open bag. The courier will pick up whatever the shell can read, including files you forgot and variables you inherited. You do not fix that habit by hoping the remote model will be polite about private material. You fix it by writing a deny manifest before the courier is allowed to leave the house.
The boundary is the packet, not the price
Price is a poor proxy for trust, because a cheap door can still open onto someone else's disk. When the host is free, you often know less about retention, tenancy, and operator logs than you would after a signed review. You should assume the porch keeps a copy until a current policy, which you have actually read, says otherwise. You should not invent a retention window, a region, or a deletion promise that the operator never published.
That assumption changes what you may pack, because a comment with an internal hostname belongs in the second room. A stack trace that includes a query string is still a log, even when you prefer to call it a bug report. An environment file is a key ring, and a key ring never rides in the same bag as a question. A screenshot of a dashboard is still a document, because pixels can carry the same names as plain text.
Tool output deserves the same sorting, because a command can print a secret the prompt never contained. If your agent runs env, git config --list, or a verbose HTTP client, the transcript becomes a second letter. You should gate the command before you gate the paragraph, since stdout can be louder than typing. A foreign room does not become safe because the secret arrived through a tool transcript instead of your keyboard.
Run a deny manifest before the session
The artifact below is a local preflight, not a certified control, and you should treat the rules as a starting deny list. It targets Python 3.10 or newer, walks the tree, and never prints a secret value it finds. You run it on a toy repository first, then you read every review hit before you open a remote session. If the script exits with a nonzero status, you keep the coding agent away from that tree.
demo=$(mktemp -d)
mkdir -p ${demo}/src
cat > ${demo}/src/app.py <<'EOF'
def add(a, b):
return a + b
EOF
cat > ${demo}/.env <<'EOF'
API_TOKEN=replace-me
EOF
python3 room_manifest.py --root ${demo} --env-file ${demo}/.env
python3 room_manifest.py --root ${demo} --include-process-env
rm -rf ${demo}
#!/usr/bin/env python3
'''Local preflight: classify paths and env keys before a remote session.
Illustrative example, not a certified control. A green result is not
approval to upload production data or regulated records.
'''
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
DENY_NAMES = {
'.env',
'.env.local',
'.npmrc',
'.pypirc',
'id_rsa',
'id_ed25519',
'credentials.json',
'service-account.json',
}
DENY_SUFFIXES = {'.pem', '.p12', '.pfx', '.key', '.kdbx'}
REVIEW_SUFFIXES = {'.log', '.har', '.pcap', '.trace', '.sql'}
REVIEW_NAMES = {'dump.sql', 'seed.sql', 'tokens.txt'}
HOT_ENV_HINTS = ('SECRET', 'TOKEN', 'PASSWORD', 'PASSWD', 'KEY', 'CREDENTIAL')
PREFIX_HINTS = ('AWS_', 'GOOGLE_', 'AZURE_', 'GITHUB_')
TEXT_RULES = (
('private_key_block', 'BEGIN PRIVATE KEY'),
('aws_access_key_prefix', 'AKIA'),
('github_pat_prefix', 'ghp_'),
('slack_bot_prefix', 'xoxb-'),
)
SKIP_DIRS = {'.git', 'node_modules', '.venv', 'venv', '__pycache__'}
MAX_BYTES = 200_000
def classify_path(path: Path) -> str:
name = path.name.lower()
if name in DENY_NAMES or path.suffix.lower() in DENY_SUFFIXES:
return 'deny'
if name in REVIEW_NAMES or path.suffix.lower() in REVIEW_SUFFIXES:
return 'review'
return 'allow'
def classify_env_key(key: str) -> str:
upper = key.upper()
if any(hint in upper for hint in HOT_ENV_HINTS):
return 'deny'
if upper.startswith(PREFIX_HINTS):
return 'review'
return 'allow'
def sniff_text(path: Path) -> list[str]:
if path.stat().st_size > MAX_BYTES:
return ['too_large']
try:
text = path.read_text(encoding='utf-8', errors='replace')
except OSError:
return ['unreadable']
return [rule for rule, needle in TEXT_RULES if needle in text]
def iter_env_keys(env_file: Path | None, include_process: bool) -> list[str]:
keys = set(os.environ) if include_process else set()
if env_file and env_file.is_file():
raw = env_file.read_text(encoding='utf-8', errors='replace')
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
keys.add(line.split('=', 1)[0].strip())
return sorted(key for key in keys if key)
def main() -> int:
parser = argparse.ArgumentParser(
description='Deny-list preflight for a remote session'
)
parser.add_argument('--root', type=Path, required=True)
parser.add_argument('--env-file', type=Path)
parser.add_argument('--include-process-env', action='store_true')
args = parser.parse_args()
root = args.root.resolve()
flagged = 0
for path in root.rglob('*'):
if not path.is_file():
continue
if any(part in SKIP_DIRS for part in path.parts):
continue
kind = classify_path(path)
rel = path.relative_to(root)
if kind != 'allow':
print(f'{kind} path {rel}')
flagged += 1
continue
for rule in sniff_text(path):
print(f'review content {rel} {rule}')
flagged += 1
for key in iter_env_keys(args.env_file, args.include_process_env):
kind = classify_env_key(key)
if kind == 'allow':
continue
print(f'{kind} env {key}')
flagged += 1
print(f'flagged={flagged}')
return 1 if flagged else 0
if __name__ == '__main__':
sys.exit(main())
deny path .env
deny env API_TOKEN
flagged=2
The walk includes ignored files, because a gitignore rule hides a file from commits and not from an agent. The sample above matches the first command only, and you should see two deny lines plus a flagged count of two. On a machine with a hot shell, the second command adds deny lines that you must clear or isolate. You isolate that shell before the session, rather than editing the script until the exit code looks friendly.
Read the hits before you trust them
A deny hit means the courier must not leave with that object, even when your question feels narrow. A review hit means you open the file or the key name, then you either redact it or leave it behind. An allow hit means the name looks ordinary, not that the bytes inside are safe to share. You still open the diff, because a file called notes.md can hold an email that no suffix rule will catch.
You can think of the script as a coat check at the door, rather than as a judge of the conversation. The coat check stops the obvious bags, and it will miss a story that uses no secret-shaped string. After the script passes, you still read the prompt and ask whether a stranger should know that story. If the answer feels uneasy, you rewrite the question with synthetic names and a trimmed stack.
Commands belong in that same pause, because a tool transcript can smuggle the second room onto the porch. Before the agent runs a collector, you decide whether the output may be echoed back to the host. A quiet git status on a scrubbed toy repo is usually a smaller risk than a full environment dump. You allow a command by writing it down, and you forbid a command by keeping it out of the tool list.
Use a free host only for the dry run
Once the manifest is clean, a remote model can help you practice the workflow on fixtures you invented. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option, which can host a dry run on synthetic code. Those availability claims are not a quota, a hardware bill, a region, or a promise that the offer remains.
You read the current product notes before you rely on either claim, because free terms can change without drama. You use the dry run to rehearse the boundary, not to process a real incident from yesterday. You might ask the model to explain a toy function, to suggest a test, or to review a scrubbed diff. You do not ask it to summarize a production log, even when that log looks mostly redacted to you.
A mostly redacted log is still a log, and the missing mask is where names tend to hide. If the session starts requesting uploads you did not plan, you stop and return to the manifest. People who already hold production secrets, payment data, or health records should not treat this script as their control. A local checker cannot replace a data-loss tool, a vendor review, or a decision from your security team.
Teams that cannot name the operator, the retention statement, and the readers of server logs should wait. If your written policy forbids third-party inference, a free price does not create a quiet exception. Unreleased customer exports belong in that same wait, because a tutorial script is not a boundary control.
Say the limits out loud
Path rules miss secrets that live inside ordinary source, and env rules miss values sitting in shell history. The checker does not know what an earlier session already stored on a host you opened last week. It does not watch the network, so a different client can still bypass it when you leave that client open. You should treat a green exit as permission to keep reading, not as permission to stop thinking.
Free access can also train a bad habit, because a low price makes a second paste feel cheap. You counter that habit by keeping the deny manifest beside the repository you intend to open. You also refuse to start a session when the exit code is dirty, even if the question feels urgent. You rotate any real credential that already touched a prompt, because closing a tab does not rewind a copy.
The report can be sensitive too, since a relative path may contain a customer name or a ticket identifier. You keep that report on your machine, and you do not paste it into the same session you are gating. A useful analogy is a boarding pass: the pass lists the gate, but you do not hand over the house keys with it. Carry the question, leave the evidence, and let the foreign room answer only what the toy fixture supports.
If you want one practice pass, build the toy tree, run the checker, and only then open a remote session. A free server is a reasonable rehearsal space when the bag is synthetic and the policy still matches your notes. Leave the key ring on the hook inside the house, where the courier cannot casually lift it. That method stays useful even if you later choose a different host for the same dry run.
Top comments (0)