{"slug": "the-scratch-server-went-green-you-still-need-a-local-gate", "title": "The Scratch Server Went Green. You Still Need a Local Gate.", "summary": "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.", "body_md": "A free agent box is not your merge queue.\n\nYou still need a local, boring, repeatable gate.\n\nWould you ship code from a laptop you do not own?\n\nThat is the whole article, stated up front.\n\nThe rest is a catalog of ways teams skip it.\n\nI wrote the checks as commands you can run.\n\nCheap inference changed the cost of a first draft.\n\nIt did not change who owns the blast radius.\n\nDid your policy file follow the model, or stay in chat?\n\nI keep six failures on a short review list.\n\nEach failure looks like speed on a busy afternoon.\n\nEach failure still ships as untracked engineering debt.\n\nThe scratch server printed a passing test line.\n\nThe pull request skips CI because the agent ran tests.\n\nMain now trusts a machine you do not control.\n\nPeople confuse a demo host with a trusted runner.\n\nA free server is a sandbox, not an attesting builder.\n\nGreen output is not a signed provenance record.\n\nReplay every agent patch on your own runner.\n\nFail the merge when local tests never executed.\n\nKeep the remote box for drafts, not releases.\n\n``` bash\n#!/usr/bin/env bash\n# local_gate.sh — labeled recipe, not a CI platform.\nset -euo pipefail\n\nif [[ ! -f .agent/origin.json ]]; then\n  echo \"missing .agent/origin.json\" >&2\n  exit 1\nfi\n\nreplayed=\"$(jq -r '.replayed_locally' .agent/origin.json)\"\nif [[ \"${replayed}\" != \"true\" ]]; then\n  echo \"patch was not replayed locally\" >&2\n  exit 1\nfi\n\nif [[ \"${CI:-}\" != \"true\" ]]; then\n  echo \"refusing to bless a merge outside CI\" >&2\n  exit 1\nfi\n\ngit diff --check\npytest -q\n```\n\nDid those agent tests even use your lockfile?\n\nIf you cannot answer, you cannot merge yet.\n\nSomeone pasted a dotenv file into the agent thread.\n\nThe free server now holds a production token.\n\nRotation starts only after the screenshot already leaked.\n\nConvenience still beats threat modeling under time pressure.\n\nA shared scratch box is not your secret store.\n\nFree compute does not include any free confidentiality.\n\nRedact first, then inject secrets only in local CI.\n\nI keep a dumb scanner in the pre-push path.\n\nIt looks ugly, and it is enough for drafts.\n\n```\n# secret_scan.py — labeled example, not a security product.\nfrom __future__ import annotations\n\nimport re\nimport sys\nfrom pathlib import Path\n\nPATTERNS = [\n    re.compile(r\"AKIA[0-9A-Z]{16}\"),\n    re.compile(r\"-----BEGIN (RSA |OPENSSH )?PRIVATE KEY-----\"),\n    re.compile(r\"(?i)(api[_-]?key|secret)\\s*=\\s*\\S+\"),\n]\n\ndef main(paths: list[str]) -> int:\n    failed = False\n    for raw in paths:\n        path = Path(raw)\n        if not path.is_file():\n            continue\n        text = path.read_text(errors=\"ignore\")\n        for pat in PATTERNS:\n            if pat.search(text):\n                print(f\"secret-like match in {path}\")\n                failed = True\n    return 1 if failed else 0\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1:]))\n# recipe: scan only staged files\ngit diff --name-only --cached -z | xargs -0 -r python secret_scan.py\n```\n\nWould you paste that token into a hallway laptop?\n\nThen do not paste it into a free agent server.\n\nThe system prompt says never touch production files.\n\nThe agent still edits the deploy workflow anyway.\n\nNobody encoded the rule as a failing check.\n\nNatural language feels like a real control plane.\n\nModels drift over sessions, while files stay put.\n\nA policy that cannot fail CI is a wish.\n\nPut deny paths in an executable policy file.\n\nI like a tiny allow and deny list in-repo.\n\nThe agent may read it, but CI must enforce it.\n\n```\n# .agent/policy.yml — proposal format\ndeny_globs:\n  - \".github/workflows/**\"\n  - \"infra/prod/**\"\n  - \"**/*.env\"\n  - \"**/credentials.json\"\nallow_globs:\n  - \"src/**\"\n  - \"tests/**\"\n  - \"docs/**\"\npython\n# policy_check.py — unexecuted example\nfrom fnmatch import fnmatch\nfrom pathlib import Path\nimport subprocess\nimport sys\nimport yaml\n\npolicy = yaml.safe_load(Path(\".agent/policy.yml\").read_text())\ndiff = subprocess.check_output(\n    [\"git\", \"diff\", \"--name-only\", \"origin/main...HEAD\"],\n    text=True,\n).splitlines()\n\nblocked = False\nfor path in diff:\n    if any(fnmatch(path, glob) for glob in policy[\"deny_globs\"]):\n        print(f\"denied path: {path}\")\n        blocked = True\n\nsys.exit(1 if blocked else 0)\n```\n\nIf the check is optional, the anti-pattern remains.\n\nMake it blocking, then argue exceptions in the PR.\n\nOne long agent thread spans three private repositories.\n\nContext from repo A leaks into a patch for B.\n\nThe summary sounds confident, but the imports are wrong.\n\nSessions are cheap, and isolation is not the default.\n\nA free server often outlives your actual attention.\n\nYesterday's stack trace becomes today's invented client API.\n\nUse one repo, one worktree, and one short session.\n\nDestroy the thread when the branch finally has a name.\n\nRecord the origin file before you close the tab.\n\n```\n{\n  \"generated_at\": \"2026-09-23T12:00:00Z\",\n  \"repo\": \"payments-api\",\n  \"base_sha\": \"REPLACE_WITH_LOCAL_SHA\",\n  \"session\": \"scratch-only\",\n  \"replayed_locally\": false\n}\n```\n\nSet replayed_locally true only after local CI passes.\n\nAnything else is still a draft, not a candidate.\n\nSomeone says the model needs a realistic payload.\n\nA customer export then lands on the scratch server.\n\nThe model is free, but that dataset is not.\n\nRealism gets confused with actual processing permission here.\n\nFree inference still does not grant data-processing rights.\n\nA redacted fixture is slower, and it is legal.\n\nBuild a tiny fixture set inside the repository.\n\nI would rather ship boring JSON than a real dump.\n\nName the file fake, and then keep it fake.\n\n```\n{\n  \"order_id\": \"ord_test_001\",\n  \"amount_cents\": 1999,\n  \"region\": \"lab\",\n  \"email\": \"user@example.test\"\n}\n# recipe: refuse prod-shaped filenames in the diff\nif git diff --name-only | grep -E '(customers|pii|prod-dump)'; then\n  echo \"prod-shaped filename in the diff\" >&2\n  exit 1\nfi\n```\n\nDo you have a deletion ticket for that upload?\n\nIf not, you already lost the data conversation.\n\nThe patch arrived as a blob from the chat.\n\nNobody can regenerate it from a recorded prompt.\n\nReviewers argue with a screenshot, not a command.\n\nGeneration starts to feel like real authorship too quickly.\n\nAuthorship without a replay path is just folklore.\n\nFolklore does not bisect when production later breaks.\n\nExport a patch file, then apply it locally.\n\nRun the same tests your CI will run tomorrow.\n\nIf apply fails, the remote box lied about the tree.\n\n``` bash\n#!/usr/bin/env bash\n# replay.sh — labeled recipe\nset -euo pipefail\n\nbase=\"$(jq -r '.base_sha' .agent/origin.json)\"\ngit checkout -B \"replay/${USER}\" \"${base}\"\ngit apply --check /tmp/agent.patch\ngit apply /tmp/agent.patch\n\ntmp=\"$(mktemp)\"\njq '.replayed_locally = true' .agent/origin.json > \"${tmp}\"\nmv \"${tmp}\" .agent/origin.json\n\npytest -q\n```\n\nCan a stranger reproduce this without the original chat?\n\nIf they cannot, you do not have a change.\n\nYou only have a vibe from a closed tab.\n\nI print this table near the merge button.\n\nI still miss a row when the review is rushed.\n\nThe table is the review, not the model output.\n\n| Signal | Merge | Why | \n|---|---|---|\n| Remote tests only | No | Host is not your runner | \n| Secrets in the thread | No | Rotate first, then rewrite | \n| Policy only in the prompt | No | Encode a failing check | \n| Mixed-repo session | No | Split and regenerate | \n| Real customer payload | No | Replace with fixtures | \n| Local apply and CI green | Yes | Now it is your patch | \n\nI still want a scratch pad for ugly first drafts.\n\nThrowaway refactors do not deserve a paid cluster.\n\nThey also do not deserve a silent path to main.\n\nMonkeyCode offers free model access and a free server option.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.\n\nI treat that option as a disposable editor, nothing more.\n\nThe workflow stays boring on purpose for a reason.\n\nDrafts can be remote without becoming merge artifacts.\n\nMerges cannot be remote without a local replay.\n\nSkip step one if your code cannot leave the building.\n\nSkip none of the later steps if it can.\n\nThis catalog will not make a model honest.\n\nIt only makes a dishonest merge much harder.\n\nThat is the actual job of these gates.\n\nDo not use a free scratch server in these cases.\n\nI also will not claim tokens, uptime, or model names.\n\nThose numbers go stale by the next product page.\n\nTrust the commands you can rerun after lunch.\n\nClone the repo, apply the patch, scan, then test.\n\nIf a step fails, the agent did not finish.\n\nThe chat window is not a merge witness.\n\nWant a disposable box for that first ugly draft?\n\nUse a free server, then run the local gate at home.", "url": "https://wpnews.pro/news/the-scratch-server-went-green-you-still-need-a-local-gate", "canonical_source": "https://dev.to/codex_1135/the-scratch-server-went-green-you-still-need-a-local-gate-481g", "published_at": "2026-09-23 16:45:32+00:00", "updated_at": "2026-09-23 16:59:02.934334+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "mlops", "ai-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/the-scratch-server-went-green-you-still-need-a-local-gate", "markdown": "https://wpnews.pro/news/the-scratch-server-went-green-you-still-need-a-local-gate.md", "text": "https://wpnews.pro/news/the-scratch-server-went-green-you-still-need-a-local-gate.txt", "jsonld": "https://wpnews.pro/news/the-scratch-server-went-green-you-still-need-a-local-gate.jsonld"}}