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.
The scene you ignored
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.
Mistake 1: no hard stop on a free loop
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.
Mistake 2: the dirty tree traveled with you
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.
Mistake 3: no closing receipt
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.
Where a free model and free server fit
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.
The workflow (do this in order)
Follow these steps before the first prompt.
Do not reorder them under time pressure.
- Create a clean worktree from a known git SHA.
- Write an open receipt with budget, SHA, and allowlist.
- Copy only allowlisted paths to the remote workspace.
- Run the agent against that workspace only.
- Stop when
max_stepsormax_secondshits zero. - Hash the resulting diff and close the receipt.
- Only then inspect the patch on your laptop.
If any step fails, delete the workspace.
Do not just retry inside the dirty tree.
Artifact: a local receipt runner
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.
#!/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 <init|tick|close>");
process.exit(2);
}
Commands
Use a detached worktree for every agent session.
Do not run this script in a dirty repo.
git fetch --quiet
SHA=$(git rev-parse HEAD)
git worktree add --detach ../agent-scratch "$SHA"
cd ../agent-scratch
export MAX_STEPS=8
export MAX_SECONDS=900
export ALLOW=src,tests
export RUNNER_HOST=free-server.example
export RUNNER_KIND=free-remote
export MODEL_ENDPOINT=unspecified
node ../main/receipt.mjs init
# Each agent tool-call wrapper should run:
node ../main/receipt.mjs tick
# After the loop writes agent.patch:
export DIFF_PATH=agent.patch
node ../main/receipt.mjs close
Wrap every remote tool invocation with a tick call.
If tick exits non-zero, kill the loop.
Why these three numbers exist
MAX_STEPS=8 is a local policy, not a vendor quota.
It forces a split when the task is still vague.
Raise it only after the allowlist shrinks.
MAX_SECONDS=900 bounds retries you will not watch.
Network blips should not become an overnight process.
Pair it with a shell timeout around SSH.
The 200k diff cap blocks silent tree dumps.
A useful patch names files and stays reviewable.
If the artifact is larger, the task was too wide.
Decision table
| Condition | Local laptop | Isolated free server |
|---|---|---|
| Secrets in tree | Stop. Redact first. | Do not copy. |
| Dirty git status | Isolate or revert. | Fresh worktree only. |
| Exploratory refactor | Optional. | Preferred with a budget. |
| Prod deploy | Human-owned. | Never. |
| Unknown model endpoint | Do not send code. | Do not send code. |
| Need replay on Monday | Receipt required. | Receipt required. |
Read the decision table before you prompt.
If two rows conflict, choose the stricter one.
What the receipt must contain
Keep the receipt document small, complete, and boring.
These fields are the minimum useful set.
-
git_shaof the isolated tree. -
allowlistof paths copied to the runner. -
budget.max_stepsandbudget.max_seconds. -
runner.hostandrunner.kind. -
model.endpointwithout secrets. -
artifact.sha256of the unified diff.
Do not store prompts that still contain credentials.
Do not store raw env values for later debug.
Name the endpoint as a hostname only.
If you cannot name it, do not open the receipt.
Limitations
This receipt is a local file, not a signed audit log.
Anyone with disk access can edit it after the fact.
A free server is not a tenancy boundary.
It is not a SOC2 control or an availability SLA.
The example script does not apply any patches.
It does not sandbox syscalls or prove runner honesty.
The budget counters remain local to the process.
A crashed wrapper can skip the tick call.
Pair this with a wall-clock timeout on the SSH session.
timeout 15m ssh runner 'cd /work && ./run-agent.sh'
If you need cryptographic provenance, add detached signatures.
This article does not implement that layer.
Who should not use this approach
Skip this workflow in the following cases.
These constraints are hard limits, not style notes.
Do not send regulated data to a free remote model.
Do not copy customer exports onto a free server.
Do not run this as your production CI.
Security teams that need attestation want a different stack.
Students on a shared laptop should still isolate paths.
If you cannot name the endpoint, do not start the loop.
Closing the letter
Friday-me, the model was not the outage.
The missing stop rule was the outage.
The dirty tree and missing receipt were the outage.
Bound the loop, isolate the tree, and stamp the receipt.
Then you can spend Monday on the patch, not the archaeology.
MonkeyCode is one place to try a free server for this drill.
Top comments (0)