claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

Field notes: git worktree pattern

Open max-sixty opened this issue 7 months ago • 7 comments

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:

  1. Start a new feature with git worktree-llm feature-name
  2. Work on the feature with Claude's assistance
  3. When ready to merge, use git worktree-merge
  4. 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:

  1. Creates a new worktree with git worktree-create passing through any arguments
  2. 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
  1. 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:

  1. Captures the current branch name
  2. Adds all changes to the git index
  3. Finishes the worktree (returning to the main repository)
  4. Rebases the feature branch onto the default branch
  5. Switches to the default branch
  6. 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 avatar May 11 '25 23:05 max-sixty

@max-sixty smart

backnotprop avatar May 12 '25 03:05 backnotprop

@max-sixty thanks for sharing this!

adamavenir avatar May 23 '25 16:05 adamavenir

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?

raine avatar May 29 '25 22:05 raine

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)

max-sixty avatar May 29 '25 23:05 max-sixty

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?

raine avatar May 30 '25 07:05 raine

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.json to approve
  • lobby here for a global flag turning off approvals

max-sixty avatar May 30 '25 14:05 max-sixty

Decent work-around! Please checkout this as a possible improved option https://github.com/anthropics/claude-code/issues/2180

web3dev1337 avatar Jun 17 '25 03:06 web3dev1337

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.

github-actions[bot] avatar Oct 10 '25 10:10 github-actions[bot]

Thanks for reporting! Shared this discussion with our team for inspiration.

catherinewu avatar Nov 29 '25 21:11 catherinewu

@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 + cd ops
  • the automated committing & merging, git llm-message + git worktree-merge above
  • ....and then a lot of newer stuff, like wt list gives 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!

max-sixty avatar Nov 30 '25 00:11 max-sixty

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

raine avatar Nov 30 '25 06:11 raine