A free agent box is not your merge queue.
You still need a local, boring, repeatable gate.
Would you ship code from a laptop you do not own?
That is the whole article, stated up front.
The rest is a catalog of ways teams skip it.
I wrote the checks as commands you can run.
Why this keeps breaking
Cheap inference changed the cost of a first draft.
It did not change who owns the blast radius.
Did your policy file follow the model, or stay in chat?
I keep six failures on a short review list.
Each failure looks like speed on a busy afternoon.
Each failure still ships as untracked engineering debt.
Anti-pattern 1: Remote green, local skip
Symptom
The scratch server printed a passing test line.
The pull request skips CI because the agent ran tests.
Main now trusts a machine you do not control.
Root cause
People confuse a demo host with a trusted runner.
A free server is a sandbox, not an attesting builder.
Green output is not a signed provenance record.
Replacement
Replay every agent patch on your own runner.
Fail the merge when local tests never executed.
Keep the remote box for drafts, not releases.
#!/usr/bin/env bash
# local_gate.sh — labeled recipe, not a CI platform.
set -euo pipefail
if [[ ! -f .agent/origin.json ]]; then
echo "missing .agent/origin.json" >&2
exit 1
fi
replayed="$(jq -r '.replayed_locally' .agent/origin.json)"
if [[ "${replayed}" != "true" ]]; then
echo "patch was not replayed locally" >&2
exit 1
fi
if [[ "${CI:-}" != "true" ]]; then
echo "refusing to bless a merge outside CI" >&2
exit 1
fi
git diff --check
pytest -q
Did those agent tests even use your lockfile?
If you cannot answer, you cannot merge yet.
Anti-pattern 2: Secrets on a scratch host
Symptom
Someone pasted a dotenv file into the agent thread.
The free server now holds a production token.
Rotation starts only after the screenshot already leaked.
Root cause
Convenience still beats threat modeling under time pressure.
A shared scratch box is not your secret store.
Free compute does not include any free confidentiality.
Replacement
Redact first, then inject secrets only in local CI.
I keep a dumb scanner in the pre-push path.
It looks ugly, and it is enough for drafts.
# secret_scan.py — labeled example, not a security product.
from __future__ import annotations
import re
import sys
from pathlib import Path
PATTERNS = [
re.compile(r"AKIA[0-9A-Z]{16}"),
re.compile(r"-----BEGIN (RSA |OPENSSH )?PRIVATE KEY-----"),
re.compile(r"(?i)(api[_-]?key|secret)\s*=\s*\S+"),
]
def main(paths: list[str]) -> int:
failed = False
for raw in paths:
path = Path(raw)
if not path.is_file():
continue
text = path.read_text(errors="ignore")
for pat in PATTERNS:
if pat.search(text):
print(f"secret-like match in {path}")
failed = True
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
# recipe: scan only staged files
git diff --name-only --cached -z | xargs -0 -r python secret_scan.py
Would you paste that token into a hallway laptop?
Then do not paste it into a free agent server.
Anti-pattern 3: Prompt-as-policy
Symptom
The system prompt says never touch production files.
The agent still edits the deploy workflow anyway.
Nobody encoded the rule as a failing check.
Root cause
Natural language feels like a real control plane.
Models drift over sessions, while files stay put.
A policy that cannot fail CI is a wish.
Replacement
Put deny paths in an executable policy file.
I like a tiny allow and deny list in-repo.
The agent may read it, but CI must enforce it.
# .agent/policy.yml — proposal format
deny_globs:
- ".github/workflows/**"
- "infra/prod/**"
- "**/*.env"
- "**/credentials.json"
allow_globs:
- "src/**"
- "tests/**"
- "docs/**"
# policy_check.py — unexecuted example
from fnmatch import fnmatch
from pathlib import Path
import subprocess
import sys
import yaml
policy = yaml.safe_load(Path(".agent/policy.yml").read_text())
diff = subprocess.check_output(
["git", "diff", "--name-only", "origin/main...HEAD"],
text=True,
).splitlines()
blocked = False
for path in diff:
if any(fnmatch(path, glob) for glob in policy["deny_globs"]):
print(f"denied path: {path}")
blocked = True
sys.exit(1 if blocked else 0)
If the check is optional, the anti-pattern remains.
Make it blocking, then argue exceptions in the PR.
Anti-pattern 4: Session soup
Symptom
One long agent thread spans three private repositories.
Context from repo A leaks into a patch for B.
The summary sounds confident, but the imports are wrong.
Root cause
Sessions are cheap, and isolation is not the default.
A free server often outlives your actual attention.
Yesterday's stack trace becomes today's invented client API.
Replacement
Use one repo, one worktree, and one short session.
Destroy the thread when the branch finally has a name.
Record the origin file before you close the tab.
{
"generated_at": "2026-09-23T12:00:00Z",
"repo": "payments-api",
"base_sha": "REPLACE_WITH_LOCAL_SHA",
"session": "scratch-only",
"replayed_locally": false
}
Set replayed_locally true only after local CI passes.
Anything else is still a draft, not a candidate.
Anti-pattern 5: Prod-shaped data on a demo box
Symptom
Someone says the model needs a realistic payload.
A customer export then lands on the scratch server.
The model is free, but that dataset is not.
Root cause
Realism gets confused with actual processing permission here.
Free inference still does not grant data-processing rights.
A redacted fixture is slower, and it is legal.
Replacement
Build a tiny fixture set inside the repository.
I would rather ship boring JSON than a real dump.
Name the file fake, and then keep it fake.
{
"order_id": "ord_test_001",
"amount_cents": 1999,
"region": "lab",
"email": "user@example.test"
}
# recipe: refuse prod-shaped filenames in the diff
if git diff --name-only | grep -E '(customers|pii|prod-dump)'; then
echo "prod-shaped filename in the diff" >&2
exit 1
fi
Do you have a deletion ticket for that upload?
If not, you already lost the data conversation.
Anti-pattern 6: No local replay of the patch
Symptom
The patch arrived as a blob from the chat.
Nobody can regenerate it from a recorded prompt.
Reviewers argue with a screenshot, not a command.
Root cause
Generation starts to feel like real authorship too quickly.
Authorship without a replay path is just folklore.
Folklore does not bisect when production later breaks.
Replacement
Export a patch file, then apply it locally.
Run the same tests your CI will run tomorrow.
If apply fails, the remote box lied about the tree.
#!/usr/bin/env bash
# replay.sh — labeled recipe
set -euo pipefail
base="$(jq -r '.base_sha' .agent/origin.json)"
git checkout -B "replay/${USER}" "${base}"
git apply --check /tmp/agent.patch
git apply /tmp/agent.patch
tmp="$(mktemp)"
jq '.replayed_locally = true' .agent/origin.json > "${tmp}"
mv "${tmp}" .agent/origin.json
pytest -q
Can a stranger reproduce this without the original chat?
If they cannot, you do not have a change.
You only have a vibe from a closed tab.
A decision table I actually use
I print this table near the merge button.
I still miss a row when the review is rushed.
The table is the review, not the model output.
| Signal | Merge | Why |
|---|---|---|
| Remote tests only | No | Host is not your runner |
| Secrets in the thread | No | Rotate first, then rewrite |
| Policy only in the prompt | No | Encode a failing check |
| Mixed-repo session | No | Split and regenerate |
| Real customer payload | No | Replace with fixtures |
| Local apply and CI green | Yes | Now it is your patch |
Where a free model and free server fit
I still want a scratch pad for ugly first drafts.
Throwaway refactors do not deserve a paid cluster.
They also do not deserve a silent path to main.
MonkeyCode offers free model access and a free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I treat that option as a disposable editor, nothing more.
The workflow stays boring on purpose for a reason.
Drafts can be remote without becoming merge artifacts.
Merges cannot be remote without a local replay.
- Draft on the scratch server with fake fixtures only.
- Export a patch file and a small origin file.
- Scan for secrets, then enforce the deny globs.
- Apply the patch on a clean local worktree.
- Run CI, and only then open the pull request.
Skip step one if your code cannot leave the building.
Skip none of the later steps if it can.
Limitations, loudly
This catalog will not make a model honest.
It only makes a dishonest merge much harder.
That is the actual job of these gates.
Do not use a free scratch server in these cases.
- You handle regulated data, even as supposed context.
- Long-lived credentials still sit in the working tree.
- No local runner matches the production environment closely.
- Your license forbids sending source to another host.
- You cannot explain the change without the chat.
I also will not claim tokens, uptime, or model names.
Those numbers go stale by the next product page.
Trust the commands you can rerun after lunch.
What I do tomorrow morning
Clone the repo, apply the patch, scan, then test.
If a step fails, the agent did not finish.
The chat window is not a merge witness.
Want a disposable box for that first ugly draft?
Use a free server, then run the local gate at home.
Top comments (0)