Git Worktrees
A while ago, a teammate (our senior backend dev, Manuel Herrera) mentioned worktrees offhand during a pairing session on the auth service refactor last spring. I'd been using Git for about ten years at that point and had never heard of them. That was a bit humbling, honestly. So here's what worktrees are, how they work, and why I think they should be part of every developer's daily Git workflow. What Even Is a Worktree? Before worktrees make any sense, it helps to understand what Git is actually doing under the hood when you work on a branch. Most people think of a Git repo as "the folder where my code lives," but that's not quite right. A Git repo is really two separate things bundled together: The object store. This is everything inside the .git folder: all your commits, all your branches, all your history. It's Git's internal database. You never touch it directly. The working tree. This is the actual folder of files you see and edit. The code you open in your editor. The stuff you compile and run. When you run git checkout some-branch , Git reaches into the object store, grabs the snapshot of the code for that branch, and writes it out into your working tree. Your files change. That's it. The object store itself doesn't move. Here's where it gets interesting. That relationship between "one object store" and "one working tree" is just the default. Git doesn't actually require it. The object store can serve multiple working trees at the same time, each checked out to a different branch, sitting in different folders on your filesystem. That's a worktree. An additional working directory, linked to the same .git folder, checked out to a different branch, completely independent from your main working directory. Think of it like this: imagine your .git folder is a library, and your working directory is a reading desk. Normally you get one desk. Worktrees let you set up a second desk, a third desk, as many as you need, all drawing books from the same library. What you're reading at one desk doesn't affect the other desks at all. No cloning. No duplicating the entire repo. No downloading anything. You're just telling Git to write a branch's files out into a new folder. What That Means in Practice Say you have a repo at /home/alex/projects/my-app . Inside that folder there's a .git directory (the object store) and all your source files (the working tree). You're on the feature/invoice-rewrite branch. When you add a worktree, Git creates a new folder, say /home/alex/projects/my-app-hotfix , and checks out a completely different branch into it. Your original folder is untouched. Both folders share the same .git object store, but each has its own working tree, its own set of files, its own branch. You can open both folders in separate editor windows. Run both simultaneously. Make changes in one without the other knowing or caring. They're independent working environments sharing one repo's history. That's really the whole concept. Once it clicks, it's hard to unsee. Setting One Up The command you want is git worktree add . Here's the basic shape of it: git worktree add ../my-project-hotfix hotfix/payment-timeout That creates a new directory at ../my-project-hotfix , checks out the hotfix/payment-timeout branch into it, and you're done. Open a new terminal tab, cd into that directory, and work on it completely independently while your original working tree stays exactly as it was. If the branch doesn't exist yet, you can create it on the fly: git worktree add -b feature/dark-mode ../my-project-dark-mode main That creates feature/dark-mode branching off main and sets up a worktree for it in one command. You can see all your active worktrees with: git worktree list Output looks something like: /home/alex/projects/my-project a3f91bc [main] /home/alex/projects/my-project-hotfix 7d204e1 [hotfix/payment-timeout] Want more detail on each one? Pass --porcelain for machine-readable output, which is handy if you're scripting around worktrees: git worktree list --porcelain worktree /home/alex/projects/my-project HEAD a3f91bc2d4e1f3a8b7c6d5e4f3a2b1c0d9e8f7a6 branch refs/heads/main worktree /home/alex/projects/my-project-hotfix HEAD 7d204e1a3b5c7d9e1f2a4b6c8d0e2f4a6b8c0d2 branch refs/heads/hotfix/payment-timeout Clean and simple. Each block is a separate working directory, each on its own branch, all sharing the same repo. The Scenario That Sold Me On This During a service migration, I was deep into rewriting the invoice generation module. Halfway through, our DevOps lead flagged a critical issue on the reporting branch: a date formatting bug that was causing overnight batch jobs to fail. Classic timing. Old me would have stashed everything, prayed nothing breaks, fixed the bug, come back, popped the stash, and then realized something was weird with the node_modules state. Old me also would have complained about it in Slack. Instead I ran: # I'm currently on feature/invoice-rewrite with uncommitted changes git worktree add ../billing-hotfix fix/report-date-format Preparing worktree (new branch 'fix/report-date-format') HEAD is now at 9c3f1a2 chore: update batch job config # Move into the new worktree in a separate terminal cd ../billing-hotfix # Confirm I'm on the right branch git branch * fix/report-date-format # Make the fix, run tests, push git add src/reports/date-formatter.js git commit -m "fix: correct UTC offset in overnight batch date formatting" git push origin fix/report-date-format My invoice rewrite was sitting there completely untouched when I came back to it. No stash, no merge conflicts with my own uncommitted work, no weird state. That was the moment I actually understood why this feature exists. Some Things That Tripped Me Up You can't check out the same branch in two worktrees. Git won't let you do it. If you try, you get an error like: git worktree add ../my-project-main main fatal: 'main' is already checked out at '/home/alex/projects/my-project' Makes sense when you think about it (two places writing to the same ref would be chaos), but it caught me off guard the first time. If you genuinely need to look at the same branch in a second location, check out a detached HEAD instead: git worktree add --detach ../my-project-main-readonly That lands you at the current HEAD commit without locking a branch name, so Git doesn't complain. Each worktree is a real directory. Your editor, your file watchers, your build tools all see it as just a normal project folder. So if you're running a dev server in both worktrees simultaneously, make sure they're on different ports. I forgot this exactly once. Not a fun 10 minutes of "why is my hot reload reflecting changes I didn't make." For a Node project, this looks like running each server on its own port: # In terminal 1, main worktree on feature/invoice-rewrite cd ~/projects/my-app PORT=3000 npm run dev # In terminal 2, hotfix worktree on fix/report-date-format cd ~/projects/my-app-hotfix PORT=3001 npm run dev Both servers are running, both are hot-reloading, neither one interferes with the other. Dependencies are not shared. This one surprises people. The object store is shared, but the actual files on disk are not. So if your project has a node_modules folder or a Python virtualenv, each worktree needs its own copy. You'll have to install deps separately in each one. cd ../billing-hotfix npm install # or pip install -r requirements.txt Not a dealbreaker, just something to know upfront so you're not staring at a missing module error wondering what happened. For Go projects this is less of an issue since the module cache is global, but for Node or Python, plan for it. Linked worktrees can get into a detached HEAD state if you're not careful. If you delete a branch that a worktree is still pointing to without cleaning up first, things get a little weird. More on cleanup below. Cleaning Up When you're done with a worktree, don't just delete the directory. You can, but you should also tell Git...