Field notes: git worktree pattern
this isn't a bug report — instead it's a pattern I've been working with claude for a few weeks, to the extent I thought it would be worth sharing. I now use it for basically all my development where I can use claude code.
the main advantage is that it allows running an arbitrary number of claude instances without overhead, by encapsulating the main git worktree operations.
fwiw I also used claude code to help write the functions, and to summarize it into an issue here. no need to keep the issue open
Git worktree workflow
The overall workflow I use is:
- Start a new feature with
git worktree-llm feature-name - Work on the feature with Claude's assistance
- When ready to merge, use
git worktree-merge - The changes are committed, the worktree is closed, and changes are rebased and merged
This creates a clean, efficient workflow for feature development with AI assistance and handles all the git operations to create a clean history.
git worktree-llm
This function creates a git worktree and immediately starts an LLM session. It's a streamlined workflow for feature development with AI assistance:
function git worktree-llm -d "Create a git worktree and start claude"
# Create the worktree, then run setup in background and start claude
if git worktree-create $argv
task setup-worktree </dev/null >/dev/null 2>&1 & disown
claude
end
end
The function:
- Creates a new worktree with
git worktree-createpassing through any arguments - Runs a background task to set up the worktree (
task setup-worktree). This is a task that each project can define which does things like:
- install dependencies;
uv sync - starts daemon processes;
uv run dmypy run --timeout=3600 - add claude configs, like
allowedTools, which didn't have global configs
- Immediately launches
claude(without waiting for the background task to finish)
(We still need to hit return twice, to approve MCPs and approve loading a CLAUDE.md from a parent directory. Hopefully these could be eliminated by claude in the future.)
Work
Then we do work!
When finished:
git worktree-merge
This function finishes work on a branch and merges it into the main branch:
function git worktree-merge --description "Finish worktree and merge branch into main"
# Save the branch name before finishing the worktree
set branch (git branch --show-current)
set default_branch (git default-branch)
# Skip if already on default branch (let git worktree-finish handle this normally)
if test "$branch" = "$default_branch"
return 0
end
# Add all changes first
git add .
# Finish the worktree first
git worktree-finish; or return
# Rebase branch onto default branch first
set_color cyan
echo "⚙️ Rebasing $branch onto $default_branch"
set_color normal
git rebase $default_branch $branch; or return
git switch $default_branch; or return
# Now merge the branch (don't delete it)
set_color cyan
echo "🔄 Merging $branch into $default_branch"
set_color normal
git merge --no-edit $branch; or return
end
The function:
- Captures the current branch name
- Adds all changes to the git index
- Finishes the worktree (returning to the main repository)
- Rebases the feature branch onto the default branch
- Switches to the default branch
- Merges the feature branch without prompting for a commit message
Supporting Functions
These main functions rely on several supporting functions:
git worktree-create
Creates a new worktree for a branch and navigates to it, either creating a new branch or using an existing one.
Implementation (click to expand)
function git worktree-create -d "Create a new git worktree and navigate to it"
set -l branch_name $argv[1]
# Try to switch to existing worktree or create a new one
# Silence errors because we'll handle branch creation below
if git worktree-switch $branch_name 2>/dev/null
# If success and already in worktree, just return
return 0
end
# No existing worktree, create one
set -l git_common_dir (git rev-parse --git-common-dir)
set -l repo_root (dirname $git_common_dir)
set -l worktree_path "$repo_root/.worktrees/$branch_name"
# Check if branch exists
if git show-ref --verify --quiet refs/heads/"$branch_name"
# Branch exists, don't use -b flag
git worktree add "$worktree_path" "$branch_name"
else
# Branch doesn't exist, create it
git worktree add "$worktree_path" -b "$branch_name"
end &&
cd "$worktree_path"
end
git worktree-switch
Navigates to an existing worktree by branch name or creates it if the branch exists but doesn't have a worktree.
Implementation (click to expand)
function git worktree-switch --description "Navigate to a git worktree by branch name, creating it if needed"
if test (count $argv) -eq 0
echo "Error: Branch name required." >&2
return 1
end
set -l branch $argv[1]
# Check if inside a git repository
if not git rev-parse --is-inside-work-tree >/dev/null 2>&1
echo "Error: Not in a git repository." >&2
return 1
end
# Try to find existing worktree with this branch
set -l current_path ""
for line in (string split \n (git worktree list --porcelain | string collect))
if string match -q "worktree *" $line
set current_path (string replace "worktree " "" $line)
else if string match -q "branch refs/heads/$branch" $line
# Found matching branch worktree
cd $current_path
set_color green; echo -n "Switched to worktree for "; set_color --bold yellow; echo "$branch"; set_color normal
return 0
end
end
# No worktree found - check if branch exists and create if so
if git show-ref --verify --quiet refs/heads/$branch
# Always use the common git directory's parent as the repository root
# This works reliably whether we're in the main repo or a worktree
set -l git_common_dir (git rev-parse --git-common-dir)
set -l repo_root (dirname $git_common_dir)
# Use absolute path for the new worktree
set -l worktree_path "$repo_root/.worktrees/$branch"
if git worktree add "$worktree_path" "$branch"
cd "$worktree_path"
set_color green; echo -n "Created and switched to worktree for "; set_color --bold yellow; echo "$branch"; set_color normal
return 0
else
echo "Error: Failed to create worktree for branch '$branch'." >&2
return 1
end
else
echo "Error: No worktree found for branch '$branch' and branch does not exist." >&2
return 1
end
end
git worktree-finish
Completes work on a worktree by committing any changes, returning to the main repository, and removing the worktree.
Implementation (click to expand)
function git worktree-finish --description "Finish work on a git worktree or branch without merging"
git diff --quiet HEAD || git commit-llm || return 1
# Save current branch and path for possible future use
set branch (git branch --show-current)
set default_branch (git default-branch)
set current_path (pwd)
set_color cyan
echo "🔀 Branch: $branch → $default_branch"
set_color normal
# Skip if already on default branch
test "$branch" = "$default_branch" && return 0
# Move back to main repo and remove worktree
set git_dir (git rev-parse --git-dir)
set common_dir (git rev-parse --git-common-dir)
if test "$git_dir" != "$common_dir"
# In worktree: go to main repo, remove worktree
set_color cyan
echo "📂 Moving to main repo"
set_color normal
cd (git rev-parse --git-common-dir)/..
git checkout $default_branch
set_color cyan
echo "🗑️ Removing worktree at $current_path"
set_color normal
git worktree remove "$current_path" 2>/dev/null
if test $status -ne 0
set_color yellow
echo "⚠️ Could not remove worktree at $current_path"
set_color normal
end
else if git checkout $default_branch 2>/dev/null
# Regular branch with available default branch, just switch
end
# Return the branch name as status so it can be captured
return 0
end
git commit-llm
Generates a commit message using an LLM based on the staged changes, providing intelligent commit messages.
Implementation (click to expand)
function git commit-llm -d "Create a commit with an LLM-generated message"
# Only add everything if there's nothing in the index
if test -z "$(git diff --cached --name-only)"
git add -A
end
# Get branch name for context
set branch_name (git branch --show-current)
set context "Write a concise, clear git commit message for branch '$branch_name'"
# Generate message using shared function and preserve newlines using -z
git llm-message "$context" | read -z msg || return 1
# Commit with the generated message
git commit -m "$msg
Co-authored-by: Claude <[email protected]>"
end
git llm-message
Uses structured git diff and branch data to generate a well-formatted commit message using an LLM.
Implementation (click to expand)
function git llm-message -d "Generate a commit message using LLM from diff and provided custom instruction"
# This function uses an XML format to structure data for the LLM:
# - <git-info> contains branch and recent commit metadata
# - <git-diff> contains the staged changes
# The LLM uses this structured format to generate a well-formatted commit message.
argparse debug -- $argv
or return # Exit if argparse finds an invalid option
# Show informative message
set files_count (git diff --cached --name-only | count)
set stat_summary (git diff --cached --shortstat)
# IMPORTANT: Using inline color codes in echo statements that are already
# redirected to stderr ensures that color codes never contaminate stdout
echo (set_color cyan)"📝 Processing $files_count files. $stat_summary. Generating commit message..."(set_color normal) >&2
# Create a temporary file that will be automatically cleaned up when the function exits
set --local input_file (mktemp)
# Ensure temp file is removed when function exits (unless --debug is passed)
if set -q _flag_debug
# When debugging, tell the user where to find the temp file
echo (set_color yellow)"=== Debug: Temporary file will be preserved at: $input_file ==="(set_color normal) >&2
else
function __cleanup --on-event fish_exit --inherit-variable input_file
rm -f $input_file
end
end
# Set default system instruction if empty
# $argv now contains non-option arguments after argparse processes them.
set --local actual_system_instruction
if test (count $argv) -gt 0
set actual_system_instruction $argv[1] # Use the first non-option arg as system instruction
end
set --local user_instruction (test -n "$actual_system_instruction" && echo "$actual_system_instruction" || echo "Write a concise, clear git commit message based on the provided diff.")
# ----- Prepare LLM Input File (Instructions + Git Info + Diff) -----
# Write detailed instructions (formerly part of system prompt) to input_file
printf "%s\n" "Format
- First line: <50 chars, present tense, describes WHAT and WHY (not HOW).
- Blank line after first line.
- Optional details with proper line breaks explaining context. Commits with more substantial changes should have more details.
- Return ONLY the formatted message without quotes, code blocks, or preamble.
Style
- Do not give normative statements or otherwise speculate on why the change was made.
- Broadly match the style of the previous commit messages.
- For example, if they're in conventional commit format, use conventional commits; if they're not, don't use conventional commits.
The context contains:
- <git-info> with branch name and recent commit messages. Recent commit messages should be used for style matching ONLY.
- <git-diff> with the staged changes. This is the ONLY content you should base your message on.
---
The following is the context for your task:
---
" >$input_file
# printf "<git-diff>\n```diff" >>$input_file
printf "<git-diff>\n```" >>$input_file
git --no-pager diff --staged >>$input_file
printf "\n```\n</git-diff>\n" >>$input_file
# Add git context information
printf "<git-info>\n" >>$input_file
# Try to get current branch name
set current_branch_name (git rev-parse --abbrev-ref HEAD 2>/dev/null)
if test $status -eq 0; and test -n "$current_branch_name"
printf " <current-branch>%s</current-branch>\n" $current_branch_name >>$input_file
end
# Try to get recent commit messages
set recent_commits_list (git log --pretty='format:%s' -n 5 --no-merges 2>/dev/null)
if test $status -eq 0; and test (count $recent_commits_list) -gt 0
printf " <previous-commit-messages>\n" >>$input_file
for commit_msg in $recent_commits_list
printf " <previous-commit-message>%s</previous-commit-message>\n" $commit_msg >>$input_file
end
printf " </previous-commit-messages>\n" >>$input_file
end
printf "</git-info>\n\n" >>$input_file
# Debug output if requested
if set -q _flag_debug
set input_size (stat -f %z $input_file)
echo (set_color yellow)"=== Debug: Temporary Files ==="(set_color normal) >&2
echo "Input file (instructions + git info + diff): $input_file ($input_size bytes)" >&2
echo (set_color yellow)"=== Debug: System Instruction (passed directly to LLM) ==="(set_color normal) >&2
echo "$user_instruction" >&2
echo (set_color yellow)"=== Debug: Input File ==="(set_color normal) >&2
cat $input_file >&2
end
# Execute the command (will be traced if in debug mode)
# Enable command tracing if debug mode is active
if set -q _flag_debug
echo (set_color yellow)"=== Debug: Executing LLM Command ==="(set_color normal) >&2
# Set local fish_trace that will automatically go out of scope
set -l fish_trace 1
end
cat $input_file | llm prompt -m gemini-2.5-flash-preview-04-17 \
-o temperature 0 \
-o google_search 0 \
-o thinking_budget 0 \
--no-log \
--system "$user_instruction" \
| read -z msg \
|| return 1
# Show generated message to user with colors
# Keep all color codes on stderr so they never contaminate stdout
echo (set_color green)"🧠 Generated message: \"$msg\""(set_color normal) >&2
# Return just the message to stdout for piping to other tools
# No color code cleanup needed because we never let them touch stdout
printf "%s" "$msg"
end
git default-branch
Function to get the default branch name (main or master) for the current repository.
Implementation (click to expand)
function git default-branch --description "Get the default branch (main or master) for this repo"
# Try to get the default branch from the origin remote
# This handles cases where the repo uses a non-standard default branch name
set -l remote_default_branch (git remote show origin 2>/dev/null | grep "HEAD branch" | sed 's/.*: //')
if test -n "$remote_default_branch"
echo $remote_default_branch
return 0
end
# If we can't get it from remote, check if main or master exists locally
if git show-ref --verify --quiet refs/heads/main
echo "main"
else if git show-ref --verify --quiet refs/heads/master
echo "master"
else
# Default to main if we can't determine
echo "main"
end
end
@max-sixty smart
@max-sixty thanks for sharing this!
How do you deal with the problem that each new worktree is a new project from claude code point of view and it asks for confirmations initially?
How do you deal with the problem that each new worktree is a new project from claude code point of view and it asks for confirmations initially?
I have a very expansive allowedTools in my user configs
I previously had a claude config set... command run as part of the script that starts a new worktree. the user configs can sometimes be a bit prickly so that's still a viable option if you get issues
(...and fwiw I'm also developing something to run in containers to avoid any worry about yolo-ing)
I have a very expansive allowedTools in my user configs
Won't it still ask the initial confirmation that can claude code read and modify the code in this new directory?
yes it will ask! a couple options:
- approve in the parent path once. this works if worktrees are subpaths of the main project
- muscle memory to hit return after starting
- guess it's possible to write something to write into
claude.jsonto approve - lobby here for a global flag turning off approvals
Decent work-around! Please checkout this as a possible improved option https://github.com/anthropics/claude-code/issues/2180
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
Thanks for reporting! Shared this discussion with our team for inspiration.
@catherinewu big fan!
I actually just open-sourced Worktrunk yesterday — a robust implementation of the ideas in my original post. It does
- the basic
git worktree+cdops - the automated committing & merging,
git llm-message+git worktree-mergeabove - ....and then a lot of newer stuff, like
wt listgives a list worktrees + stats + Claude Code waiting status + CI status
demo and docs at https://github.com/max-sixty/worktrunk/
let me know any questions!
This thread and wtp as inspiration I've also made my own tool to suit my workflow.
The big difference to others is the tight coupling to tmux. Each worktree gets its own tmux window, which makes managing worktrees quite natural if you're an experienced tmux user.
https://github.com/raine/workmux