Essay

A Worktree Isn't a Dev Environment

Color takes as long as color takes. You don’t stand there and watch it.

Back when I did hair, working two clients at once wasn’t a stunt and it wasn’t me hustling. It was the schedule. It was written down.

Here’s how that goes. Your ten o’clock is a color. You mix it, you get it on, and then it sits. How long depends on the color and the head it’s on. It’s processing, and you need to wait. So the book already has another client scheduled. You didn’t squeeze the second client in. You planned her.

And you learned to read the book that way. You’d look at a Saturday and see it in layers instead of rows. One color at ten, the next at eleven while the first one processes. Rinse the first while the second sits and processes. Two clients, one stylist, and if you did it right neither of them ever felt like the other one existed.

I got good at that. Then I went and started a completely different career.

One at a time

Programming doesn’t have processing time. Not really.

There’s the build. There’s the test suite. There’s CI, and those are real, but they’re a few minutes and a few minutes isn’t room to start anything in. Maybe waiting on a PR review. That one’s closer, but it isn’t quite the same, because you don’t know when it’s coming back. You can’t schedule around a gap you can’t size. Mostly the work is you. You’re the thing that’s processing. There’s no gap to book in.

One column, top to bottom, one thing at a time. That’s not a complaint. That’s just the shape of the work, and I never once thought about it, because there was nothing there to think about.

Then the gap came back

I’ve been running a lot of agents lately. (More on how I work with them over here.)

And there it was. A gap.

You load up the context, you describe the work, you plan, you review the plan, you hit go, and then it goes. Fifteen minutes. Half an hour. Whatever it is. It’s processing, and there is nothing you can do to speed it up and nothing useful to do to it while it works.

The first few times, I mostly waited. I checked email, I looked to see if anything on my todo list could get tackled quickly. I stared at the agent doing the work. But I didn’t start any more work.

Then something old woke up. Because there was a whole other task sitting right there with nothing to do with the one that was running, different part of the app, no overlap, no reason on earth those two couldn’t run in parallel. And I knew that feeling. I’d had it a hundred times.

Who would have thought. Thirteen years out of the salon and the scheduling instinct came back like I’d used it last week.

So I went to go book the gap.

One at a time, still

Worktrees. Obviously worktrees.

If you haven’t used them, git worktree hands you a second checkout of the same repository in a second folder, on a different branch, sharing one .git. It’s genuinely great and I use it constantly. So I made a worktree, dropped an agent in it, and told it to go.

Then I booted the server so I could look at what it had done.

And it grabbed the port the other server was already sitting on. Or it didn’t, it came up perfectly fine, and then the agent in the second worktree ran a migration.

Against the same database my first task was sitting on top of.

There it is. The worktree gave me a second set of files and not one thing more. One database. One set of ports. One of every service that actually runs. And there is no rolling another database over.

A worktree isolates your files. It does not isolate your running system.

Once I said that out loud it seemed too obvious to bother saying. But I had been quietly treating “I have a separate folder” as “I have a separate dev environment”, and I don’t think I’m the only one. File isolation is the part you can see. Everything else stays invisible until it bites, and it bites as a migration you didn’t run turning up in a database you were in the middle of thinking about.

Back to two at a time, or more

So I started poking at what a real second client would need. Not a second folder. A second everything.

  • Its own working copy (the worktree part, the part I already had)
  • Its own database, clean
  • Its own ports, for every service, not just the web one
  • A process manager to run it in the background, because holding a terminal open per environment defeats the whole purpose
  • And this one: change zero tracked files

That last one is the ballgame. It’d be easy to write a script that edits your config to point at port 4010 and a database called myapp_dev_2. That script works on the first try. And then you’ve got a dirty git status in two places, and one day you commit a port number, and now your tool is a thing you have to remember to undo. A tool you have to remember to undo isn’t a tool. It’s a chore with extra steps.

So all of the isolation has to ride on files the repo already ignores. Env files, local overrides, things that were invisible to git before you got there. Once I wrote that constraint down, a lot of the design questions answered themselves.

How it actually works

It’s two skills, each pointed at a script. One sets an environment up, the other tears it down. Hand the first one a branch name and it creates the worktree, gives it its own clean database, boots the app on its own ports, and changes ZERO tracked files.

Once I had this cooking at home, I brought it to work and adapted it to run in that environment.

It does two jobs equally well. One is running a second track of work, or more, alongside your main one. The other is UI testing, one branch or several of them at once, side by side. (More on that one in a post coming soon.)

Though at this point I don’t even keep a main checkout going. New envs all the way down.

Every environment gets a number, and the number decides the ports. The database gets its name from the branch, so it comes out unique for the same reason. All of the overrides ride on files git was already ignoring.

Picking the number is the part I’d have gotten wrong if I’d written it the obvious way. Booting several at once isn’t the edge case here, it’s the whole point, so choosing a number and reserving it have to happen atomically. The script takes a short lock, reads every other environment’s saved claim plus whatever is actually listening, picks the lowest number that is neither, and writes down its own claim, so a terminal that boots a moment later sees the reservation and steps right past it. The claim takes an instant. Do those in the wrong order and two terminals a minute apart both see the same pair sitting free and both take it.

A stopped environment keeps its number. The claim file stays put, so picking it back up later is collision free. When you’re really done with it, the teardown skill drops the database and removes the worktree.

Back to the salon schedule

Spinning up a branch now takes a command and about a minute, and the interesting part is what that did to how I treat branches. When it’s cheap, you stop being precious. Throwaway experiment, second opinion on the same task, a branch somebody wants eyes on. Sure. Spin it up, look at it, tear it down.

I don’t have a number for how much time it saves and I’m not going to invent one. What I have is the qualitative version, and it’s the one I recognize. I look at the day in layers instead of rows again. Kick this off, start that, come back and rinse the first one while the second is still processing.

A second folder was never a second environment. It took a salon schedule to remind me of the difference.

Who would have thought.