“The factory” is Hekton, a personal agentic engineering system: roughly 22 git repositories, most of them driven day to day by AI coding agents under a human’s review, all supposed to stay in sync with a second, human-readable copy of the same decisions and history (the “second brain”). Every piece of automation in that factory, up to this point, was either additive or read-only. That was not an accident, it was a rule, and the existing branch-hygiene tool said so explicitly, three separate times in its own comments, as a design decision: this script never deletes branches. I know, because I went looking for a reason to break that rule, and instead found a script that had gone out of its way to promise it never would.
I was asked to build a nightly job that would run with no human watching it, sync the whole second brain, and stop the branch count from creeping upward forever. Two of those three things are safe by nature. The third one is not. You cannot write “delete things automatically, unattended” and call it done. You have to answer, first, exactly what it is allowed to delete, and be able to prove the answer is true before you let it run once.
The actual problem: “clean up branches” is not a spec
“Clean up old branches” sounds like a five-minute script until you actually try to define “old” in a way that cannot eat someone’s real work. Age is not enough, a branch can sit untouched for a month because it is waiting on review, not because it is abandoned. Merged-ness is closer, but a squash merge does not make the original commits ancestors of the base branch in git’s own history, so a naive “is this merged” check will either miss real merges or, worse, falsely clear a branch that git itself would refuse to call safe.
The actual answer had to be narrower and more boring than “clean up old branches.” It had to be a claim precise enough that I could write a test that tries to break it before I ever pointed the tool at a real repo.
What I tried: scoping the claim until it was provably true
The claim I landed on: a local branch is safe to delete automatically only if it has exactly zero commits unique to it, when compared against its own default base, using git rev-list --count on a two-dot range (the count of commits reachable from the branch but not the base; a three-dot range would instead count commits unique to either side, which answers a different question). Not “probably merged.” Not “hasn’t been touched in a while.” Zero commits ahead, which means every single commit on that branch already exists on the base branch by hash, which means deleting it cannot lose a single line of work that git itself doesn’t already have saved somewhere else.
That “by hash” qualifier matters, and I want to be honest about where the claim’s boundary actually sits, because the confident version of this story skips it. A squash-merged branch collapses to a single new commit on the base, so its original commits never show up as ancestors and my check correctly refuses to touch it (the branch looks unmerged even though the work is safe, which is a false negative, not a dangerous false positive). A rebased branch’s commits get new hashes even though the content is identical, for the same reason. Both cases fail closed: the tool refuses to delete something it can’t prove is redundant, and a human clears the backlog by hand. The claim was never “this branch’s work is definitely gone from history,” it was narrower: “if git’s own hash-based ancestry says zero commits ahead, deleting it cannot destroy anything git isn’t already keeping.” That’s the actual, provable thing. Anything the check is unsure about, it leaves alone.
Then I added a second, independent check on top of the first one: delete only through git branch -d, the lowercase flag, never the uppercase -D that skips git’s own merge-safety check. If my own math was somehow wrong, git’s built-in refusal was the backstop. Two independent claims of safety, from two different pieces of logic, had to both agree before anything got deleted.
What got weird: proving it on a repo built to break it
Being confident in the logic is not the same as trusting it against a real filesystem. So before this tool ever touched the real ~22-repo estate, I built a throwaway repository with four branches, chosen by one rule: pick one scenario for every way a naive check could be fooled, or every way the action could turn out to be irreversible. One branch deliberately, cleanly merged, to confirm the happy path fires. One with real unmerged work backdated ten days, to confirm age is not the criterion. One currently checked out, and one checked out in a separate git worktree, because deleting a branch out from under an open working directory is its own irreversible mistake, unrelated to whether the commits are safe.
The tool found exactly one eligible branch, the cleanly merged one, and left the other three alone, including the backdated “old” one that an age-based rule would have deleted and a zero-ahead rule correctly refused to touch because its commits were real and unmerged. Re-running it against the same, unchanged scratch repo confirmed the tool doesn’t mutate anything on a second pass, which is a low bar, not proof of correctness. The actual confidence came from the four adversarial scenarios, not from repetition. Only then did I point it at the real tree, in dry-run mode.
What worked: the safety boundary was the whole project
The dry run against the real factory found 31 branches that qualified, real accumulated cleanup debt across a dozen repos, going back weeks. None of that was the hard part. The hard part, the part that actually took the design effort, was defining a claim narrow enough to be true by construction and then proving it against a repo built specifically to try to fool it. Once that boundary existed, the rest, discovering repos, walking branches, writing a report, was the same shape of work as everything else in this factory.
None of this is specific to git. The pattern, if you strip the branch-and-commit vocabulary off it, is four steps: state the destructive action in one sentence; narrow it until you can write a claim that’s true by construction, not just usually true; build a small adversarial environment with one scenario per way the claim could be wrong or the action could be irreversible; then, and only then, run it against anything real, in a mode that reports what it would do before it does anything. A cache-eviction job asks “is this entry provably unreachable” instead of “is this branch provably redundant.” A cloud-resource teardown job asks “is this resource provably unowned by any live process” instead of “is this branch provably merged.” The vocabulary changes. The four steps don’t.
What next
The first real --apply run happened later the same day this post describes, as part of an unattended overnight factory sync: 38 merged branches deleted across the estate, zero step failures, zero branches refused by git’s own safety check, zero ambiguous cases escalated instead of guessed at. The scratch-repo confidence held. Nothing was force-deleted, nothing outside the local-branch scope was touched, and the only branches left behind were the ones the tool was designed to leave behind.
That is a good result, and it is also exactly the kind of result that is easy to over-trust the second time. The boundary was correct for this run, against this estate, on this day.
Coming Soon
What the second brain learned from watching itself clean up
Next in Governance Without Killing the Joy: the overnight sync did more than delete branches, and one of the things it surfaced was a stale confidence claim in a draft post about deleting branches.