What Are Git Worktrees?
Git worktrees let you have multiple branches checked out simultaneously in different directories. Instead of stashing or committing work-in-progress to switch branches, you just cd into another folder.
With a normal git workflow, your repository can only have one branch checked out at a time. With worktrees, each branch lives in its own directory, but they all share the same git history. Commits, stashes, and remotes are shared across all worktrees.
Two Approaches to Worktrees
Approach A: Add Worktrees to an Existing Repo
You keep your current clone and add worktrees beside it. Quick to start, but the main worktree is a regular clone with a .git folder, making it structurally different from the others.
Approach B: Bare Clone + Worktrees (Recommended)
You create a bare clone (no working directory) and then add worktrees for every branch, including main. This is the proper worktree workflow: all branches are equal, and the bare repo is just a shared git database.
We use Approach B because it gives you a clean, symmetric structure where no branch is special.
Setting Up a Bare Clone
Step 1: Create the Bare Clone
The wrapper folder replaces your regular clone, so use the same project name you have always used:
cd ~/Work/my-org
mv my-project my-project-old
mkdir my-project
cd my-project
git clone --bare <repo-url> .bareYou might be tempted to add a suffix like -wt or .git to distinguish it from a regular clone. Don't. Once you commit to worktrees, this is your repo. A suffix just adds noise you will type every day.
Step 2: Set Up the .git Pointer
echo "gitdir: ./.bare" > .gitThis creates a .git file (not directory) that tells git the actual repo is in .bare. Git commands now work from the wrapper folder.
Step 3: Configure Remote Fetch Refs
By default, a bare clone does not fetch remote branch refs properly. Fix this:
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"Step 4: Fetch Everything
git fetch originNow you have the full repo history and can create worktrees.
Creating and Managing Worktrees
Create your first worktree for the main branch:
git worktree add mainThis creates a main/ directory with the branch checked out. For feature branches:
# Existing branch
git worktree add feature-branch
# New branch
git worktree add -b my-new-feature my-new-featureTo list, remove, and clean up:
# List all worktrees
git worktree list
# Remove a worktree (keeps the branch)
git worktree remove feature-branch
# Delete the branch too
git branch -d feature-branch
# Clean stale references
git worktree pruneThe Directory Structure
After setup, your project looks like this:
my-project/ # Wrapper (you never work here directly)
├── .bare/ # Git database (shared by all worktrees)
├── .git # File pointing to .bare
├── .shared/ # Gitignored files symlinked into worktrees
│ └── .env
├── new-worktree.sh # Helper script
├── main/ # Worktree: main branch (keep clean)
│ ├── .env -> ../.shared/.env
│ ├── src/
│ └── package.json
└── feature-branch/ # Worktree: feature branch (work here)
├── .env -> ../.shared/.env
├── src/
└── package.jsonEach worktree has its own node_modules, build output, and working state. They are fully independent directories. You will need to run npm install in each one.
Lazygit Integration
Lazygit has built-in worktree support. Open it from any worktree directory and it detects the setup automatically.
Finding the Worktrees Panel
The Worktrees tab lives inside the Branches panel:
- Press
3to open the Branches panel - Cycle sub-tabs with
](next) and[(previous): Local Branches → Remotes → Tags → Worktrees
Be aware that the w key is context-dependent: in the Files panel it commits staged files, in the Branches panel it creates a worktree from the selected branch. It does not open a worktrees panel.
Worktree Actions
From the Worktrees tab:
Enter— Switch to the selected worktreen— Create a new worktreed— Remove the selected worktree?— Show all keybindings (works in any panel)
Side-by-Side Diffs with Delta
Lazygit does not have a built-in split diff view, but you can get side-by-side diffs by using delta as a custom pager:
brew install git-deltaThen create the lazygit config (macOS: ~/Library/Application Support/lazygit/config.yml, Linux: ~/.config/lazygit/config.yml):
git:
paging:
colorArg: always
pager: delta --dark --paging=never --side-by-sideAll diffs in lazygit now display side-by-side with syntax highlighting. Press e in any diff view to open the file in your $EDITOR.
Non-US Keyboard Layouts
The [ and ] keys for tab cycling can be hard to reach on non-US keyboards:
- German Mac:
[=Option+5,]=Option+6 - German Windows/Linux:
[=AltGr+8,]=AltGr+9
Yazi Integration
Yazi is a terminal file manager that pairs well with worktrees. Since all branches are sibling directories, you can browse between them visually.
# Open yazi in the wrapper folder
cd my-project
yaziTab-Based Workflow
The recommended setup is one yazi tab per active worktree:
- Open yazi in the wrapper folder
- Enter
main/→ presstto open in a new tab - Go back with
h, enterfeature-x/→ presstagain - Switch between tabs with
1and2 - Close a tab with
Ctrl+c
Running Commands from Yazi
;— Run a command in the background (non-blocking, great fornpm start):— Run a command in the foreground (blocking, great fornpm install)w— Open the task manager to see background tasksEnter(in task manager) — View task logsx(in task manager) — Cancel a taskq(in task manager) — Back to yazi
This means you can start a dev server with ;, type npm start, and keep browsing files while it runs.
Handling Gitignored Files Across Worktrees
This is a critical detail that catches people off guard. Gitignored files are not shared between worktrees. Each worktree is its own directory on disk. Files like .env, IDE configs, or tool settings only exist in the worktree where you created them.
The Symlink Solution
Keep shared gitignored files in a .shared/ directory in the wrapper folder, then symlink them into each worktree:
mkdir .shared
cp main/.env .shared/.env
# Symlink into existing worktrees
ln -s "$(pwd)/.shared/.env" main/.env
ln -s "$(pwd)/.shared/.env" feature-branch/.envOne source of truth, and every worktree sees the same file. Editing in one worktree changes it everywhere because they all point to the same location.
Share config and tooling (.env, .editorconfig, tool settings). Keep separate generated files (node_modules, dist, build caches).
The Trailing Slash Gotcha
If your .gitignore uses a trailing slash to ignore directories:
.myconfig/This will not match symlinks to directories. Git treats symlinks as files, so .myconfig/ only matches a real directory. The symlink shows up as untracked.
Fix: remove the trailing slash:
.myconfigThis matches directories, files, and symlinks. Review your global gitignore and remove trailing slashes from any pattern that might be symlinked.
Automating with a Helper Script
Creating symlinks manually gets tedious. Place a helper script in the wrapper folder:
#!/usr/bin/env bash
set -euo pipefail
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
echo "Usage: ./new-worktree.sh <branch-name> [--new]"
echo ""
echo " <branch-name> Checkout existing branch"
echo " <branch-name> --new Create new branch and worktree"
echo ""
echo "All files in .shared/ are symlinked into new worktrees."
exit 0
fi
BRANCH_NAME="$1"
CREATE_NEW="${2:-}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SHARED_DIR="$SCRIPT_DIR/.shared"
WORKTREE_DIR="$SCRIPT_DIR/$BRANCH_NAME"
if [ "$CREATE_NEW" = "--new" ]; then
git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR"
else
git worktree add "$WORKTREE_DIR" "$BRANCH_NAME"
fi
if [ -d "$SHARED_DIR" ]; then
for item in "$SHARED_DIR"/.[!.]* "$SHARED_DIR"/*; do
[ -e "$item" ] || continue
name="$(basename "$item")"
target="$WORKTREE_DIR/$name"
if [ ! -e "$target" ]; then
ln -s "$item" "$target"
echo " Linked: $name"
else
echo " Skipped (exists): $name"
fi
done
fi
echo "Worktree ready: $WORKTREE_DIR"Now every new worktree automatically gets all shared files symlinked in.
Daily Workflow
Keep your main worktree clean as a stable baseline. Do actual work in feature worktrees:
main/ ← keep clean, pull here, branch from here
feature/something/ ← do actual work here
feature/other/ ← another piece of work hereStarting a New Feature
./new-worktree.sh feature/my-feature --new
cd feature/my-feature
npm install
npm startSwitching Context
No stashing, no committing WIP. Just:
# Terminal
cd ../main
# Lazygit: Branches panel (3) → Worktrees tab (]) → Enter
# Yazi: navigate to sibling directoryCode Review
git fetch origin
git worktree add pr-review origin/someones-branch
cd pr-review
npm install && npm start
# Review, test, done
git worktree remove pr-reviewTips and Gotchas
- Same branch rule: You cannot have two worktrees on the same branch.
- Shared git state: Commits and stashes are shared. A commit in one worktree is visible from all others.
- Separate dependencies: Each worktree needs its own
npm install. - IDE handling: Open your IDE in individual worktree folders, not the wrapper. VS Code handles the
.gitfile well; some other IDEs may need configuration. - Cleanup: Remove worktrees you are done with. Run
git worktree pruneperiodically to clean stale references.
