I Made a Lightweight Git Worktree Manager (Because I Couldn't Find One)

Andy Goldsworthy, Split Oak Wood

Andy Goldsworthy, Split Oak Wood

I built a git worktree manager because I couldn't find a simple one.

The problem hit me when running multiple AI coding agents at once—one was refactoring accessibility code while I needed to review the current implementation for a bug. Same files, different contexts. I couldn't commit the half-done work, couldn't stash and lose the agent's state, couldn't review without a clean checkout.

Git worktrees solved this, but the syntax was awkward. Branchyard exists and is great if you want VS Code integration and git hooks, but I wanted something I could drop in my dotfiles and understand in 20 minutes.

So I built gwt (git-worktree-utils): • ~1K lines of bash, zero dependencies • Source-based, lives in ~/.config • One branch = one directory (keeps the mental model simple) • Built-in cleanup for orphaned directories

Use cases beyond AI agents: • Code review without losing your place • Emergency hotfixes without stashing • Running tests in one branch while coding in another

The pattern works: multiple branches in separate directories beats constant switching. No stashing, IDE state persists, contexts stay separate.

Project: https://github.com/jamesfishwick/git-worktree-utils

I Made a Lightweight Git Worktree Manager (Because I Couldn't Find One)

Why I made gwt (git-worktree-utils) when Branchyard is already there, and why one bash script is all you need.


The Issue

I didn't know I had a problem until I tried running multiple AI coding agents at the same time. Git worktrees are perfect for this.

Traditional git workflow: stash, checkout, fix, checkout back, unstash. Before worktrees (added in Git 2.5, 2015), developers created multiple repository clones to work on different branches simultaneously.

Why? Switching branches loses context—IDE closes files, build artifacts get wiped, dependencies reinstall. Multiple clones meant keeping production ready for hotfixes while developing features, running long builds while coding elsewhere, or comparing branches side-by-side. The downside: disk space, manual syncing, duplicate .git directories.

Worktrees solve this by sharing .git metadata across multiple working directories.

My use case: one AI agent was refactoring my web app's a11y while I reviewed how the current code handled an outdated media player. Same files, different contexts. I couldn't commit the half-done refactor, couldn't stash and lose the agent's state, couldn't review without a clean checkout. Worktrees gave me both contexts live simultaneously.

The pattern works beyond AI agents. Multiple branches in separate directories beats constant switching—no stashing, IDE state persists, contexts stay separate.

But git's syntax is awkward:

git worktree add ../myapp-feature-auth -b feature/auth
# Branch name typed twice, manual -b flag, path calculated by hand

After struggling with the syntax, I looked for tools. Branchyard has VS Code integration, git hooks, auto-cleanup.

Ultimately, it was too heavy for me. I wanted something to drop in my dotfiles. Something I could read and understand completely in 20 minutes.

So I built gwt (git-worktree-utils). Pronounced "guh-wit" (or not).

What It Does

gwt feature/auth   # Create/switch to worktree
gwtlist            # List with status
gwts               # Interactive switcher
gwtclean           # Clean orphaned directories
gwthelp            # Built-in help

Just one file with five commands. Bash and git only.

When to Use Worktrees

Worktrees aren't a replacement for branches—they're for when you need physical separation, not just logical.

Code review: Stash or WIP commit, checkout PR branch, review, checkout back, unstash. Your IDE closes files, loses scroll positions. With worktrees: cd to review directory, cd - back. Everything stays put.

Emergency hotfix: No stashing, no WIP commits. Do a clean checkout of main in a separate directory. Fix, push, cd - back.

Parallel work: Run long tests in one tree while coding in another. Maintain different build artifacts. You get actual parallelism, not task switching.

Lose the mental overhead of tracking stash/WIP state. Keep IDE state intact so your build tools don't get confused.

Design Decisions

Given these use cases, I had specific goals for the implementation.

What it is:

  • ~1K line bash script
  • Lives in ~/.config (XDG-compliant)
  • Checked into dotfiles (if you use that pattern)
  • Cross-platform (macOS, Linux, BSD)
  • Zero dependencies beyond bash and git

What it's not:

  • IDE-integrated
  • Git hook automated
  • Package manager distributed

The 1:1 Constraint

Git worktrees are flexible: same branch in multiple directories, arbitrary naming, complex mappings. This tool enforces one branch = one directory.

Why? It matches how you already think. Traditional git has one directory where you git checkout different branches. This keeps that 1:1 mental model—just makes the "switch" physical instead of logical. Think cd instead of git checkout.

Yes, it's less flexible, but it's easier to reason about. I've found the flexibility unnecessary in practice besides. Tell me why I'm wrong please!

More on Dotfile Integration

I use git to manage my dotfiles—shell config, vim, git aliases. One repo, all config, synchronized across machines. The worktree manager belongs there too.

# In dotfiles repo
.config/git-worktree-utils/git-worktree-utils.sh

# In ~/.zshrc
source ~/.config/git-worktree-utils/git-worktree-utils.sh

Core Features

Smart Worktree Creation

gwt feature/auth

Automatically finds new, local, and remote branches. Creates {base}-{branch} directory. Handles paths with spaces (porcelain format). Sets up your submodules. cds into the directory.

No flags or typing branch names twice.

Configurable Patterns

# ~/.config/git-worktree-utils/config
GWT_DIR_PATTERN="{base}-{branch}"

# Options:
# {base}-{branch}           -> myapp-feature-auth
# {branch}                  -> feature-auth
# worktrees/{base}/{branch} -> worktrees/myapp/feature-auth

Interactive Switcher

$ gwts

Select worktree:
  1) /Users/dev/myapp [CURRENT]
  2) /Users/dev/myapp-feature-auth
  3) /Users/dev/myapp-hotfix-urgent

Enter number (1-3): 2
Switched to /Users/dev/myapp-feature-auth

Cleanup Automation

$ gwtclean

Git Worktree Cleanup
Pruning broken references...
Searching for orphaned directories...

Found 3 orphaned directories:
  ../myapp-feature-old (458M)
  ../myapp-hotfix-merged (12M)
  ../myapp-review-pr-123 (234M)

Delete all? (y/N)

It will show disk space usage, ask for confirmation, and never touch active worktrees.

Why do orphaned directories exist? When you delete a branch that had a worktree, git removes the worktree from its metadata, but the working directory stays on disk. Manual deletions can also leave metadata pointing to nuked directories.

gwtclean runs git worktree prune to clean git's internal state, then scans the parent directory for directories matching your naming pattern. It compares these against active worktrees—anything matching the pattern but not active is orphaned. Run it periodically to reclaim disk space, or simply after deleting feature branches.

Built-in Help

gwthelp             # Overview
gwthelp gwt         # Command details
gwthelp config      # Config reference
gwthelp workflows   # Examples

Workflows

Real-world usage:

Emergency Hotfix

Production breaks while you're deep in a feature branch:

gwt hotfix/critical-security-fix
# Clean environment on main
# Fix, commit, push
cd -  # Back to work on the feature

Code Review

Check a coworker's PR without stopping what you're doing:

gwt pr/123
# Test locally
cd ../myapp
gwtclean  # Remove review worktree

Parallel Approaches

Try two solutions at the same time and compare them:

gwt approach-a
gwt approach-b
diff -r ../myapp-approach-a ../myapp-approach-b
# Keep winner, clean loser

Technical Details

For those who want to know a little more how this works behind the scenes:

Porcelain Format

Git's default output isn't parseable. Paths can have spaces. Branch names can have newlines.

while IFS= read -r line || [[ -n "$line" ]]; do
    case "$line" in
        worktree\ *)
            path=${line#worktree }
            ;;
        branch\ *)
            branch=${line#branch }
            ;;
    esac
done < <(git worktree list --porcelain)

Handles edge cases correctly.

XDG Compliance

GWT_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
GWT_CONFIG_DIR="${GWT_CONFIG_HOME}/git-worktree-utils"

Respects XDG_CONFIG_HOME, falls back to ~/.config.

Platform Detection

macOS uses BSD stat while Linux uses GNU stat, so different flags need to be used.

case "$(uname -s)" in
    Darwin)
        stat -f "%m %N" "$path"
        ;;
    Linux)
        stat -c "%Y %n" "$path"
        ;;
    *)
        # Fallback
        find "$dir" -mindepth 1 -maxdepth 1 -print
        ;;
esac

Namespace Convention

All helpers prefixed _gwt_. Clear, hopefully no conflicts.

_gwt_print()                   # Colored output
_gwt_get_worktree_paths()      # Parse paths
_gwt_list_recent()             # File listing
_gwt_display_worktree_info()   # Info display

Install

# One-line
curl -fsSL https://raw.githubusercontent.com/jamesfishwick/git-worktree-utils/main/install.sh | bash

# Or inspect first
curl -fsSL https://raw.githubusercontent.com/jamesfishwick/git-worktree-utils/main/install.sh -o install.sh
chmod +x install.sh
./install.sh

Project: https://github.com/jamesfishwick/git-worktree-utils License: MIT