How I use git worktree in developing Rails apps

2026-03-29


Git comes with so many features in them, I will likely never know or get to use all of them in my lifetime. One such feature that has been gaining traction is Git Worktree. Riding the industrial seismic shift to Claude Code and other agentic coding tools, it has allowed engineers to keep working on multiple branches at the same time without needing to git stash; git fetch; git checkout -b; git stash pop; repeatedly.

This is one of my daily workflows, in which the 2 scripts I am sharing today will be incredibly helpful for:

  1. Someone else in our org (be it a fellow engineer or a triage bot) has created a pull request on github that I want to look into, it's on a branch called bugfix/integer-overflow-abc123 for example.
  2. I run cd ~/org/project-1 and worktree-checkout bugfix/integer-overflow-abc123
  3. This will cd me to ~/org/project-1__bugfix-integer-overflow-abc123, complete with all the gitignored files, such as .env .mcp.json .claude/* etc.
  4. I run bin/dev and it spins up development server on a different port number than my other worktrees, enabling Claude Code and I to work and test on different things simultaneously.

All the codes below I am sharing to you are generated by Claude Code, but I believe this workflow I evolved over time will be helpful to you. Feel free to get your agent to look at it and adapt to your needs.

worktree-checkout shell function

You can copy this function into your shell rc file, what this does:

  1. Run git fetch to pull the latest branches.
  2. Checkout the branch to a directory same level as the project dir

    project-1                          # master branch
    project-1__feature-add-something   # feature/add-something
    project-1__bugfix-fix-something    # bugfix/fix-something
    

Code:

# ~/.zshrc

worktree-checkout() {
  if [ -z "$1" ]; then
    echo "Usage: worktree-checkout <branch-name>"
    return 1
  fi

  local branch="$1"
  local repo_dir="$(basename "$(pwd)")"
  local sanitized="$(echo "$branch" | tr '/' '-')"
  local worktree_path="../${repo_dir}__${sanitized}"

  git fetch || return 1
  git worktree add "$worktree_path" -b "$branch" "origin/$branch" 2>/dev/null \
    || git worktree add "$worktree_path" "$branch" \
    || return 1
  cd "$worktree_path"
}

.git/hooks/post-checkout

The .git directory is usually hidden in text editors and IDEs, you can access it with command line:

# ALWAYS modify .git only in the main worktree
cd ~/org/project-1    
touch .git/hooks/post-checkout

# Substitute code with other IDE, if you aren't using vs code
code .git/hooks/post-checkout

This is my post-checkout hook:

#!/bin/bash

# Git post-checkout hook to automatically symlink/copy environment files to new worktrees
# This hook runs after git checkout, git switch, and git worktree add

# CONFIGURATION: Modify these lists for your project
# Files/directories to symlink (relative to repo root)
SYMLINK_FILES=(
    ".env"
    ".env.test"
    ".mcp.json"
    ".claude"
)

# Files/directories to copy (relative to repo root)
# Here you put the files that can deviate from main/branch worktrees
COPY_FILES=(
    "tmp/tunnel"
    "tmp/cloudflared-config.yml"
)

# ============================================================================

prev_head="$1"
new_head="$2"
branch_checkout="$3"

# Check if this is a worktree creation (previous HEAD is null)
if [[ "$prev_head" == "0000000000000000000000000000000000000000" ]] && [[ "$branch_checkout" == "1" ]]; then
    echo "🔧 New worktree detected - setting up environment files..."

    # Get the main repository path
    main_repo_path="$(git rev-parse --git-common-dir)/.."
    main_repo_path="$(cd "$main_repo_path" && pwd)"

    # Get current worktree path
    current_path="$(pwd)"

    echo "📁 Copying from: $main_repo_path"
    echo "📁 Copying to: $current_path"

    # Create necessary directories from COPY_FILES
    while IFS= read -r dir; do
        if [[ ! -d "$current_path/$dir" ]]; then
            mkdir -p "$current_path/$dir"
            echo "📁 Created $dir"
        fi
    done < <(printf '%s\n' "${COPY_FILES[@]}" | xargs -I {} dirname {} | sort -u)

    # Symlink files/directories
    for item in "${SYMLINK_FILES[@]}"; do
        if [[ -e "$main_repo_path/$item" ]]; then
            # Remove existing file/directory first to avoid nested symlinks
            rm -rf "$current_path/$item"
            ln -s "$main_repo_path/$item" "$current_path/$item"
            echo "✅ Symlinked $item"
        else
            echo "⚠️  $item not found in main repository"
        fi
    done

    # Copy files
    for file in "${COPY_FILES[@]}"; do
        source_file="$main_repo_path/$file"
        target_file="$current_path/$file"

        if [[ -f "$source_file" ]]; then
            mkdir -p "$(dirname "$target_file")"
            cp "$source_file" "$target_file"
            echo "✅ Copied $file"
        else
            echo "⚠️  $file not found in main repository"
        fi
    done

    # Generate a random port between 3000-3999 and write to tmp/port
    PORT=$(( (RANDOM % 1000) + 3000 ))
    echo "$PORT" > tmp/port
    echo "✅ Generated random port: $PORT"

    echo "🎉 Environment setup complete for new worktree!"
fi

Supporting random port number

In post-checkout hook, we generate random number into tmp/port file to avoid port collision between worktrees. Now you need to ensure whatever script you use to boot up development server reads from that port file. For example, bin/dev:

#!/usr/bin/env sh

# Read .port file, else defaults to 3000
PORT=${PORT:-$(test -f tmp/port && cat tmp/port || echo 3000)}
bin/rails s -p $PORT

Be careful of brain frying

The problem I have with this workflow is not the tech, but me - I have trouble juggling this much work in my biological context window.

We are judged by others with our output quality, so even with all these new agentic coding workflows, we should be careful and not get too carried away of playing the token slot machines and lose track of our work.


Built with Ruby on Rails. Statically generated and hosted on Cloudflare Pages.