-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Description
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
endThe 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
endThe 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"
endgit 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
endgit 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
endgit 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 <no-reply@anthropic.com>"
endgit 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"
endgit 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