~ / ..

Worktrees, Baby

A second checkout without a second clone, at a path you don't have to think about

an announcement by texarkanine

Git’s known how to do this for years!

The Object Database Is the Whole Trick#

Linus Torvalds’s first README, April 2005, splits Git into two abstractions.

There are two object abstractions: the “object database”, and the “current directory cache”.

Under the heading for the database, the same file says:

The object database is literally just a content-addressable collection of objects.

The cache is the checkout you are looking at: names, permissions, and blobs. History lives in the database. Another folder can point at the same database, hold a different cache, and show a different branch. Both folders read the same objects.

Mermaid Diagram Mermaid Diagram

The second folder took longer than the idea.

In March 2007 Julian Phillips checked in contrib/workdir/git-new-workdir, a script that symlinks objects, refs, and config into a new directory. He was writing down a habit Junio Hamano already had. The commit message carries Junio’s caveat whole:

This risks confusion for an uninitiated if you update a ref that is checked out in another working tree, but modulo that caveat it works reasonably well.

One branch, two directories, and a shared refs. Move the branch in one directory and the other directory is standing on a ref that just left.

Git 2.5.0, released 29 July 2015, gave the second checkout its command. Linked worktrees were already in Git by then, doing the job of that symlink script. Nguyễn ThĂĄi Ngọc Duy introduced git worktree and Eric Sunshine added git worktree add, which gives a linked worktree its own HEAD and its own index.1 Git refuses to check out a branch that another worktree already has.2 The release announcement still called the feature experimental, and git worktree add still wanted a path of your choosing. Removing the worktree meant remembering that path. I stayed in one checkout. The agent stayed there with me.

The Path Was the Tax#

git worktree will make the second checkout, and then it will ask you where to put it. I built git wt so I would stop answering. It lives in git-aliases, my collection of small Git utilities.

git wt go is the way in, and it is idempotent. You name a branch and land in a worktree whose path comes from the repository and that branch. Name the branch again and you get the same folder. The second run changes nothing.

~/worktrees/<owner>/<repo>/<repo>-<branch>

[email protected]:octocat/Hello-World.git on fix-login is ~/worktrees/octocat/Hello-World/Hello-World-fix-login. A slash in the branch name adds a directory level, so feature/login becomes Hello-World-feature/login. A branch that does not exist yet is created from the commit you are on.

git wt done is the way out. The folder is deleted. The branch and the commits stay, so you can throw the directory away. Uncommitted work stops the command. Ignored files go with the directory: a .env you never committed, and node_modules, leave with the folder. The main checkout, the clone you already had, stays.

git wt cleanup is the pile. Every worktree go made for this repository, one list, one question: delete it?. Clean ones go. Dirty ones stay. This means you don’t have to check every one by hand to see if there’s any real work left there.

Because they’re worktrees, deleting the folder of a clean branch just means you can git switch ... to it later, because it’s still in your clone’s database - just not at that path on disk anymore.

By default it only cleans up this repository’s worktrees, but --all lets you run that sweep for every repository under ~/worktrees, from any directory.

The rest of the contract is in the README.

git wt go prints a path and leaves your shell where it is. You’ve still got to cd - no process can do that for you.

Well, there is one process that can - your shell! The git-aliases have optional shell support, which, when installed with make shell, installs a wt function for bash and zsh that wraps the helper and changes directory for you. Now you just wt go <branch> or wt done and it works. Scripts can call git wt and cd (or perform other necessary operations) on the printed path.

Cursor’s Clock#

git wt cleanup sees the worktrees you made with git wt go. Cursor makes its own worktrees, and it deletes them on another schedule.

Ask in the IDE with /worktree, or on the command line with agent --worktree, and the checkout lands under ~/.cursor/worktrees. Since Cursor 3.5 the editor deletes older entries once the machine passes a documented cap of 25.3

Is 25 worktrees a lot?

So depending on the situation, you might want to clean up Cursor’s worktrees much sooner than Cursor would get around to it. You’d need to enumerate them all, check to see if each one had any uncommitted work left in it, and
 hey, that sounds a lot like what we just built above!

git wt cleanup +cursor lumps Cursor’s worktrees in with yours. Now you can easily clean up these potentially-languishing folders of potentially-large size!

Type This#

You need git, bash, and ~/.local/bin on your PATH. make helps with the installation but isn’t used to actually do the thing.

git clone https://github.com/Texarkanine/git-aliases.git
cd git-aliases
make
make shell

Open a new shell so the function is loaded.

cd /path/to/your/repo
wt go fix-login

That puts you in the new folder, on fix-login. If the branch did not exist, it does now, forked from whatever you had checked out. Edit, commit, and push the way you already do.

wt done
git branch

You are back in the main checkout. fix-login is still in that list. wt go fix-login brings the folder back.

When a few of them have piled up:

wt cleanup --list
wt cleanup
wt cleanup +cursor --list
wt cleanup +cursor

Read the list
 then answer the one question!

A Lock Is a Lock#

+cursor had a pile I could find, so I wanted a +claude beside it. Same sweep, one more token.

The reason why that didn’t work is instructive in reinforcing the reason to use git wt instead of either harness’ own built-in worktree management!

Claude Code can make the worktree for you. claude --worktree feature-auth puts it inside the repository, at .claude/worktrees/feature-auth/, on a branch named worktree-feature-auth.

The docs tell you to gitignore that directory, so those files stay out of git status in the main checkout.

Whaaat? Doesn’t that sound like a colossal, looming footgun?

The location is a real limit. cleanup --all finds repositories by reading ~/worktrees, and with +cursor by reading ~/.cursor/worktrees. Claude’s folders sit inside whichever clone you launched from. Once you are in that clone, git worktree list shows them. Finding every clone on a machine is a whole different ballgame. I could have shipped a flag that works only when you are already there - just don’t support --all with +claude.

The lock is where that plan stopped.

While a session is running, Claude holds git worktree lock on the worktree, and git worktree remove will not remove a lock. A live session asked the directory to stay.

An interactive session’s exit already cleans the worktree up after itself. An unnamed session with no changes is also cleaned up. A named session, or one that still has (uncommitted) work, asks you before it deletes anything. The worktrees that survive are ones that you chose to keep.

At least in the moment.

There are two ways that a Claude Code worktree might linger after you want it to:

  1. a noninteractive session with a worktree - claude -p --worktree - that has since exited.
  2. an interactive session you chose to keep, but have since changed your mind and forgotten about.

And, on disk, those will look indistinguishable from

  1. a noninteractive session’s worktree whose session is currently running.
  2. an interactive session you chose to keep, and still want

We can’t tell them apart, so we can’t unlock and delete them.

Technically, Claude Code can tell the first one apart because the git worktree lock is a file with freeform text content, and Claude Code writes a sentence with a process id in it, which it parses on its own to check if the process is still running. We can’t rely on that, as that is not a documented, public interface of Claude Code.

Technically, Cursor would have the same issue in a different flavor with its managed worktrees - it also periodically cleans up worktrees.

In both cases, the answer is simple: Make & manage the worktree yourself, and launch your harness in that directory:

wt go feature-auth
claude

You are already in ~/worktrees/.../<repo>-feature-auth.

Now your harness “doesn’t even” know it’s using a worktree - at least, it certainly doesn’t think it’s supposed to manage them. You’re using one tool to manage them both, and so if you git wt done when you’re done, then you know that anything lingering is something you kept on purpose.

And if you’re changing your mind and want to abandon some, git wt cleanup --list --all can list all of them, so you can decide which, if any, should be kept.

I Used to Clone Into /tmp#

You are on a branch, and the work is unfinished. A second task shows up, and it does not belong on this branch. Checking out the other branch means putting this one down: a stash, a commit you are not ready to make, or waiting until the first task is done.

I cloned the repository a second time. The clone often went in /tmp. Now the second task had a folder but each clone carried its own copy of the objects, and I had to remember which folder(s) I was allowed to throw away. And where they were!

A worktree from git wt go is that second folder without the second clone or detective work. The first branch stays checked out, dirty, where you left it. The second task gets its own checkout and its own branch. A third task gets a third. Six, if you have six tasks. They share one object database. Git refuses to check the same branch out in two of them, so each task sits on a branch of its own2
 you can’t mess it up!

When the task is finished, wt done removes the folder and leaves the branch. The name is still there when you run git branch. The moment to notice is the itch to wait, or to clone into /tmp. That itch is a worktree waiting to sprout.

Once you have opened the second checkout yourself, you can tell an agent to take one instead of editing the branch you are standing on. Cursor’s /worktree is that ask, as is Claude’s own --worktree
 resist the temptation! git wt go keeps your extra disk usage all in one discoverable place.

Worktrees. Just try the thing!.


References#

  1. Git Rev News, Edition 5, 8 July 2015. The linked-worktree facility had already landed, more than half a year earlier, to replace contrib/git-new-workdir. The same edition names Nguyễn ThĂĄi Ngọc Duy for the git worktree command and Eric Sunshine for git worktree add. Git 2.5.0 shipped on 29 July 2015. https://git.github.io/rev_news/2015/07/08/edition-5/ â†©

  2. Git documentation, git-worktree. Adding a worktree refuses a branch that is already checked out in another worktree. https://git-scm.com/docs/git-worktree â†© â†©2

  3. Cursor documentation, “Worktrees,” section “Worktrees cleanup,” describing Cursor 3.5 and later. The default cursor.worktreeMaxCount is 25 per machine. The settings example sets cursor.worktreeCleanupIntervalHours to 6. https://cursor.com/docs/configuration/worktrees â†©

~ / ..