Skip to main content

git worktree: Work on Two Branches at the Same Time

git worktree lets you check out multiple branches as real directories simultaneously — no stashing, no context loss. Keep your Docker stack running while handling hotfixes in parallel.

· 10 min read
View raw
Christophe
Markdown, WSL and Docker lover ~ Insatiable curious.
Claude Code
AI Assistant
Cet article existe aussi en français — lire la version française.

Changelog

Latest
added a Docker-first demo of the hotfix scenario

git worktree: Work on Two Branches at the Same Time

⚡
TL;DR

You're deep in a feature branch — half-written code, running Docker containers, open files everywhere. Then: "urgent hotfix needed on main." With the usual workflow, you stash everything, switch branches, fix, commit, push, then unstash and hope nothing explodes. With git worktree, you check out a second branch into a separate folder. Both branches exist simultaneously, as real directories, with their own terminals and their own Docker stacks.

It's 3pm on a Thursday. You've been working on a new feature for two days. The Docker containers are running, your editor has eight files open, and you're in the middle of a refactor that's not compiling yet.

Your phone buzzes. Production is broken. A fix needs to go out on main in the next thirty minutes.

What do you do?

The stash workflow and its pain​

The classic approach:

>user@machine: ~/yourproject

It works. Most of the time. But:

  • Your Docker containers are now pointing at the wrong codebase — you need to restart them
  • Your editor has lost context — all those open files are gone or pointing at main's versions
  • If git stash pop has a conflict, you now have a three-way mess to resolve while still stressed from the hotfix
  • You can only have one active branch at a time

The deeper issue: a git stash is fragile state. It's not a branch. It has no history. It can conflict. And it disappears once you pop it.

Since we're talking about juggling branches: Git - Some tips for your .gitconfig file shows how to sort git branch by last commit date, and Showing the last 3 updated branches when you jump in a git repo displays them automatically as you enter a repository.

What git worktree does​

A git worktree is a second (or third, or fourth) working directory linked to the same git repository.

Instead of one folder with one checked-out branch, you get:

~/projects/my-blog/ ← main branch (original)
~/projects/my-blog-hotfix/ ← hotfix/invoice-fix branch (worktree)
~/projects/my-blog-feature-auth/ ← feature/auth branch (worktree)

Each folder is a real directory with its own files, its own terminal, its own running processes. They share the same .git database (commits, objects, history), but each has an independently checked-out branch.

You never leave your feature branch. The hotfix lives next door.

The basic commands​

Add a worktree​

>user@machine: ~/yourproject

Create a new directory at <path> with [branch] checked out:

>user@machine: ~/yourproject

If the branch doesn't exist yet, create it at the same time:

>user@machine: ~/yourproject

This creates hotfix/invoice-fix branching off main, checked out into ../my-blog-hotfix.

List all worktrees​

>user@machine: ~/yourproject

Output:

/home/christophe/projects/my-blog a4b2c1d [main]
/home/christophe/projects/my-blog-hotfix e8f3d2a [hotfix/invoice-fix]

Remove a worktree​

>user@machine: ~/yourproject

This deletes the folder and unregisters the worktree. The branch itself (hotfix/invoice-fix) still exists — you can delete it separately with git branch -d.

Seeing It in Action with Docker​

git needs no installing — it's already on your machine. What's worth trying risk-free is the scenario itself: a throwaway repo, already mid-refactor on a feature branch with uncommitted changes, and a VAT calculation bug waiting on main. Exactly the Thursday-afternoon setup below, minus the stress.

Why Docker first?

The Dockerfile below builds a repo already sitting on feature/user-notifications with two unstaged files — the same starting point as step 1 below — plus the gwt fzf navigator from further down, wired via real ZSH autoload. Nothing on your own repos is touched.

Build and run it:

>user@machine: ~/worktree-demo

$ docker build -t worktree-demo . [+] Building 24.1s (8/8) FINISHED ✔ exporting to image

$ docker run --rm -it worktree-demo 🐳 root ~/projects/my-blog # git status On branch feature/user-notifications Changes not staged for commit: modified: src/api/notifications.ts modified: src/components/Notifications.tsx

That's step 1 below, already set up. Now run the rest of the scenario for real, exactly as written — git worktree add -b hotfix/invoice-fix ../my-blog-hotfix main, fix src/billing/invoice.ts in the new folder, commit, git worktree remove, and confirm with git status that your feature branch never moved. gwt is wired too, if you want to try the fzf navigator from the "ZSH function" section further down.

Run step 4's commands one at a time

Don't paste the whole block from step 4 in one shot: vim is a full-screen editor, so anything you paste after vim src/billing/invoice.ts gets typed into vim instead of run as shell commands. Run that line alone, make the edit (see the tip in step 4), save and quit, then paste the rest. git push works in this container too — the image wires a local bare repo as origin so there's something real to push to, no GitHub account needed.

The hotfix scenario, step by step​

Back to Thursday afternoon. Here's the same situation handled with worktrees.

1. You're on your feature branch, in the middle of work​

>user@machine: ~/yourproject

You don't touch this. You don't stash anything.

2. Create a hotfix worktree next to your project​

>user@machine: ~/yourproject

git worktree add -b hotfix/invoice-fix ../my-blog-hotfix main

>user@machine: ~/yourproject

Preparing worktree (new branch 'hotfix/invoice-fix') HEAD is now at a4b2c1d chore: update dependencies

3. Open a new terminal, cd into the hotfix folder​

>user@machine: ~/yourproject

This directory has main's files. Your feature branch directory is untouched, still running, still open in your editor.

4. Fix the bug and push​

>user@machine: ~/yourproject

The actual VAT fix

calculateVat() returns amount * rate — just the VAT portion, not the invoice total. In vim: press i, change the line to return amount * (1 + rate);, then Esc followed by :wq to save and exit. Only then run the git add/commit/push lines.

5. Clean up the worktree​

>user@machine: ~/yourproject

Why -D and not -d

You're still on feature/user-notifications here, which never received the hotfix commit — only origin/hotfix/invoice-fix did, via the merged PR. git branch -d refuses to delete a branch it can't see as merged into your current branch, and errors with "not fully merged". Since you know it's safely merged on the remote, -D (force) is the correct call, not a workaround for a mistake.

6. Back to your feature branch — nothing changed​

>user@machine: ~/yourproject

No stash, no conflicts, no context switch.

One constraint: a branch can only be in one worktree at a time​

Git enforces this. If feature/user-notifications is checked out in your main directory, you cannot add a second worktree with the same branch. Attempting it gives:

>user@machine: ~/yourproject

This is intentional — two directories writing to the same branch simultaneously would corrupt the working tree. Each worktree gets its own branch.

Docker + worktrees: each branch gets its own stack​

This is where worktrees become genuinely powerful for a Docker workflow.

Each worktree is a separate directory. Each directory can have its own .env file and its own running Docker Compose stack, as long as you avoid port conflicts.

A practical convention: use a different port per worktree. Keep the ports in .env, which is .gitignored:

>user@machine: ~/yourproject

Now you can run both stacks simultaneously:

>user@machine: ~/yourproject

// Terminal 1 — feature branch cd ~/projects/my-blog docker compose up

// Terminal 2 — hotfix branch cd ~/projects/my-blog-hotfix docker compose up

Both servers are running at the same time. You can test the hotfix at http://localhost:3001 without stopping your feature development at http://localhost:3000.

Named Compose projects

Docker Compose uses the folder name as the default project name. Since your worktrees are in different folders, Compose creates separate networks and container sets automatically — no additional configuration needed.

A ZSH function to navigate between worktrees​

If you follow the modular ZSH workflow pattern, you can add an interactive worktree navigator using fzf:

Usage:

gwt # fzf picker — select any active worktree and cd into it
gwt feature/dark-mode # create + cd into a new worktree for that branch

Practical tips​

Name worktrees consistently​

A predictable naming scheme avoids confusion:

# Pattern: <repo-name>-<branch-slug>
git worktree add ../my-blog-hotfix-invoice hotfix/invoice-fix
git worktree add ../my-blog-feat-auth feature/auth

Worktrees work with VS Code​

Open a worktree folder in a new VS Code window:

>user@machine: ~/yourproject

Each window operates independently, with its own open files and terminal sessions.

Prune stale worktrees​

If you manually delete a worktree folder (instead of using git worktree remove), git may still think it exists. Clean up the stale references:

>user@machine: ~/yourproject

Conclusion​

git stashgit worktree
Branch switchYes, requiredNo — branches coexist
Docker containersMust restartRun independently
Editor contextLostPreserved
Parallel developmentNoYes
Conflict riskOn stash popNone
CleanupAutomatic on popManual worktree remove

Stash remains useful for tiny, quick things — saving half a line of typing before a git pull. For anything that involves running services, an editor session, or more than five minutes of work, a worktree is cleaner.

The first time you handle a hotfix without leaving your feature branch, you'll wonder how you ever worked any other way.


🦋 Join the conversation on BlueskyRequires a free Bluesky account to like, repost or comment. This block talks to Bluesky's public API directly from your browser (no cookies, no sign-in) — that means your IP address reaches Bluesky's servers just by this section loading.

Loading comments…

Was this article helpful?
Did you try the steps in this article?