Letter to Friday-Me: Budget, Isolate, Then Stamp the Receipt A developer published a reconstructed failure log describing how an uncapped coding agent loop on a free remote server consumed an entire working day, with no receipt recording the runner, files, or stop condition. The writeup argues that free model access and free server time are not infinite budgets, and proposes a local control plane enforcing three gates — budget, isolation, and a session receipt — before any prompt is sent. The workflow is presented as product-agnostic, runnable on any SSH box and model endpoint, with sample Node.js code that hashes state and refuses to proceed on a dirty working tree. The following letter is a reconstructed failure log. It is not a claim about any employer. Friday at 16:40, the coding agent was still looping. No receipt named the runner, files, or stop condition. Friday-me, You treated a free remote loop as unbounded compute. That single assumption consumed an entire working day. Three process mistakes did the real damage here. None of them were about model quality. A refactor request looked small at noon. You pointed an agent at a dirty working tree. You let it retry on a remote box with no cap. By late afternoon the tree was noisy. Two patches conflicted, and one secret file entered context. You could not reconstruct the successful step. This letter is the workflow you lacked. It is a local control plane for free remote loops. Free model access does not mean an infinite loop. Free server time does not mean an infinite retry budget. Without a cap, failure becomes a quiet background process. You watched tokens burn through duplicate tool calls. The same failing test command ran without a recorded cap. No file recorded the step limit or the continue reason. A loop without a stop rule is not an agent. It is a cron job you forgot to kill. The remote workspace inherited your entire laptop state. node modules , .env , and a half-applied patch came along. The model then fixed symptoms that were only local dirt. Context windows then filled with unrelated local diffs. The runner executed scripts against stale local dependencies. You debugged the model instead of the dirty tree. Isolation is not a style choice here. It is the difference between a session and a mess. When the loop died, evidence died with it. You had chat scrollback and a dirty git status. You did not have a hash, a file list, or a budget remainder. Monday-you cannot replay what Friday-you actually ran. Support cannot tell which runner produced the patch. You spend the next day reconstructing a session that never closed. A session receipt is not extra process theater. It is the smallest artifact that makes the day replayable. Keep exploratory loops off the laptop when the tree is messy. A free remote server plus free model access can host that drill. MonkeyCode currently offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The workflow below does not depend on that product. Any SSH box and any model endpoint can run the same receipt. Follow these steps before the first prompt. Do not reorder them under time pressure. max steps or max seconds hits zero. If any step fails, delete the workspace. Do not just retry inside the dirty tree. The script below is an example control plane. Treat it as unexecuted sample code, not a benchmark. It enforces three gates: budget, isolation, and receipt. It never sends secrets and it never applies a patch. bash /usr/bin/env node // Example only. Review before use. Not a hosted service client. import { createHash } from "node:crypto"; import { execFileSync } from "node:child process"; import { existsSync, readFileSync, writeFileSync, } from "node:fs"; import path from "node:path"; const ROOT = process.cwd ; const RECEIPT = path.join ROOT, ".session-receipt.json" ; const FORBIDDEN = new Set ".env", ".env.local", "id rsa", "id ed25519" ; function git args { return execFileSync "git", args, { encoding: "utf8" } .trim ; } function sha256 text { return createHash "sha256" .update text .digest "hex" ; } function load { if existsSync RECEIPT { throw new Error "no open receipt; run init first" ; } return JSON.parse readFileSync RECEIPT, "utf8" ; } function save doc { writeFileSync RECEIPT, JSON.stringify doc, null, 2 + "\n" ; } function assertCleanAllowlist allow { const dirty = git "status", "--porcelain" ; if dirty { throw new Error "worktree is dirty; isolate first" ; } for const rel of allow { const base = path.basename rel ; if FORBIDDEN.has base { throw new Error forbidden path in allowlist: ${rel} ; } } } function init { const maxSteps = Number process.env.MAX STEPS || 8 ; const maxSeconds = Number process.env.MAX SECONDS || 900 ; const allow = process.env.ALLOW || "src,tests" .split "," .map s = s.trim .filter Boolean ; if maxSteps < 1 || maxSteps 32 { throw new Error "MAX STEPS must be between 1 and 32" ; } assertCleanAllowlist allow ; const doc = { status: "open", opened at: new Date .toISOString , git sha: git "rev-parse", "HEAD" , branch: git "rev-parse", "--abbrev-ref", "HEAD" , allowlist: allow, budget: { max steps: maxSteps, max seconds: maxSeconds, steps used: 0, }, runner: { host: process.env.RUNNER HOST || "unspecified", kind: process.env.RUNNER KIND || "free-remote", }, model: { endpoint: process.env.MODEL ENDPOINT || "unspecified", }, artifact: null, }; if existsSync RECEIPT { throw new Error "receipt already exists; close or delete it" ; } save doc ; console.log "receipt opened:", RECEIPT ; } function tick { const doc = load ; if doc.status == "open" { throw new Error "receipt is not open" ; } const started = Date.parse doc.opened at ; const elapsed = Date.now - started / 1000; doc.budget.steps used += 1; if doc.budget.steps used doc.budget.max steps { throw new Error "step budget exhausted" ; } if elapsed doc.budget.max seconds { throw new Error "time budget exhausted" ; } save doc ; console.log step ${doc.budget.steps used}/${doc.budget.max steps}; ${Math.floor elapsed }s elapsed , ; } function close { const doc = load ; const diffPath = process.env.DIFF PATH || "agent.patch"; if existsSync diffPath { throw new Error missing diff: ${diffPath} ; } const diff = readFileSync diffPath, "utf8" ; if diff.startsWith "diff --git " && diff.startsWith "--- " { throw new Error "artifact is not a unified diff" ; } if diff.length 200 000 { throw new Error "diff exceeds 200k characters; split the task" ; } doc.status = "closed"; doc.closed at = new Date .toISOString ; doc.artifact = { path: diffPath, sha256: sha256 diff , bytes: Buffer.byteLength diff , }; save doc ; console.log "receipt closed:", doc.artifact.sha256 ; } const cmd = process.argv 2 ; if cmd === "init" init ; else if cmd === "tick" tick ; else if cmd === "close" close ; else { console.error "usage: receipt.mjs