← All posts

The Git Check That Proves Isolation Isn't the One You'd Reach For

Before an agent writes anything, it has to prove it's in a disposable copy of the repo and not my real one. The obvious command for that returns a confident answer to a different question.

  • agents
  • git
  • worktrees
  • safety

Every agent I dispatch gets its own throwaway copy of the repository. It branches there, commits there, and when it’s done I take the pull request and the copy gets deleted. The whole arrangement rests on one assumption: the agent is actually in the throwaway copy and not in my working checkout — the one with my uncommitted changes in it.

So the first instruction every agent gets is: prove it before you touch anything.

The interesting part is which command proves it, because the one that feels right is wrong.

The mechanism, briefly

Git has a feature called a worktree: a second working directory attached to the same repository. You get a separate folder with its own checked-out branch, but it shares one underlying object store with the original. Think of a shared warehouse with several loading docks — different docks, same inventory.

That sharing is the point. It’s also the trap.

If you ask a worktree where its git data lives, you get a path that points back into the original checkout:

$ git rev-parse --git-dir
/opt/projects/my-repo/.git/worktrees/task-3

$ git rev-parse --git-common-dir
/opt/projects/my-repo/.git

Both of those answers name the primary checkout. Both are correct. Neither one tells you whether you are standing in it.

I want to sit on that for a second, because this is the actual lesson and the git specifics are just the vehicle. --git-dir answers “where is my metadata.” The question I care about is “where am I.” Those produce different answers, and the first one is more authoritative-sounding while being completely useless for the check I’m making. An agent that runs --git-dir, sees a path containing worktrees/, and concludes “I’m isolated” has reasoned from a real fact to a correct-looking conclusion by way of a coincidence.

Worse, the failure is asymmetric. In the safe case it happens to give a comforting answer. In the dangerous case — an agent that got launched in the primary checkout by mistake — --git-dir returns /opt/projects/my-repo/.git, which is also a perfectly normal-looking answer. There’s no shape you can pattern-match on that separates the two reliably.

The check that actually answers the question

pwd -P
git rev-parse --show-toplevel

--show-toplevel gives the root of the working tree you are standing in, not the repository the working tree belongs to. pwd -P resolves symlinks so a symlinked path can’t disguise where you really are. If either resolves to the primary checkout, the agent stops — it does not branch, does not commit, and reports that it was launched in the wrong place.

That’s it. Two commands, and the rule that goes with them: the path is authoritative; the git plumbing is diagnostic. The plumbing commands are still useful for understanding the repo’s structure. They just don’t get a vote on this particular question.

The general version

Strip out git and the pattern is one I keep re-encountering: verifying a proxy for the property instead of the property.

Each one has the same tell, and it’s a good tell because you can check for it deliberately: the proxy and the property agree in every case you thought to try, and disagree exactly in the case you’re afraid of. That’s not a coincidence. It’s why you reached for the proxy — it was correlated in all the situations you had in mind. The situation you didn’t have in mind is the one the check exists for.

The question I now ask when writing any guard: what’s the case where this check passes and the thing I care about is false? If I can’t construct that case, I don’t understand the check well enough to rely on it. If I can construct it and it’s the exact scenario the guard was written for, I have a proxy problem.

Why the stop is absolute

One more design decision worth naming: when the path check fails, the agent stops completely. It doesn’t try to fix its situation, doesn’t create a worktree for itself, doesn’t move to a safer directory.

That’s deliberate, and it cost me an argument with myself. Self-correction is usually the better behavior — an agent that resolves its own problem is worth more than one that files a ticket. But this specific failure means the agent’s understanding of its environment is already wrong, and an agent acting confidently on a wrong model of where it is happens to be the exact thing the guard exists to prevent. Recovery logic written by a component that has just proven it’s confused is not recovery logic I want running against my working checkout.

So it reports and halts. The same instinct as not running git checkout inside a loop’s working directory: when the blast radius includes uncommitted human work, the correct amount of cleverness is none.

A guard that can act is a guard that can be wrong in a new way. Guards should stop.