A free model reply is not a release decision. A free server log is not an audit trail either. I still want a local fence before any agent runs.
That fence sits on the branch I plan to merge. I do not trust a scratch host to invent it later. If the stop is missing, the green line is theater.
This is a composite review rule, not a measured outage report. I am not timing a cluster for this post. I am writing the stop I want in git.
Does a green free run mean the patch is safe to merge? It does not, and I want that said first. It only means some host answered a prompt I sent.
Why free access keeps lying to me
Free model access feels like permission to wander. I add another file because the call looks cheap. I retry a vague prompt and call the result research.
Vibe-coded patches are filling review queues this season. The model often sounds sure of the fix. The run record is still empty of stops.
Would I ban a free server for scratch work? No. I would ban unbounded use of that server. A scratch host can help, then it must stop.
The debt is not the model voice. The debt is the missing fence around retries, context, secrets, and model identity. Those four failures stack quietly.
Anti-pattern 1: Retry until something looks green
Symptoms
- The same file shows three overlapping edits.
- Logs repeat one tool name without a new goal.
- A flaky check passes only on the final try.
Root cause
I treated free calls as an endless budget. The price felt like zero, so I never set a max. The loop then chased a green line, not a reviewable diff.
Replacement
Cap attempts in the runner before the first call. Exit when the cap hits. Do not ask the model whether it feels done.
# Proposal only. This cap is local policy, not a benchmark.
MAX_ATTEMPTS = 2
def allow_attempt(n: int) -> None:
if n < 0:
raise SystemExit("attempt fence: bad counter")
if n >= MAX_ATTEMPTS:
raise SystemExit("attempt fence: stop and read the diff")
Two is my personal policy, not a law of nature. You can pick three if your suite is slow. Write the number down before you prompt.
What should the runner do after that exit? Leave the worktree dirty and visible to me. Do not auto-commit, and do not open a pull request.
If I raise the cap mid-run, I have restarted the anti-pattern. The new number belongs in a new commit. Reviewers should see the change of mind.
Anti-pattern 2: Upload the world because the call is free
Symptoms
- Answers cite paths I deleted last week.
- Latency jumps without a real code change.
- Two files in the bundle disagree on one type.
Root cause
I skipped a context budget and called it thorough. Free access is not a design for relevance. More files often add noise, not truth.
Replacement
Build a short manifest, then count bytes on disk. Refuse the send when the local cap breaks. Send a map of files, not the whole world.
# Unexecuted teaching example. Tune the byte cap locally.
find src tests -type f \( -name '*.py' -o -name '*.md' \) \
| sort > /tmp/fence-manifest.txt
wc -c $(cat /tmp/fence-manifest.txt) \
| awk '$1 > 80000 { print "bundle fence"; exit 2 }'
Eighty thousand bytes is a classroom number, not a vendor quota. I want that refusal on my laptop first. The free server should never see a rejected bundle.
Should tests enter the bundle by default? Sometimes yes, when they explain intent. Fixtures with live customer rows should not. I keep a deny file beside the manifest.
# deny.txt — paths that never leave the machine
.env
.env.*
secrets/
fixtures/live/
The deny file is plain text on purpose. I can diff it like any other review. A hidden client toggle would fail that simple test.
Anti-pattern 3: Treat the free session as a vault
Symptoms
- A key shape shows up in a shared transcript.
- A teammate can replay the prompt from logs.
- Rotation starts in chat instead of the secret store.
Root cause
I confused a free server option with a private enclave. Convenience is not custody of my keys. The host may log prompts for its own reasons.
Replacement
Scan the outbound bundle for obvious secret shapes. Block the upload on the first match. Then read the bundle anyway, because scanners miss novel formats.
import re
import sys
PATTERNS = (
r"AKIA[0-9A-Z]{16}",
r"-----BEGIN ",
r"(?i)api[_-]?key\s*=",
r"(?i)secret[_-]?key\s*=",
)
def main() -> None:
blob = open(sys.argv[1], encoding="utf-8", errors="replace").read()
for pattern in PATTERNS:
if re.search(pattern, blob):
raise SystemExit("redaction fence: secret shape in bundle")
if __name__ == "__main__":
main()
Is this a full data-loss program? It is not, and I will not pretend. It catches the paste mistakes I worry about most. New token formats will still slip through.
I also refuse to send shell history to any scratch host. History is full of one-off exports. Those lines look harmless until a key is sitting there.
Who owns the deny file on a team? A named reviewer, not the agent. If nobody owns redaction, do not use this flow at all.
Anti-pattern 4: Swap the model when the free path blips
Symptoms
- Monday notes name one model id.
- Wednesday's patch reads like another writer.
- Style checks fail while behavior checks still pass.
Root cause
I treated free model access as a stable contract. The client was allowed to pick whatever was up. I never stored a pin in the run record.
Replacement
Require a model id in a local policy file. If the server cannot echo that id, stop the job. Do not improvise a cousin model to save the demo.
# run-fence.yml — local policy, not a published quota
model_id: "set-by-operator"
max_attempts: 2
max_bundle_bytes: 80000
fail_closed: true
I will not invent a model name for you in this note. Pin the id your current docs actually list. If that id disappears, fail closed and reread the docs.
How do I check the echo without a stale URL in print? I keep the HTTP path out of this article. A copied endpoint rots faster than the rule. Fill the path from the page you trust today.
def echo_matches(pin: str, echoed: str) -> bool:
if not pin or pin == "set-by-operator":
raise SystemExit("model fence: pin is still a placeholder")
return pin.strip() == echoed.strip()
A placeholder pin must never count as a match. That guard looks petty until review day. A surprise substitute is a different experiment, so stop.
The local fence, step by step
I run this as a preflight, before any scratch host is called. Each step is local. Each failure exits nonzero. None of these steps ask the model for permission.
- Read
run-fence.ymland reject a placeholder model id. - Build the manifest, then subtract every path in
deny.txt. - Fail if bundle bytes exceed
max_bundle_bytes. - Run the secret scan and stop on the first hit.
- Increment the attempt counter and stop at
max_attempts. - Call the scratch host only after those checks pass.
- Compare the echoed model id with the pin, then stop on drift.
- Write a tiny report file and leave the merge decision to me.
# Proposal. I have not executed this as a published bench.
def ready_to_review(report: dict) -> bool:
return (
report["attempts"] <= report["max_attempts"]
and report["secret_scan"] == "pass"
and report["bundle_bytes"] <= report["max_bundle_bytes"]
and report["model_echo"] == report["model_pin"]
)
If ready_to_review is false, I do not open the pull request. I fix the fence or the bundle first. I do not ask the model to talk me out of a red check.
Where does the report live? Next to the diff, in the branch, as a small text file. Chat scrollback is not the record. A future reviewer cannot see my closed tab.
Where a free server is allowed to help
MonkeyCode is one host I can point at this fence. It offers free model access and a free server option for a scratch run.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I am not stating a token quota, a chip type, or a duration. I am not stating that the free option is permanent. I have not published latency numbers here. If the current docs disagree with this paragraph, believe the docs.
What do I send that host? Only a bundle that passed the deny list and the secret scan. What do I require back? An echoed model id that matches my pin. What do I do on a mismatch? I stop, then I pick a pin the docs still list.
# Sketch only. Set FENCE_MODEL from run-fence.yml yourself.
test -n "$FENCE_MODEL" || { echo "missing pin"; exit 2; }
test "$FENCE_MODEL" != "set-by-operator" || exit 2
echo "local fence ready pin=$FENCE_MODEL"
Would I call this path a production deploy? I would not. It is a scratch path with brakes on it. Production still needs your normal CI, review, and secret store.
| Question | I require locally | A free server may do |
|---|---|---|
| Who sets the stop? | The fence file in git | Nothing, until I upload |
| Where do secrets live? | The secret store only | Nowhere in the prompt |
| Which model ran? | The pinned id | Echo that id or fail |
| What does green mean? | Tests I trust already ran | A sample, not a merge vote |
| What if the host blips? | Fail closed | No silent model swap |
Can the free server be my only CI? No, and I do not want that shortcut. Can it be a first pass before CI? Yes, if the fence file is in the same commit. Reviewers should see the policy with the patch.
Who should skip this approach
Skip this if you need a signed availability contract today. Free access is an option, not an SLA I can hand an auditor. I will not dress it up as one.
Skip this if production keys live in the repo and nobody owns redaction. A sample regex is not your compliance boundary. Fix ownership before you automate uploads.
Skip this if you need a fair public benchmark. I gave no hardware notes and no latency table. Anyone inventing those numbers is guessing, including me.
Use it if you maintain a small service and want a cheaper scratch loop. Use it if reviewers already read diffs and you only need a hard stop. The fence is the product of this workflow, not the host.
What I refuse to count as proof
A calm paragraph from the model is not proof. A timestamp on a free server is not proof. A green badge from an unpinned model is not proof either.
Proof, for this rule, is boring and local. Attempts stayed under the cap I wrote down. The secret scan exited clean on the bundle. The echoed model id matched the pin I set.
Then I still read the diff myself. The fence only earns a review, not a merge. If I skip that read, I have rebuilt the original anti-pattern with extra files.
If you want a scratch host for that first pass, start from the current MonkeyCode docs for free model access and the free server option. Pin an id your docs name today. I would rather hear what the fence blocked than hear that the chat felt smart.
Top comments (0)