All Articles

Git Worktrees: A Practical Guide with Lazygit and Yazi

GitTutorialDevTools
Git Worktrees: A Practical Guide with Lazygit and Yazi

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> .bare

You 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" > .git

This 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 origin

Now 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 main

This 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-feature

To 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 prune

The 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.json

Each 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:

  1. Press 3 to open the Branches panel
  2. 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 worktree
  • n — Create a new worktree
  • d — 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-delta

Then 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-side

All 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
yazi

Tab-Based Workflow

The recommended setup is one yazi tab per active worktree:

  1. Open yazi in the wrapper folder
  2. Enter main/ → press t to open in a new tab
  3. Go back with h, enter feature-x/ → press t again
  4. Switch between tabs with 1 and 2
  5. Close a tab with Ctrl+c

Running Commands from Yazi

  • ; — Run a command in the background (non-blocking, great for npm start)
  • : — Run a command in the foreground (blocking, great for npm install)
  • w — Open the task manager to see background tasks
  • Enter (in task manager) — View task logs
  • x (in task manager) — Cancel a task
  • q (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/.env

One 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:

.myconfig

This 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 here

Starting a New Feature

./new-worktree.sh feature/my-feature --new
cd feature/my-feature
npm install
npm start

Switching Context

No stashing, no committing WIP. Just:

# Terminal
cd ../main

# Lazygit: Branches panel (3) → Worktrees tab (]) → Enter
# Yazi: navigate to sibling directory

Code 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-review

Tips 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 .git file well; some other IDEs may need configuration.
  • Cleanup: Remove worktrees you are done with. Run git worktree prune periodically to clean stale references.

Want to learn more?

Let's discuss how these technologies can help your business.