WZ / PROJECT LINE
01 / 06STARTING POINT

EVERY GOOD THING STARTS SOMEWHERE.

Choose the starting point that feels like you. We’ll find the next move together.

Your starting point

No perfect pitch required. A rough idea is welcome here.

A conversation starts here.
← SIGNAL LOG
lopperOCT 17, 20265 min read

WHY I BUILT LOPPER

AI coding agents leave git worktrees and branches behind, and deleting them is the easy part. Why I built a Mac app that proves each one is safe first.

Coding agents are good at starting work somewhere safe. Claude Code, Cursor, Codex and Conductor each give a task its own git worktree: a second checkout of the repository, on its own branch, so the agent can work without touching what you have open.

That part is great. What nobody does is tell you when a worktree is finished with.

It is small until it isn't

A worktree is a full copy of your files. If the agent installed dependencies there, it is a copy of node_modules too, so each one costs hundreds of megabytes. A few are nothing. After a few weeks of running agents in parallel there are dozens, spread across several repositories, and the branches they hold stop you from tidying up:

CODE
git branch -d claude/checkout-redesign
# error: cannot delete branch 'claude/checkout-redesign' used by worktree
#        at '~/code/web/.claude/worktrees/checkout-redesign'

You can fix one of these by hand. I wrote a guide with the exact commands. Doing it for thirty is a different job, because each one needs the same checks and a single mistake costs you work that exists nowhere else.

Deleting is easy, knowing it is safe is the work

Removing a branch takes one command. The question that matters is whether anything is lost, and git does not answer it as directly as you would hope.

Work can reach the default branch in several ways. A plain merge leaves ancestry you can check. A squash merge leaves no ancestry at all, so git branch -d says "not fully merged" about work that is already in main. A rebase rewrites the commits. And sometimes the honest answer is that nothing shows the work landed.

So Lopper asks in order: is there a merged pull request, is the branch an ancestor of the default branch, is the content identical, is every commit patch-equivalent? The first one that holds is shown as the evidence. If git cannot show any of them, the answer is "no evidence". It never guesses, and a worktree whose state it could not read is never marked safe.

The page I nearly skipped

Building the cleanup list made me ask the opposite question: what exists only on this Mac? Unpushed commits, uncommitted files, stashes, commits that no branch holds any more.

My own Mac listed 105 of them, across six repositories. Lopper never removes anything on that page and never pushes for you. It shows what is at risk, what is inside each item, and the one command that would back it up.

Making a removal reversible

Before anything is removed, Lopper checks the item again against fresh git state. The verdict has to be at least as safe as the one you approved, and the branch has to point at the same commit. Then it writes a keep-alive ref so git cannot clean the commits up, then an undo record, and only then runs the command. If the record cannot be written, nothing is removed.

The delete itself is a compare-and-delete: it only removes a branch that still points where you saw it. It never uses --force. Everything can be undone for 30 days.

What the review found

I had the code that deletes things read cold by two independent reviewers, and I reproduced every finding on the unmodified code before fixing it. Some of what came back was not subtle. One bad value in the settings file reset every protection. A scan of 200 repositories took 100 seconds, and later repositories failed with "could not start git", because each finished git command held its file descriptors open for thirty seconds. After the fix the same scan takes 15 seconds. That figure comes from a synthetic world on an Apple silicon Mac, with the script in the repository so you can rerun it.

The dangerous bugs were in the code I trusted most, and I would not have found them by reading it again myself.

Boring on purpose

Lopper does not reimplement git. It runs your real git and reads what it says, in batches, so a big scan stays quick. The app is native SwiftUI, and the lop command line tool shares the same engine, with --json for scripts and for agents that want to clean up after themselves. There is no account and no analytics in the app, and it makes no network calls of its own.

I have learned to pick the boring option for the infrastructure and spend the effort on the actual problem. Here the actual problem was proof, and git already holds the proof.

Try it

Lopper is free and open source under the MIT licence.

CODE
brew install --cask zawalid/lopper/lopper

The site is lopper.zawalid.com, the code is on GitHub, and there is a case study with the screens and a short film. If it removes something it should have left alone, or leaves something it should have taken, I want to hear about it.

// CONTENTS—