A green agent run on free compute is not a merge decision. It is a draft produced on an untrusted host. You promote the change only after isolation, provenance, and a trusted re-run all pass. If any of those are missing, you fail closed.
Agents assume. Free sandboxes hide that. Your job is to make promotion boring, labeled, and refusible.
The merge you should refuse
You let an agent edit a service on a laptop, a shared VM, or a complimentary cloud box. Tests pass there. The PR looks complete. Then you merge from that host’s word.
That is the bug. The sandbox is not your CI. It is not your production identity provider. It is not allowed to mint a release.
Treat every agent workspace as hostile until a trusted pipeline repeats the same git SHA with your real secrets policy, your real network policy, and your real artifact store.
What “untrusted host” actually means
Untrusted does not mean “malware.” It means you cannot prove four things:
- Who ran it. A free box may be shared, preempted, or reused.
-
What it could see. Prompt context,
.envfiles, and cloud metadata are easy to leak. - Where it could send bytes. Egress is the silent failure.
- Whether you can replay it. If you cannot re-run the SHA, you cannot defend the merge.
You do not argue with the model. You refuse the promotion path.
Copy this promotion checklist
Use it as a gate file in the repo. Empty fields fail the job. “Not applicable” is not a field. Delete a gate only when the change cannot touch prod, data, or credentials.
Gate 0 — Label the environment
Every agent job exports an environment class. No label, no artifacts.
# proposal: required env for any agent runner
AGENT_ENV_CLASS=sandbox # sandbox | trusted_ci | prod
AGENT_RUN_ID=$(uuidgen)
AGENT_GIT_SHA=$(git rev-parse HEAD)
Fail closed if AGENT_ENV_CLASS=prod on a developer laptop, a free VM, or any host missing your CI OIDC identity. Sandbox may propose. Only trusted_ci may attest.
Gate 1 — Data and secrets stay out of the sandbox
You do not load production dumps “just this once.” You do not mount the org’s cloud keys so the agent can “debug faster.”
Fail closed when any of these are true:
- Production connection strings are present in the sandbox process environment.
- The workspace can read
~/.aws,~/.kube, or CI OIDC tokens. - Logs may contain live customer payloads.
- The prompt pack includes real tickets with PII.
Replacement rule: synthetic fixtures, scrubbed snapshots, or a dedicated sandbox account with no prod IAM trust.
Gate 2 — Network and writes are allowlisted
If the agent can hit the internet, it can exfiltrate the repo. If it can write anywhere, it can persist a hook you will not review.
Minimum allowlist:
| Direction | Allow | Deny by default |
|---|---|---|
| Egress | Package registry, your git host, your artifact store | Everything else |
| Writes | Repo worktree, job temp dir |
$HOME, Docker socket, extra volumes |
| Reads | Declared fixture paths | Secret stores, sibling repos |
A missing allowlist is a deny. Do not “open it for now.”
Gate 3 — Trusted re-run is mandatory
Sandbox green is a hint. Merge green is a second execution.
You pin the SHA. You rebuild. You re-run tests and scanners on trusted_ci. You compare artifact digests. If the sandbox cannot produce a digest, it cannot propose a merge.
# proposal: promotion.json (checked into the PR, not a screenshot)
{
"git_sha": "REPLACE_WITH_FULL_SHA",
"sandbox_run_id": "",
"sandbox_artifact_sha256": "",
"trusted_ci_run_id": "",
"trusted_ci_artifact_sha256": "",
"env_class_attested": "trusted_ci",
"feature_flag": "agent-change-TICKET",
"owner": "oncall-handle",
"rollback": "gh pr revert --yes"
}
Fail closed if sandbox_artifact_sha256 equals trusted_ci_artifact_sha256 but env_class_attested is still sandbox. Same bytes from the wrong host are not an attestation.
Gate 4 — Blast radius is numeric
“Small PR” is not a control. You cap what the change may touch.
- Max paths changed (example: 20 files, 2 services).
- Max IAM actions added (example: zero in sandbox-origin PRs).
- Max regions or clusters (example: one staging namespace).
- Feature flag default off.
- No schema drop, no authz change, no secret rotation from an agent sandbox.
If the diff exceeds the cap, the promotion job fails. A human splits the work. The agent does not get a bigger blast radius because the model was confident.
Gate 5 — Owner and rollback exist before merge
No owner, no merge. No rollback command that has been typed once in staging, no merge.
You need a named human, a chat channel, and a revert that does not depend on the agent being available. If rollback is “ask the model to undo it,” you do not have rollback.
A fail-closed workflow you can paste
Label the following as a proposal. Wire it to your CI. Do not run it as a clever local alias that can be skipped with --no-verify.
# proposal: .github/workflows/promotion-gate.yml
name: promotion-gate
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
refuse-sandbox-attestation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail closed on missing promotion evidence
env:
AGENT_ENV_CLASS: ${{ vars.AGENT_ENV_CLASS }}
run: bash scripts/promotion_gate.sh
# proposal: scripts/promotion_gate.sh
set -euo pipefail
FILE="promotion.json"
if [[ ! -f "$FILE" ]]; then
echo "fail-closed: $FILE missing"
exit 1
fi
python3 - <<'PY'
import json, sys
required = [
"git_sha", "sandbox_run_id", "sandbox_artifact_sha256",
"trusted_ci_run_id", "trusted_ci_artifact_sha256",
"env_class_attested", "feature_flag", "owner", "rollback"
]
data = json.load(open("promotion.json"))
missing = [k for k in required if not str(data.get(k, "")).strip()]
if missing:
print("fail-closed: empty fields:", ", ".join(missing))
sys.exit(1)
if data["env_class_attested"] != "trusted_ci":
print("fail-closed: sandbox cannot attest a merge")
sys.exit(1)
if data["sandbox_artifact_sha256"] == data["trusted_ci_artifact_sha256"] and not data["trusted_ci_run_id"]:
print("fail-closed: digest copied without a trusted run id")
sys.exit(1)
print("promotion evidence present; human review still required")
PY
Keep the script stupid. Clever exceptions become merge holes.
Decision table: sandbox result vs merge action
| Sandbox result | Trusted CI result | Secrets/egress gates | Action |
|---|---|---|---|
| Pass | Pass, same SHA | Pass | Human review, flag off |
| Pass | Fail | Pass | Do not merge; sandbox was a false green |
| Fail | Pass | Pass | Do not auto-merge; explain the delta |
| Pass | Not run | Anything | Fail closed |
| Pass | Pass | Missing allowlist | Fail closed |
Pass on prod label from a laptop |
Anything | Anything | Fail closed, rotate credentials |
The interesting row is sandbox pass / trusted fail. That is the whole reason this checklist exists. You will see it. You should want to see it.
Where a free sandbox still helps
You still want a cheap place to let an agent thrash. Draft diffs. Throwaway fixtures. Prompt iteration. That host should be labeled sandbox and stripped of prod trust.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. If you need a place to iterate without burning paid CI minutes, MonkeyCode’s free model access and free server option can sit on the sandbox side of this split. They do not replace trusted_ci. They must not write env_class_attested.
Use whatever sandbox you already have. The gate is the product. The host is disposable.
Limitations
This checklist does not prove the change is correct. It proves you refused a class of unsafe promotions.
It will not catch a wrong business rule that still compiles. It will not catch a model that omitted a test you never wrote. It will not survive a team that grants themselves continue-on-error: true.
Digests can match and still be wrong if both runs used the same poisoned fixture. Environment labels can be spoofed if your CI does not mint them. Feature flags do not help if the flag wraps only the happy path.
Free sandboxes also disappear, throttle, or change under you. That is another reason they cannot be the system of record. Pin the SHA on infrastructure you operate.
Who should not use this
Skip the full gate if you are:
- Working in a throwaway repo with no secrets, no customers, and no deploy.
- Pairing on a spike that will be deleted before it touches
main. - Already running agents only inside a locked-down
trusted_ciimage with no sandbox hop.
Do not skip it if the agent can open a PR, talk to a package registry, or see a staging cluster. That is already production-adjacent.
What you do Monday
Pick one service. Add promotion.json as required evidence. Add the script. Break a PR on purpose by leaving trusted_ci_run_id empty. Confirm the job fails.
Then break it again by labeling a laptop run as trusted_ci. If that merge is possible, your identity story is the incident, not the model.
Sandbox green is allowed. Sandbox authority is not. Fail closed, then promote.
Top comments (0)