Hash the Side-Effect Ledger Before You Accept a Cleanup Refactor A developer proposes a workflow to prevent coding agents from breaking hidden couplings during cleanup refactors. The method involves freezing a ledger of side effects and storing a SHA-256 hash before any structural changes, ensuring the hash remains identical to validate behavior preservation. The approach addresses the failure mode where return-value tests pass but implicit file layouts or environment dependencies shift. Messy modules rarely break because a pure helper returns the wrong integer on a tidy fixture. They break because three functions share a temporary CSV path, an environment flag, and a cache nobody named. A coding agent then proposes a cleanup that deletes dead branches, renames locals, and still satisfies every existing assertion. The next production export fails because the implicit file layout moved while the return payload stayed identical. That failure mode is the reason this workflow exists, and it is not a style problem. The first commit should freeze a ledger of hidden couplings and store a hash beside it. Only after that hash is in source control should you allow one structural change. The cleanup is legitimate only when the recorded hash remains identical. Feature work usually changes an observable on purpose, so reviewers know which assertions must move. Cleanup work is sold as behavior-preserving, which trains people to trust deletions and rename-only hunks. Coding agents amplify that bias because they optimize for shorter files, conventional names, and green unit tests. Reviewers then accept large deletions that would look suspicious inside a feature pull request. Return-value tests are the wrong gate for that class of change. The public function can still return {"ok": true, "rows": 12} while the working directory quietly shifts. Downstream jobs that glob files or catch a named exception will fail after merge. Those hidden couplings remain part of the contract even when no unit test mentions them. Treat the messy module as a black box that emits more than a return value. A ledger is a canonical JSONL file with one record per fixture and fully sorted keys. Side-effect entries need stable ordering so the serialized bytes stay deterministic across reruns. The SHA-256 digest of that file is the only number that must remain constant. Each record should capture the following fields and omit anything that varies by machine: return , raise , or timeout Canonicalize every path against the sandbox root before you serialize the record. Wall-clock timestamps and absolute home-directory prefixes make the hash flaky on contact. A flaky gate teaches the team to skip the protocol, which is worse than having no gate. ledger.jsonl plus ledger.sha256 with no production code changes. If the hash changes, the cleanup is not a cleanup and should not keep that label. Treat the diff as a behavior change, add an intentional test, and restart the protocol. Agents that continue after a mismatch are doing product work under a refactor heading. The script below is an unexecuted example you can adapt to one entrypoint. It does not claim production coverage numbers, and it will miss native writes outside the sandbox. Read it as a starting template rather than as a library you vendor unchanged. ledger recorder.py — proposal: pin hidden couplings for one entrypoint from future import annotations import hashlib import json import os import sys import traceback from pathlib import Path from typing import Any, Callable SANDBOX = Path os.environ "LEDGER SANDBOX" .resolve LEDGER PATH = Path os.environ.get "LEDGER PATH", "ledger.jsonl" def canonicalize path: Path - str: try: return str path.resolve .relative to SANDBOX except ValueError: return f"