Using Git Worktrees for Parallel AI Agent Development
Tags: ai, development, tools • Categories: Software
Git worktrees are a lesser-known feature of git that only really became useful as CLI coding tools became really popular. They enable "duplicates" of a git project to exist on your file system that share the same .git database.
This enables stashes, branches, etc to be shared across all worktrees. Since the folders + files are duplicated, this enables you to have different test environments, databases, env config, etc so multiple coding agent sessions can be run in parallel.
However, switching between and creating git worktrees is a pain. The syntax is cumbersome, there are no great CLI completions, and it’s not supported within tools like forgit.
I’ve created a nice CLI tool to make creating & switching between worktrees easier:

Cursor’s new agent view basically wraps worktrees. They’ve done an amazing job overall with their product, but I’m more bullish on CLI coding agents winning for pro users over the long term. Optimizing your cli-based workflow is going to have big gains as agent modalities continue to iteratively improve, so I’m still very bullish on incrementally improving CLI tooling both for yourself and agents to use.
Here’s the gwt tool:
# Usage:
#
# gwt # List and switch to existing worktree via fzf
# gwt <branch> # Create new worktree for <branch> (creates branch if not exists) in $GWT_BASE/<project>/<branch>, then cd to it
#
# Environment variables:
# - GWT_BASE: Customize base dir for worktrees (default: ~/Projects/worktrees)
gwt() {
local git_path=$(command -v git)
if [[ -z "$git_path" ]]; then
echo "git not found in PATH" >&2
return 1
fi
if ! "$git_path" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Not in a git repo" >&2
return 1
fi
local main_dir=$("$git_path" worktree list | head -1 | awk '{print $1}')
if [[ -z "$main_dir" ]]; then
echo "Failed to get main worktree dir" >&2
return 1
fi
local project_name=${main_dir:t}
local common_git_dir=$("$git_path" rev-parse --git-common-dir)
common_git_dir=${common_git_dir:A}
local base_dir=${GWT_BASE:-~/Projects/worktrees}
get_worktree_for_branch() {
local target_branch=$1
local current_path
while IFS= read -r line; do
if [[ $line == "worktree "* ]]; then
current_path=${line#worktree }
elif [[ $line == "branch refs/heads/$target_branch" ]]; then
echo "$current_path"
return 0
elif [[ -z $line ]]; then
current_path=
fi
done < <("$git_path" worktree list --porcelain)
return 1
}
if (( $# == 0 )); then
local gray=$'\033[90m'
local reset=$'\033[0m'
local green=$'\033[32m'
local lines=()
local current_path current_branch ts date_str file wt_name
while IFS= read -r line; do
if [[ $line == "worktree "* ]]; then
current_path=${line#worktree }
elif [[ $line == "branch "* ]]; then
current_branch=${line#branch refs/heads/}
elif [[ $line == "detached" ]]; then
current_branch="detached"
elif [[ $line == "bare" ]]; then
current_branch="bare"
elif [[ -z $line && -n $current_path ]]; then
if [[ -z $current_branch ]]; then
current_branch="unknown"
fi
# Get the last commit date from git log
date_str=$("$git_path" -C "$current_path" log -1 --format=%cd --date=format:%Y-%m-%d 2>/dev/null) || date_str="unknown"
lines+=("${current_path} ${gray}(${current_branch})${reset} ${green}[${date_str}]${reset}")
current_path= current_branch=
fi
done < <("$git_path" worktree list --porcelain)
local selected=$(printf '%s\n' "${lines[@]}" | fzf --ansi --height 40% --border)
if [[ -n "$selected" ]]; then
local path="${selected%% *}"
cd "$path" || return 1
fi
else
local branch=$1
local existing_path=$(get_worktree_for_branch "$branch")
if [[ -n "$existing_path" ]]; then
cd "$existing_path" || return 1
else
local path=$base_dir/"$project_name"/"$branch"
/bin/mkdir -p ${path:h}
if [[ $? -ne 0 ]]; then
echo "Failed to create parent dir" >&2
return 1
fi
if "$git_path" rev-parse --verify "$branch" >/dev/null 2>&1; then
"$git_path" worktree add "$path" "$branch"
else
"$git_path" worktree add -b "$branch" "$path"
fi
if [[ $? -ne 0 ]]; then
echo "Failed to add worktree" >&2
return 1
fi
cd "$path" || return 1
fi
fi
}
Was this vibe coded? Absolutely! This is an amazing example of where vibe coding really shines. Small, sharp, specific tools with clear and simple testable outputs that incrementally improve your productivity.
Some things I’d like to add in the future:
- Cleanup operation to delete really old unused worktrees.
inithook that enables a script to be run when a new worktree is created. For most projects, there is local config that is hidden from git that needs to be setup for each project folder.
I did need to patch the gcb shortcut of forgit to play nicely with git worktrees.
How I Use Git Worktrees for CLI Agent Development
I’ve found worktrees really helpful while working on railpack:
- There are very distinct product areas (different providers/bundlers for each language) whose codepaths do not collide with each other. Worktrees, and therefore parallel agents, aren’t great when there’s a lot of codepath overlap.
- I maintain a general-sounding workspace name (
python-workspace,mise-upgrade, etc) that i can quickly switch to when I’m working on a change related to that section of the codebase. This has paired with well zoxide enabling me to easily switch to the worktree dir without remembering the path. - This has worked well with Claude Code Web. I can work on many potential changes in Claude Code Web in parallel, pull changes into the related workspace using
gh pr checkout 376, "de-slop" the AI work, make any final tweaks, and push any changes. Overall, I really like this workflow. It’s felt really productive. - I’ve found that having multiple tabs in Ghostty where each tab has a two terminals (an agent and a regular terminal) has been a great way to work on multiple changes in parallel. I can switch through each tab, responding to any required input on that session. Using tools like yazi, micro, git-fuzzy, aiautocommit, etc to inspect and commit agent changes quickly feels very fast compared to VS Code / Cursor changes.
Using worktrees for traditional web app projects is more challenging. Most of my projects at this point use my fastapi + react router project template. I’ve found that agents work best when they have a full dev server and test services available to work with, but the golden path for full stack applications does not make this easy. Here are some sharp edges I’m still working on solving:
- Separate https domains for each worktree dynamically without any config required for each worktree.
- Separate database + redis instances for each worktree (or same instance, but separate database name).
- Instruct agents how to access the dev server / domain without manually editing instructions for each worktree.
- A bit unrelated, but I need to do some additional work to allow concurrent test runs within a single worktree. This includes parallel runs within a single
pytestinvocation (using xdist). Database cleaning ends up wiping out other runs fixtures.