Did your last free run leave any receipt? I keep hearing four confident claims after cheap runs. They collapse the moment you try a re-run.
Why this FAQ exists
Free models make that first draft feel cheap. A free server makes a first execution cheap. Proof does not get cheaper with either.
Why do we talk like the cost vanished? Nobody sent an invoice for this session. We still mix price with evidence here.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode matters here for two reasons only. It offers free model access for drafts. It also offers a free server option.
I still want a receipt from that box. The product run is not the proof. Your committed files remain the actual proof.
Myth 1: No invoice means no paper trail
"It was free, so I just moved on." Sound familiar in your last standup? That sentence is accounting, not engineering work.
A paper trail is for your next self. The host will not store your intent. Duty does not leave when the price hits zero.
What people repeat
- The session was cheap, so notes would be extra.
- The vendor dashboard will remember the important bits.
- I can rewrite the command from memory later.
What I actually check
- Can a stranger replay this from files only?
- Do I still have the exact command vector?
- Do I still have cwd, exit code, and a hash?
Need a corrected model for tired nights? Price is not provenance at all. A missing invoice does not erase the run.
Ask this before you close the laptop lid. Can another clone replay the run from files? If not, you have a story only.
Myth 2: Resetting the free server is rollback
People call the box disposable without much shame. Then they treat a reset as undo. Those operations are not the same act.
Reset drops machine state in one shot. Rollback restores a known stored artifact. Did you actually store that artifact anywhere?
If you did not store it, stop now. You cannot roll back that work later. You can only hope the next box rhymes.
What people repeat
- Disposable hosts forgive missing artifacts tonight.
- I will just click reset and try again.
- The next box will be close enough.
What I actually check
- Is the output file still in git or object storage?
- Do I have a hash that survives a host wipe?
- Can I install the same command on a cold machine?
Here is the corrected model, said plainly. Disposable hosts demand stored outputs right now. Hope is not a restore path.
Would you ship a binary you cannot refetch? Then do not cite a vanished home directory. Cite a hash you still hold today.
Myth 3: Cheap tokens replace a failing-test list
The model is free, so prose often wins. We retry until the narrative sounds finished. Did a named check fail first though?
A test list is a hard contract. Retries are only a search process. Search can hide flakes for a long time.
What people repeat
- I will know it when the answer looks right.
- Another retry is cheaper than writing tests.
- The model will remember the last failure mode.
What I actually check
- What is the first named check that should fail?
- Did I write that check before generating code?
- Can I run that check without opening a chat?
Corrected model: tokens draft a change. Named checks accept or reject it. A green anecdote is not a suite.
Write failing tests before the model drafts code. Then cheap tokens have a real job. Without that list you are shopping vibes.
Myth 4: The free-box operator signs my release
Someone else hosts the CPU tonight. So we act like they own merges. Do they own your main branch though?
They own a machine, not merge authority. A hosted shell is not a change board. You still sign every real release.
What people repeat
- If their box is green, my branch is green.
- Hosting implies they accepted this risk.
- I am only a guest, so standards are lighter.
What I actually check
- Who can revert this if the hash is wrong?
- Which CI job copies the same command later?
- Which human still owns the merge button?
Corrected model: you remain the release manager. The box remains a rented tool. Tools do not accept your risk.
If that feels heavy, that is healthy. Free compute should feel lighter on money. It should not feel lighter on judgment.
The artifact: RECEIPT.json
I want one file before trusting a free run. Name it RECEIPT.json and keep it boring. Missing fields mean the run is still story.
This is a proposed workflow, not a study. Copy the files, then break them on purpose. Do not treat my gist as measured production data.
Fields I refuse to skip
-
started_at: UTC timestamp from that host -
cwd: absolute working directory after a physicalpwd -
git_head: commit hash or the tokenUNSET -
command: exact argument vector stored as JSON -
exit_code: integer from the wrapped process -
output_file: one declared output path -
output_sha256: hash of that declared output -
host_class:laptop,ci, orfree-server -
model_channel:none,local, orfree-model
Why skip a model name in this schema? Names go stale after one catalog shuffle. A channel label ages a bit slower.
model_channel is self-reported on purpose. The receipt records how you claim the command was born. It cannot audit a chat you never stored.
Proposed wrapper
Save this file as session_receipt.py. Review it before you run anything. This article did not execute the script.
#!/usr/bin/env python3
"""Proposed session receipt wrapper. Unexecuted in this article."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(65536), b""):
digest.update(chunk)
return digest.hexdigest()
def git_head() -> str:
try:
completed = subprocess.run(
["git", "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
)
except (OSError, subprocess.CalledProcessError):
return "UNSET"
return completed.stdout.strip() or "UNSET"
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--host-class", required=True)
parser.add_argument("--model-channel", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("command", nargs=argparse.REMAINDER)
args = parser.parse_args()
if not args.command or args.command[0] != "--":
print("usage: session_receipt.py ... -- command", file=sys.stderr)
return 2
command = args.command[1:]
if not command:
print("missing command", file=sys.stderr)
return 2
started_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
cwd = os.path.realpath(os.getcwd())
output_path = Path(args.output)
completed = subprocess.run(command)
exit_code = int(completed.returncode)
if output_path.is_file():
output_sha = sha256_file(output_path)
else:
output_sha = "UNSET"
receipt = {
"started_at": started_at,
"cwd": cwd,
"git_head": git_head(),
"command": command,
"exit_code": exit_code,
"output_file": str(output_path),
"output_sha256": output_sha,
"host_class": args.host_class,
"model_channel": args.model_channel,
}
Path("RECEIPT.json").write_text(
json.dumps(receipt, indent=2) + "\n",
encoding="utf-8",
)
print("wrote RECEIPT.json")
return exit_code
if __name__ == "__main__":
raise SystemExit(main())
Proposed checker
Save this file as check_receipt.py. Run it after the wrapper exits. It only validates the souvenir file.
#!/usr/bin/env python3
"""Proposed RECEIPT.json checker. Unexecuted in this article."""
from __future__ import annotations
import json
import sys
from pathlib import Path
REQUIRED = (
"started_at",
"cwd",
"git_head",
"command",
"exit_code",
"output_file",
"output_sha256",
"host_class",
"model_channel",
)
HOSTS = {"laptop", "ci", "free-server"}
CHANNELS = {"none", "local", "free-model"}
def main() -> int:
path = Path("RECEIPT.json")
if not path.is_file():
print("RECEIPT.json is missing")
return 1
data = json.loads(path.read_text(encoding="utf-8"))
missing = [key for key in REQUIRED if key not in data]
if missing:
print("missing fields:", ", ".join(missing))
return 1
if data["host_class"] not in HOSTS:
print("unknown host_class")
return 1
if data["model_channel"] not in CHANNELS:
print("unknown model_channel")
return 1
if data["exit_code"] != 0:
print("exit_code is not zero:", data["exit_code"])
return 1
if data["output_sha256"] == "UNSET":
print("output hash is UNSET")
return 1
if not isinstance(data["command"], list) or not data["command"]:
print("command must be a non-empty list")
return 1
print("receipt fields present")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Tiny demo test
This fixture only supports the workflow. It is not a benchmark. Do not cite it as performance evidence.
# test_receipt_demo.py
def test_addition():
assert 1 + 1 == 2
Commands I would type
These commands are a proposed path. Label them unexecuted until you run them.
git init receipt-demo
python3 -m pip install pytest
chmod +x session_receipt.py check_receipt.py
python3 session_receipt.py \
--host-class free-server \
--model-channel free-model \
--output junit.xml \
-- pytest -q --junitxml=junit.xml
python3 check_receipt.py
cat RECEIPT.json
Why pass --model-channel free-model on a pytest command? Only if a free model drafted that command list. Otherwise set none and stay honest.
Numbered plan you can re-run
- Put both scripts in a fresh git repo.
- Add
test_receipt_demo.pyand commit immediately. - Run the wrapper around
pytest -q --junitxml=junit.xml. - Run
check_receipt.pyand expect a clean pass. - Flip the assertion to
1 + 1 == 3. - Re-run the wrapper and watch the hash move.
- Delete
RECEIPT.jsonand confirm the checker dies.
If step six does not change the hash, stop. Your output file is the wrong object. You hashed air and called it evidence.
Decision table
What claim are you trying to make now? Match the claim to required fields. Then read the last column twice.
| Claim you want | Fields you must have | Is a free server enough? |
|---|---|---|
| It ran once |
command, exit_code
|
Only on that host |
| It ran the same | plus cwd, git_head, hash |
Only after a re-run |
| Merge it | plus CI on the same tests | No |
| Roll it back | plus a stored artifact | No. Reset is not rollback |
A free server can host the experiment. It cannot host the merge decision. Keep that split when you get tired.
Questions after the table
Can I skip the free server?
Yes, a laptop can write the same receipt. The free server is optional cheap compute. Use it when your laptop should stay clean.
Can I skip the free model?
Yes, the receipt never calls a model. Use a model only to draft commands. Then wrap the real command with the script.
Why hash one output only?
Because this FAQ is about minimum honesty. One declared output is a low bar. Side effects can still leak around it.
If your run writes three artifacts, extend the script. Hash a manifest of those paths instead. Do not pretend the default caught them.
Why record model_channel at all?
So you remember how the command was born. Drafted text is not executed text. The receipt stores the executed text only.
What this does not prove
The receipt is not a cryptographic attestation. Anyone can type JSON by hand. Trust the process, not the filename.
A hash of one file misses side effects. Temp files can live outside the hash. Logs can vanish with the process tree.
Tool versions are absent on purpose here. Add them when your bug actually cares. The script will not pin pytest for you.
Host clocks drift without asking you first. started_at is only a coarse hint. Do not cite it as capacity planning data.
A free server may differ next session. I am not promising sameness at all. That uncertainty is why receipts exist.
Who should skip this approach
Skip this if you already have real CI. Do not replace CI with a JSON souvenir. Souvenirs do not gate the main branch.
Skip this for secret-heavy work as well. Name the isolation boundary first, out loud. If you cannot name it, keep secrets off.
Skip this if you need a vendor SLA. A free option is not that contract. Skip this if you merge from laptop vibe.
Closing
Which myth did you repeat last week? I still repeat myth three when tired. The receipt is how I catch myself.
If you already have free model access and a free server, run the wrapper once. Then name the first field you could not fill.
Top comments (0)