DEV Community

Sam Chen
Sam Chen

Posted on

The Scratch Server Went Green. You Still Need a Local Gate.

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
Enter fullscreen mode Exit fullscreen mode

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:]))
Enter fullscreen mode Exit fullscreen mode
# recipe: scan only staged files
git diff --name-only --cached -z | xargs -0 -r python secret_scan.py
Enter fullscreen mode Exit fullscreen mode

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/**"
Enter fullscreen mode Exit fullscreen mode
# 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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. Draft on the scratch server with fake fixtures only.
  2. Export a patch file and a small origin file.
  3. Scan for secrets, then enforce the deny globs.
  4. Apply the patch on a clean local worktree.
  5. 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)