The Scratch Server Went Green. You Still Need a Local Gate. A developer published a set of local CI gate recipes and policy files designed to stop teams from trusting free agent sandboxes as merge queues. The writeup argues that green output from an untrusted scratch server is not a signed provenance record, and that agent patches must be replayed on a controlled runner with local tests, secret scanning, and an executable deny-list policy before merging. It includes bash, Python, and YAML examples for enforcing those checks. 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. 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. 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. 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. Replay every agent patch on your own runner. Fail the merge when local tests never executed. Keep the remote box for drafts, not releases. bash /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. 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. 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. 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. The system prompt says never touch production files. The agent still edits the deploy workflow anyway. Nobody encoded the rule as a failing check. 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. 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/ " python 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. bash /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. 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 | 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. Skip step one if your code cannot leave the building. Skip none of the later steps if it can. 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. 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. 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.