Have you ever applied a model patch and then failed to name the prompt that produced it? I spent forty-eight hours on that exact embarrassment, and I was not chasing a clever new sampling trick. The code looked fine in the working tree, and the unit tests even passed on retry. The real problem was dumber than model quality because I had no receipt for the edit.
I was not trying to prove a ranking, and I was not collecting a leaderboard screenshot for later. I wanted a boring paper trail that could survive a free-server reboot and a messy terminal. If a future me asked why a helper existed, I needed a hash, a prompt, a file list, and an apply record. That may sound like compliance theater, yet it still beat grepping tmux scrollback after midnight on a tired shift.
What I actually tried for forty-eight hours
I kept a tiny Python helper on a free server and pointed it at free model access I already had through MonkeyCode. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The model only proposed text; the server was simply the machine where that text could touch a checkout. I did not need a hidden agent runtime with tools I could not audit later.
Here is the loop I actually ran, in order, without dressing it up as a platform:
- Snapshot the target files into a content-addressed hash before any write.
- Send a single bounded prompt that may only propose a unified diff.
- Parse the diff, reject denylist hits, and never shell out to
patchblindly. - Write a receipt row first, and only then mutate the working tree.
- Apply, run a fast test command, and mark the receipt kept or rolled back.
Would a full agent framework have done some of this for me automatically in one sitting? It might have, and it might have hidden the write behind a tool call I would not replay. I wanted the journal in sqlite, in a file I still owned after a reconnect.
The denylist I should have started with
I learned the denylist late, which is a polite way to say I learned it after a scare.
-
.env,.env.*, and any path matching*secret* -
id_rsa,*.pem, and*.p12material that never belongs in a patch -
node_modules/,.git/, and lockfiles I did not explicitly ask to rewrite - deployment manifests I was not prepared to review line by line that night
If your model can write files, your denylist is part of the product you are running. Treat it that way even when the box is only a free server for notes.
What broke first
The first break was not a refused prompt at all; it was a successful prompt with an unowned side effect. I asked for a retry helper and received a new logging format as a so-called small cleanup. Have you watched a cleanup rewrite timestamps in three files you never named in the prompt? I have, and the git diff looked industrious while the receipt still said nothing useful.
The second break was replay after a timeout, which every free stack will give you eventually if you keep retrying. I sent the same prompt again, and the model proposed a slightly different patch than before. Without a receipt id, I could not tell which proposal had already hit the working tree. Was the directory from attempt three or attempt four that hour?
The third break was reboot amnesia, and a free server is allowed to restart without asking me first. My tmux buffer is not a database, no matter how long I leave it attached like a lucky charm. After a reconnect I had a dirty tree, a passing test, and no story that I could quote. That pattern is not an outage; it is an unreviewable repository sitting on disk.
The receipt table I ended up with
I wanted something I could query with the sqlite CLI at the end of a session. The schema below is the whole product I actually kept. I am labeling it as the helper from these notes, not as a library you should import into production.
CREATE TABLE IF NOT EXISTS patch_receipts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
prompt_hash TEXT NOT NULL,
prompt_text TEXT NOT NULL,
response_hash TEXT NOT NULL,
proposed_files TEXT NOT NULL,
snapshot_hash TEXT NOT NULL,
apply_status TEXT NOT NULL,
test_command TEXT,
test_exit_code INTEGER,
notes TEXT
);
Status values I actually used during the window:
rejected_denylistrejected_parseapplied_tests_failedapplied_tests_passedrolled_back
If a row is missing, the patch did not happen, and I treated that as law. The rule sounds petty until the moment you need to defend a file.
A local helper, not a platform
The script is deliberately ugly on purpose so I would not spend the window polishing packaging. It uses the Python standard library so the free server does not need a poetry novel just to start journaling patches.
#!/usr/bin/env python3
"""Patch receipt helper used during 48-hour field notes. Not a production agent."""
from __future__ import annotations
import hashlib
import json
import os
import sqlite3
import time
from pathlib import Path
DB = Path(os.environ.get("RECEIPT_DB", "patch_receipts.sqlite3"))
ROOT = Path(os.environ.get("CHECKOUT_ROOT", ".")).resolve()
DENY_PARTS = (".env", "id_rsa", ".git/", "node_modules/", ".pem")
def sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def snapshot_files(paths: list[Path]) -> str:
digest = hashlib.sha256()
for path in sorted(paths):
rel = path.resolve().relative_to(ROOT)
body = path.read_bytes() if path.exists() else b""
digest.update(str(rel).encode("utf-8"))
digest.update(b"\0")
digest.update(body)
digest.update(b"\0")
return digest.hexdigest()
def touches_denylist(relpath: str) -> bool:
lowered = relpath.replace("\\", "/").lower()
return any(token in lowered for token in DENY_PARTS)
def proposed_paths_from_diff(diff_text: str) -> list[str]:
files: list[str] = []
for line in diff_text.splitlines():
if line.startswith("+++ b/") or line.startswith("+++ "):
name = line.split(None, 1)[1]
name = name[2:] if name.startswith("b/") else name
if name != "/dev/null":
files.append(name)
return files
def open_db() -> sqlite3.Connection:
conn = sqlite3.connect(DB)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS patch_receipts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL,
prompt_hash TEXT NOT NULL,
prompt_text TEXT NOT NULL,
response_hash TEXT NOT NULL,
proposed_files TEXT NOT NULL,
snapshot_hash TEXT NOT NULL,
apply_status TEXT NOT NULL,
test_command TEXT,
test_exit_code INTEGER,
notes TEXT
)
"""
)
return conn
def record(
prompt: str,
response: str,
files: list[str],
snap: str,
status: str,
test_command: str | None = None,
test_exit: int | None = None,
notes: str = "",
) -> None:
conn = open_db()
conn.execute(
"""
INSERT INTO patch_receipts (
created_at, prompt_hash, prompt_text, response_hash, proposed_files,
snapshot_hash, apply_status, test_command, test_exit_code, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
sha256_text(prompt),
prompt,
sha256_text(response),
json.dumps(files),
snap,
status,
test_command,
test_exit,
notes,
),
)
conn.commit()
conn.close()
I am not pasting a full HTTP client here, and that omission is intentional for these notes. Wire whatever free model endpoint you already have, then pass the raw prompt and raw response into record() before you touch the tree. If you cannot log the exchange, you do not apply the exchange. That is the entire workflow I trusted.
How I parsed diffs without executing them
Did I really need a custom parser when patch already exists on almost every box? Yes, because patch will happily write if I hand it a path I did not review. The helper above only extracts +++ paths. A separate apply step, which I am labeling as local glue rather than battle-tested library code, copied bytes from a snapshot directory after the denylist check.
def reject_or_continue(diff_text: str, prompt: str) -> str | None:
files = proposed_paths_from_diff(diff_text)
if not files:
record(prompt, diff_text, [], snapshot_files([]), "rejected_parse",
notes="no +++ paths")
return None
if any(touches_denylist(name) for name in files):
record(prompt, diff_text, files, snapshot_files([]), "rejected_denylist",
notes="denylist hit")
return None
paths = [ROOT / name for name in files]
snap = snapshot_files(paths)
record(prompt, diff_text, files, snap, "applied_tests_failed",
notes="pre-apply row; update after tests")
return snap
Would I ship that function as a package? No. Would I run the next model call without it after the cleanup scare? Also no.
Commands I kept in the scrollback on purpose
export RECEIPT_DB=$HOME/receipts/patch_receipts.sqlite3
export CHECKOUT_ROOT=$HOME/work/sample-service
mkdir -p "$HOME/receipts" "$HOME/work/sample-service.snapshots"
python3 - <<'PY'
from receipt_helper import open_db
open_db().close()
print("receipt db ready")
PY
sqlite3 "$RECEIPT_DB" <<'SQL'
.mode line
SELECT id, created_at, apply_status, proposed_files, test_exit_code
FROM patch_receipts
ORDER BY id DESC
LIMIT 8;
SQL
git -C "$CHECKOUT_ROOT" status --short
git -C "$CHECKOUT_ROOT" diff --stat
When those three views disagree, you stop and you do not negotiate with the prompt. Do not rerun the same request until the receipt, the diff, and git tell one story. That habit saved me more often than any temperature tweak I could have copied from a thread.
A decision table I actually followed
| Observation | Do not do this | Do this instead |
|---|---|---|
| HTTP layer failed | Retry straight into the working tree | Record nothing; keep the snapshot |
| Response empty or not a diff | Ask the model to try harder on disk |
rejected_parse, leave files alone |
| Diff hits the denylist | Edit the denylist in the heat of the moment |
rejected_denylist, write a note |
| Tests fail after apply | Commit anyway because the comment looks smart | Roll back from the snapshot, keep the row |
| Tests pass | Delete the receipt to stay tidy | Keep the row, then make a git commit |
Notice that git is the second journal in this setup, not the first source of truth. Git records what I finally accepted into history as a human. The sqlite table records what I considered, including the ugly rejects I would rather forget. Those rejects are the field notes I came for.
What I would repeat
I would repeat the snapshot-before-write rule on any box where a model can touch files I own. I would repeat the denylist as code, not as a sticky note on the side of the monitor. I would repeat the boring sqlite table, because it still opens after a process restart. I would also repeat one prompt per receipt, since batched small fixes made the paper trail lie by aggregation.
Would I repeat the forty-eight hour window as a hard stop on scope and ambition? Yes, because a short window stops me from turning the journal into an accidental platform. The point was to leave a checkout I could explain on Monday, not to invent another agent brand overnight.
What I would not repeat
I would not let the model rewrite tests and implementation inside the same receipt ever again. That pair can pass forever and still mean nothing about the behavior I cared about. I would not store raw prompts that contain production secrets, even on a free server I only use for notes. Redact the prompt first, then hash it, then write the row.
I would not treat a green test command as a product decision just because the comment in the diff sounded confident. Tests encode what I remembered to ask, and I forget things under time pressure. I also would not skip git because sqlite felt official after a few successful queries. If the disk dies, both journals die together unless you copy them off the box.
Limitations, and who should skip this
This workflow does not make a free model safe for unattended writes, and I will not pretend otherwise here. It makes an unsafe write explainable after the fact, which is a smaller claim. It will not catch a plausible bug that the test command does not cover. It will not stop a prompt that asks for a dangerous change you then approve by habit.
Skip this if you need multi-user locking around a shared checkout on that server. Skip this if your compliance team needs a real audit log with retention you can defend. Skip this if you cannot snapshot the files the model might touch before the apply step. Skip this if you were hoping the journal would replace a second human on the diff.
The helper also assumes you can parse a unified diff without executing it as a shell script. If your apply step is os.system(model_text), stop and do not take operational advice from this article. Receipts cannot redeem a command injection you invited in.
Field notes I am keeping next to the database
I wrote three lines in a NOTES.md file beside the sqlite database, because future me will not remember the mood of the window.
- One prompt, one receipt, one apply attempt, with no silent batching.
- Denylist failures are successes of the journal, not wasted model calls.
- If I cannot explain a file after reboot, the file is unowned by me.
That is the whole lesson I would copy into the next checkout without waiting for a better model. The model will keep proposing patches that look locally reasonable in a diff. The server will keep being restartable at inconvenient times. The only question left is whether the next diff comes with a receipt you can query.
If you already have a quiet box and a free model slot, copy the receipt table before you copy another prompt.
Top comments (0)