DEV Community

Taylor Wang
Taylor Wang

Posted on

I Would Not Land the Edit Until the Server Tree Shared My Hash

I spent two days chasing a failing command, and the tempting shortcut was to let a model rewrite the file. Why would I keep hand-editing a small parser when a free model could draft the change? The trouble started when the server copy and my laptop copy stopped being the same tree. Treat the notes below as a worked failure you can reproduce, not as a measured outage report.

The failure I kept blaming on the edit

The command was small, and that smallness made the miss much easier to excuse at first. I ran a Python check, watched it fail, and asked for a unified diff instead of a full rewrite. The diff looked tidy, applied on my laptop, and still failed after I copied those names into a clean directory. Have you ever watched a green local apply turn red the moment the tree was copied elsewhere?

What broke was not a clever parser bug hiding inside the project I was trying to fix. The server job started from a slightly older checkout, so matching context lines still let the patch land. I had treated a successful apply as proof that both sides still shared one unchanged tree. They did not share that tree, and the original failing exit code was never stored beside the diff.

I also let the afternoon sprawl across three places that never pointed back at each other. Notes lived in the chat, the patch lived in my editor, and the failing command lived only in shell history. Which of those three would still be trustworthy if I opened the same ticket tomorrow morning? None of them formed a bundle I could hash, copy, and run again without guessing at the missing context.

What I would freeze before a model sees the tree

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

I looked at MonkeyCode because free model access and a free server option were confirmed for this workflow. That availability is enough to try a bounded repro, and it is not a reason to skip a hash. I am not naming a model, a quota, a machine size, or a promise that the free option stays fixed. If those terms matter to the job you are running, read the current product docs before you depend on them.

The receipt I started requiring is deliberately dull, which is the compliment I want from a gate. It holds relative paths, a sha256 for each file, one hash of that sorted list, and the exact command. It also holds a slot for the diff, saved beside the receipt rather than pasted into a scrolling chat. A free server is useful when that receipt is the whole job, not a memory of yesterday's disk.

Would I send a prompt that only says fix the tests, with none of those fields filled in? Not after I watched the wrong function change while the apply command still reported a clean success. The model can still suggest a bad edit, but at least I can prove which tree it was answering. If the hash differs, I stop the review, and I do not negotiate with a diff that lost its tree.

The gate I would run before apply

The script below is a local gate I would run before any remote apply, including a free server job. It copies the receipt files into a temp directory, checks the diff, applies it there, and runs the recorded command. I am not publishing timing numbers for it, because a correctness gate does not need a leaderboard to be useful. Read it as a proposal you can execute, then decide whether the refusal messages match the risk you actually carry.

#!/usr/bin/env python3
"""Temp-copy apply gate for a unified diff against a frozen tree receipt.

Unexecuted proposal: run this on a toy repo before any remote apply.
It does not call a network service, and it does not print a benchmark.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import shlex
import shutil
import subprocess
import tempfile
from pathlib import Path


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    digest.update(path.read_bytes())
    return digest.hexdigest()


def tree_hash(files: list[dict]) -> str:
    lines = [f"{item['sha256']}  {item['path']}" for item in files]
    payload = "\n".join(lines).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()


def build_receipt(root: Path, rels: list[str], command: list[str]) -> dict:
    files = []
    for rel in sorted(rels):
        path = root / rel
        if not path.is_file():
            raise SystemExit(f"missing file: {rel}")
        files.append({"path": rel, "sha256": sha256_file(path)})
    return {
        "command": command,
        "files": files,
        "tree_sha256": tree_hash(files),
    }


def assert_same_tree(root: Path, receipt: dict) -> None:
    names = [item["path"] for item in receipt["files"]]
    current = build_receipt(root, names, receipt["command"])
    if current["tree_sha256"] != receipt["tree_sha256"]:
        raise SystemExit("tree hash mismatch; refuse the diff")


def apply_in_copy(root: Path, receipt: dict, diff: Path) -> int:
    with tempfile.TemporaryDirectory(prefix="apply-gate-") as tmp:
        dest = Path(tmp) / "tree"
        dest.mkdir()
        for item in receipt["files"]:
            target = dest / item["path"]
            target.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(root / item["path"], target)
        subprocess.run(["git", "init", "-q"], cwd=dest, check=True)
        checked = subprocess.run(
            ["git", "apply", "--check", str(diff)],
            cwd=dest,
            text=True,
            capture_output=True,
        )
        if checked.returncode != 0:
            detail = checked.stderr.strip() or "git apply --check failed"
            raise SystemExit(detail)
        applied = subprocess.run(
            ["git", "apply", str(diff)],
            cwd=dest,
            text=True,
            capture_output=True,
        )
        if applied.returncode != 0:
            detail = applied.stderr.strip() or "git apply failed"
            raise SystemExit(detail)
        completed = subprocess.run(receipt["command"], cwd=dest)
        return completed.returncode


def main() -> None:
    parser = argparse.ArgumentParser(description="Freeze a tree, then gate a diff.")
    parser.add_argument("--root", type=Path, required=True)
    parser.add_argument("--receipt", type=Path, required=True)
    parser.add_argument("--write", action="store_true")
    parser.add_argument("--files", nargs="*")
    parser.add_argument("--command", default="")
    parser.add_argument("--diff", type=Path)
    args = parser.parse_args()

    if args.write:
        if not args.files or not args.command.strip():
            raise SystemExit("write mode needs --files and --command")
        command = shlex.split(args.command)
        receipt = build_receipt(args.root, args.files, command)
        text = json.dumps(receipt, indent=2) + "\n"
        args.receipt.write_text(text, encoding="utf-8")
        print(receipt["tree_sha256"])
        return

    receipt = json.loads(args.receipt.read_text(encoding="utf-8"))
    assert_same_tree(args.root, receipt)
    if args.diff is None:
        print("tree ok", receipt["tree_sha256"])
        return
    code = apply_in_copy(args.root, receipt, args.diff)
    if code != 0:
        raise SystemExit(f"recorded command exited {code}; do not land the diff")
    print("apply ok", receipt["tree_sha256"])


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

A receipt written by that script looks like the placeholder below, with real digests filled only after you run it. Do not paste these stand-in strings into a ticket and pretend they prove anything. The shape is the useful part, and the hashes are evidence only when the script just printed them.

{
  "command": ["python3", "-m", "unittest", "test_parse.py"],
  "files": [
    {"path": "parse.py", "sha256": "filled-by-the-script"},
    {"path": "test_parse.py", "sha256": "filled-by-the-script"}
  ],
  "tree_sha256": "filled-by-the-script"
}
Enter fullscreen mode Exit fullscreen mode

What the script refuses to do

The script refuses three quiet failures that a long chat window will happily ignore until review. It exits when a receipt file is missing, when the tree hash moved, or when the apply check rejects context. It also exits when the recorded command fails inside the temp copy, which is the result I used to shrug off. It will not open a network connection, choose a model, or decide that a product offer is still valid this week.

Commands I would keep in the ticket

Create a throwaway directory so the check has a real worktree, not a pile of unnamed loose files. Copy only the files named in the receipt, write that receipt, and only then ask for a patch. The commands below are the ones I would repeat on the next failure, and they do not depend on a vendor CLI. If your tool cannot emit a unified diff, stop there, because this gate has nothing safe to apply.

mkdir -p /tmp/replay-demo
git init -q /tmp/replay-demo
cp parse.py test_parse.py /tmp/replay-demo/
python3 replay_gate.py --root /tmp/replay-demo --receipt /tmp/receipt.json --write --files parse.py test_parse.py --command "python3 -m unittest test_parse.py"
python3 replay_gate.py --root /tmp/replay-demo --receipt /tmp/receipt.json --diff /tmp/suggested.diff
Enter fullscreen mode Exit fullscreen mode

Notice that the recorded command runs inside the temp copy, not against the only folder you care about. That split is the whole point, because a model diff should have to earn its way back onto the branch. Why give a generated patch write access to the original tree before a second process has exited zero? I made that mistake on the first afternoon, and the clean directory is what finally made the drift obvious.

How I decide whether the edit may land

I keep a short table in the ticket so the next choice is mechanical instead of hopeful. The rows are not a scoring rubric for models, and they are not a claim about which host is faster. They only answer whether this particular diff may touch the branch that other people will pull. If a row says refuse, I refuse, even when the patch reads like something I would have typed.

What I see What I do next What I refuse
Receipt hash matches, apply check passes, and the temp command exits zero Keep the diff as a candidate for the branch Landing it on a server tree I have not rehashed
Apply check passes, but the recorded command still fails Save the log beside the receipt and stop Asking for another diff against a dirty tree
Tree hash mismatches before any apply Rebuild the receipt from the tree I will run Saying the files are probably the same
Diff touches a path that the receipt never listed Add that path and rehash, or reject the diff Applying extra files the server never received

The row I violated first was the mismatch row, and it did not look dramatic while I was tired. The filenames matched, the patch applied, and I told myself the extra blank line could not matter. Could a blank line move a context match into the neighboring function and still look successful? Yes, and that is why the receipt hash is stricter than my memory of what I uploaded.

Where the free server fits

I would use the free server as a clean room, not as a second laptop that remembers yesterday. I would place only the receipt files there, run the same command, and compare that exit code with the temp copy. If I also use free model access, I ask only for a unified diff against those exact paths. That diff goes back through the gate, and it does not get applied on a long-lived machine first.

Is a free seat a reason to skip the temp copy and trust the remote edit directly? I do not think so, even when the second run costs nothing and the queue looks empty. Free access can lower the cost of repeating a command, not the cost of a wrong shared edit. Availability is not the same thing as a captured tree, which is the mistake I kept repeating after lunch.

What I would repeat on the next failure

The next failure does not need a new theory, because the useful part was already visible on day two. I would repeat the same five moves, and I would write them into the ticket before I open a chat. Would I change the order just because a model answered quickly and sounded sure of the patch? No, speed is not evidence that the uploaded bytes still match the receipt I hashed locally.

  1. Freeze the named files and the exact command into a receipt before I write any prompt.
  2. Run the local gate first so a bad diff dies inside a temp directory instead of my branch.
  3. Repeat that command on the free server only after the receipt hash matches the uploaded files.
  4. Accept the diff as a candidate only when both runs exit with the same recorded code.
  5. If either side drifted, rebuild the receipt instead of editing the prompt to hide the miss.

That list is the part I would tape above the editor, because the model suggestion is optional and the hash check is not. If the free server is busy, offline, or no longer offered on the terms I expected, the local gate still stands alone. I would rather lose a convenient second machine than land an edit I cannot replay from the receipt. Have you kept a green apply and lost the command that was supposed to justify it?

Who should skip this gate

Skip this gate when the change is a binary asset, a huge generated lockfile, or a rename across half the repository. A context diff gets clumsy there, and a sha256 receipt will not tell you whether a migration is wise. Also skip it when you cannot name the command that should pass after the edit lands. A clean apply with no command behind it is only a text merge, and I already know how those lie.

Do not use a free seat as a drawer for secrets, production dumps, or customer data just because the seat exists. A tiny parser and a fake failing test are enough to learn the habit without dragging private material along. If your policy forbids sending source to an external model, run the gate on diffs you wrote yourself. The hash check still earns its place in that case, and the model can stay out of the loop entirely.

What I still will not invent

I still do not know how long any free server offer remains, or which model names sit behind it today. I also do not know the quota, the hardware, or the duration you will see when you open the product. Those facts go stale quickly, so inventing them here would make this note less honest than the gate it describes. Check the current MonkeyCode docs, and if the docs disagree with this note, trust the docs.

If you already hash the files you meant to upload, borrow one extra step from this weekend. Take one failing command you already understand, and run it through the temp-copy gate before a generated diff touches the branch. That is the only practice I would bother repeating, and it stays useful even if you never open the free seat. The seat can make the second run cheaper, but the receipt is what tells you the second run was the same tree.

Top comments (0)