Skip to content

Instantly share code, notes, and snippets.

@davidmks
Created March 5, 2026 15:05
Show Gist options
  • Select an option

  • Save davidmks/bcb14933a060c57dadc6c03e12678fc9 to your computer and use it in GitHub Desktop.

Select an option

Save davidmks/bcb14933a060c57dadc6c03e12678fc9 to your computer and use it in GitHub Desktop.
Git Worktree + Tmux Workflow — parallel development with auto-configured tmux sessions

Git Worktree + Tmux Workflow

A workflow for parallel development using git worktrees, each with its own tmux session pre-configured with dedicated windows.

How it works

start-worktree feat/GEN-1234-my-feature

This single command:

  1. Creates a git worktree (isolated working directory sharing git history)
  2. Symlinks shared config (.env, secrets, ssl certs, etc.)
  3. Installs dependencies (make setup)
  4. Opens a tmux session with 4 windows: terminal | claude | nvim | script

When you're done:

delete-worktree feat/GEN-1234-my-feature

This kills the tmux session, removes the worktree, and optionally deletes the branch. Works even from inside the worktree being deleted.

Components

File What it does
start-worktree Entry point — creates worktree + opens tmux session
delete-worktree Entry point — kills tmux session + removes worktree
create_worktree.sh Core logic: git worktree creation, symlinks, dependency setup
cleanup_worktree.sh Core logic: worktree removal, branch deletion, pruning
tmux-sessionizer Fork of ThePrimeagen's script — creates/switches tmux sessions for directories
.tmux-sessionizer Hydration script — defines the 4-window layout for every new session

Flow diagram

start-worktree <branch> [base]
  │
  ├─► create_worktree.sh
  │     ├─ git worktree add -b <branch> ~/wt/lizy-backend/<branch> <base>
  │     ├─ symlink .env, .env.secrets, .mcp.json, ssl/, etc.
  │     └─ make setup (auto-cleans up on failure)
  │
  └─► tmux-sessionizer ~/wt/lizy-backend/<branch>
        ├─ creates tmux session named after the directory
        └─ runs ~/.tmux-sessionizer (hydration):
              window 1: terminal
              window 2: claude (auto-starts Claude Code)
              window 3: nvim (opens editor with .env.test loaded)
              window 4: script (for running scripts)
delete-worktree <branch>
  │
  ├─► tmux kill-session (if running)
  │
  └─► cleanup_worktree.sh
        ├─ git worktree remove --force
        ├─ optionally delete the branch
        └─ git worktree prune

Installation

All entry points (start-worktree, delete-worktree, tmux-sessionizer) live in ~/.local/bin/. The core scripts (create_worktree.sh, cleanup_worktree.sh) live in <repo>/local_scripts/worktree/. The hydration script lives at ~/.tmux-sessionizer.

Usage

# Create worktree from trunk (default base branch)
start-worktree feat/GEN-1234-my-feature

# Create from a specific base branch
start-worktree feat/GEN-1234-my-feature main

# Auto-generate a random name (e.g. "calm-river")
start-worktree

# List existing worktrees
delete-worktree

# Remove a worktree + kill its tmux session
delete-worktree feat/GEN-1234-my-feature
#!/usr/bin/env bash
set -euo pipefail
MAIN_REPO="$HOME/work/lizy-backend"
CREATE_SCRIPT="$MAIN_REPO/local_scripts/worktree/create_worktree.sh"
if [[ ! -f "$CREATE_SCRIPT" ]]; then
echo "Error: create_worktree.sh not found at $CREATE_SCRIPT"
exit 1
fi
# Pass -h/--help through
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
"$CREATE_SCRIPT" "$@"
exit 0
fi
cd "$MAIN_REPO"
# Run create_worktree.sh, showing output while capturing it to extract the path
output_file=$(mktemp)
trap 'rm -f "$output_file"' EXIT
"$CREATE_SCRIPT" "$@" 2>&1 | tee "$output_file"
# Extract the worktree path from the output
WORKTREE_PATH=$(grep '📁 Path:' "$output_file" | tail -1 | sed 's/.*📁 Path: //')
if [[ -z "$WORKTREE_PATH" || ! -d "$WORKTREE_PATH" ]]; then
echo "Error: Could not determine worktree path"
exit 1
fi
tmux-sessionizer "$WORKTREE_PATH"
#!/usr/bin/env bash
set -euo pipefail
MAIN_REPO="$HOME/work/lizy-backend"
CLEANUP_SCRIPT="$MAIN_REPO/local_scripts/worktree/cleanup_worktree.sh"
if [[ ! -f "$CLEANUP_SCRIPT" ]]; then
echo "Error: cleanup_worktree.sh not found at $CLEANUP_SCRIPT"
exit 1
fi
# Pass -h/--help through
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
"$CLEANUP_SCRIPT" "$@"
exit 0
fi
# No args — list available worktrees
if [[ $# -eq 0 ]]; then
cd "$MAIN_REPO"
"$CLEANUP_SCRIPT"
exit 0
fi
BRANCH_NAME="$1"
WORKTREE_DIR="${BRANCH_NAME//\//-}"
# Mirror tmux-sessionizer's session naming: basename + tr . _
SESSION_NAME=$(basename "$WORKTREE_DIR" | tr . _)
# Kill tmux session if it exists
if tmux has-session -t="$SESSION_NAME" 2>/dev/null; then
echo "Killing tmux session: $SESSION_NAME"
tmux kill-session -t "$SESSION_NAME"
fi
cd "$MAIN_REPO"
"$CLEANUP_SCRIPT" "$BRANCH_NAME"
#!/usr/bin/env bash
set -euo pipefail
# create_worktree.sh - Create a new git worktree for parallel development
#
# Worktrees allow you to work on multiple branches simultaneously without
# stashing or committing. Each worktree is a separate working directory
# with its own branch, sharing the same git history.
readonly WORKTREES_BASE="$HOME/wt/lizy-backend"
show_help() {
cat << EOF
Usage: $(basename "$0") [worktree_name] [base_branch]
Create a new git worktree for parallel development work.
Arguments:
worktree_name Name for the worktree/branch. If not provided, generates
a unique human-readable name (e.g., calm-river).
Slashes in the name are converted to dashes for the directory.
base_branch Branch to create from (default: trunk)
Examples:
$(basename "$0") # Auto-generated name from trunk
# → branch: calm-river, dir: calm-river
$(basename "$0") feat/GEN-1234-add-auth # New feature from trunk
# → branch: feat/GEN-1234-add-auth, dir: feat-GEN-1234-add-auth
$(basename "$0") feat/v2 feat/v1 # Branch from specific base
The worktree will be created at: $WORKTREES_BASE/<worktree_name>
What this script does:
1. Creates a new worktree with a new branch
2. Symlinks shared config files (.env, .mcp.json, ssl/, etc.)
3. Runs 'make setup' to install dependencies
4. Cleans up automatically if setup fails
EOF
exit 0
}
# Handle --help/-h flag
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
show_help
fi
# Function to generate a unique worktree name
generate_unique_name() {
local -r adjectives=(
"calm" "bright" "gentle" "steady" "quiet" "swift" "clear" "warm"
"cool" "soft" "solid" "smooth" "brave" "kind" "wise" "fresh"
"light" "open" "simple" "stable" "focused" "balanced" "grounded" "clean"
"radiant" "soothing" "glimmering" "mellow" "tranquil" "shining"
"noble" "peaceful" "serene" "vivid" "luminous"
)
local -r nouns=(
"river" "path" "spark" "atlas" "orbit" "horizon" "forest" "breeze"
"signal" "echo" "stone" "cloud" "wave" "beacon" "compass" "anchor"
"harbor" "delta" "comet" "summit" "valley" "drift" "ember" "flame"
"trail" "bridge" "island" "source" "core" "pulse" "thread" "link"
"sparkle" "glow" "flare" "current"
)
local -r adj=${adjectives[$RANDOM % ${#adjectives[@]}]}
local -r noun=${nouns[$RANDOM % ${#nouns[@]}]}
echo "${adj}-${noun}"
}
# Validate we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "❌ Error: Not in a git repository" >&2
exit 1
fi
# Auto-detect main repo via git worktree list (first entry is always the main repo)
MAIN_REPO=$(git worktree list | head -1 | awk '{print $1}')
readonly MAIN_REPO
# Get worktree name from parameter or generate one
readonly WORKTREE_NAME=${1:-$(generate_unique_name)}
# Sanitize name for directory (replace / with -)
readonly WORKTREE_DIR="${WORKTREE_NAME//\//-}"
readonly WORKTREE_PATH="${WORKTREES_BASE}/${WORKTREE_DIR}"
# Get base branch from second parameter, default to trunk
readonly BASE_BRANCH=${2:-trunk}
echo "🌳 Creating worktree: ${WORKTREE_NAME}"
echo "📁 Location: ${WORKTREE_PATH}"
echo "🏠 Main repo: ${MAIN_REPO}"
echo "🔀 Base branch: ${BASE_BRANCH}"
# Create worktrees base directory if it doesn't exist
if [[ ! -d "$WORKTREES_BASE" ]]; then
echo "📁 Creating worktrees base directory: $WORKTREES_BASE"
mkdir -p "$WORKTREES_BASE"
fi
# Check if worktree already exists
if [[ -d "$WORKTREE_PATH" ]]; then
echo "❌ Error: Worktree directory already exists: $WORKTREE_PATH" >&2
exit 1
fi
# Change to main repo to create worktree
cd "$MAIN_REPO"
# Create worktree (creates branch if it doesn't exist)
BRANCH_CREATED=false # Track to preserve pre-existing branches on failure
if git show-ref --verify --quiet "refs/heads/${WORKTREE_NAME}"; then
echo "📋 Using existing branch: ${WORKTREE_NAME}"
git worktree add "$WORKTREE_PATH" "$WORKTREE_NAME"
else
echo "🆕 Creating new branch: ${WORKTREE_NAME}"
git worktree add -b "$WORKTREE_NAME" "$WORKTREE_PATH" "$BASE_BRANCH"
BRANCH_CREATED=true
fi
# Symlink config files from main repo instead of copying.
# This ensures all worktrees share the same configuration (single source of truth)
# and changes to .env or secrets propagate automatically.
echo "🔗 Creating symlinks..."
if [[ -f "$MAIN_REPO/.env" ]]; then
ln -sf "$MAIN_REPO/.env" "$WORKTREE_PATH/.env"
echo " ✓ .env"
fi
if [[ -f "$MAIN_REPO/.env.secrets" ]]; then
ln -sf "$MAIN_REPO/.env.secrets" "$WORKTREE_PATH/.env.secrets"
echo " ✓ .env.secrets"
fi
if [[ -f "$MAIN_REPO/.env.test" ]]; then
ln -sf "$MAIN_REPO/.env.test" "$WORKTREE_PATH/.env.test"
echo " ✓ .env.test"
fi
if [[ -f "$MAIN_REPO/.mcp.json" ]]; then
ln -sf "$MAIN_REPO/.mcp.json" "$WORKTREE_PATH/.mcp.json"
echo " ✓ .mcp.json"
fi
if [[ -d "$MAIN_REPO/ssl" ]]; then
# Use -snf to avoid creating nested ssl/ssl if directory exists
ln -snf "$MAIN_REPO/ssl" "$WORKTREE_PATH/ssl"
echo " ✓ ssl/"
fi
# Symlink settings.local.json (gitignored user settings) so Claude Code works in the worktree.
# The rest of .claude/ is tracked in git and already present in the worktree.
if [[ -f "$MAIN_REPO/.claude/settings.local.json" ]]; then
ln -sf "$MAIN_REPO/.claude/settings.local.json" "$WORKTREE_PATH/.claude/settings.local.json"
echo " ✓ .claude/settings.local.json"
fi
# Change to worktree directory and run setup
cd "$WORKTREE_PATH"
echo "🔧 Setting up worktree dependencies..."
if ! make setup; then
# Clean up on failure to avoid leaving broken worktrees that confuse users.
# --force is needed because the worktree may have generated files.
echo "❌ Setup failed. Cleaning up worktree..." >&2
cd "$MAIN_REPO"
git worktree remove --force "$WORKTREE_PATH"
if $BRANCH_CREATED; then
git branch -D "$WORKTREE_NAME" 2>/dev/null || true
fi
echo "❌ Not allowed to create worktree from a branch that isn't passing setup." >&2
exit 1
fi
echo ""
echo "✅ Worktree created successfully!"
echo "📁 Path: ${WORKTREE_PATH}"
echo "🔀 Branch: ${WORKTREE_NAME}"
echo ""
echo "To work in this worktree:"
echo " cd ${WORKTREE_PATH}"
echo ""
echo "To remove this worktree later:"
echo " ${MAIN_REPO}/local_scripts/worktree/cleanup_worktree.sh ${WORKTREE_NAME}"
#!/usr/bin/env bash
set -euo pipefail
# cleanup_worktree.sh - Clean up git worktrees created by create_worktree.sh
readonly WORKTREE_BASE_DIR="$HOME/wt/lizy-backend"
show_help() {
cat << EOF
Usage: $(basename "$0") [branch_name]
Clean up a git worktree and optionally delete its branch.
Arguments:
branch_name The branch name of the worktree to remove.
If not provided, lists all available worktrees.
Examples:
$(basename "$0") # List available worktrees
$(basename "$0") feat/GEN-1234-add-auth # Remove the worktree
What this script does:
1. Removes the worktree directory
2. Asks whether to delete the associated branch
3. Prunes stale worktree references
EOF
exit 0
}
# Handle --help/-h flag
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
show_help
fi
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Validate we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo -e "${RED}❌ Error: Not in a git repository${NC}" >&2
exit 1
fi
# Function to list worktrees
list_worktrees() {
echo -e "${YELLOW}Available worktrees:${NC}"
# Use grep -F for fixed-string matching to avoid regex interpretation of paths
if ! git worktree list | grep -F -- "$WORKTREE_BASE_DIR"; then
echo "No worktrees found in $WORKTREE_BASE_DIR"
return 1
fi
}
# Function to clean up a specific worktree
cleanup_worktree() {
local -r branch_name="$1"
# Convert slashes to hyphens for directory path (matching create_worktree.sh behavior)
local -r worktree_dir="${branch_name//\//-}"
local -r worktree_path="$WORKTREE_BASE_DIR/${worktree_dir}"
# Trailing space avoids partial path matches
if ! git worktree list | grep -qF -- "${worktree_path} "; then
echo -e "${RED}❌ Error: Worktree not found for branch '$branch_name'${NC}" >&2
echo ""
list_worktrees
echo ""
echo "Hint: Use the branch name shown in [brackets] above"
exit 1
fi
echo -e "${YELLOW}Cleaning up worktree: $worktree_path${NC}"
# Remove the worktree.
# --force is needed because worktrees often have uncommitted generated files
# (node_modules, .venv, etc.) that would otherwise block removal.
echo "Removing git worktree..."
if git worktree remove --force "$worktree_path"; then
echo -e "${GREEN}✓ Worktree removed successfully${NC}"
else
echo -e "${RED}❌ Error: Failed to remove worktree${NC}" >&2
echo "The worktree might be in an inconsistent state."
echo ""
echo "Try manually running:"
echo " rm -rf $worktree_path"
echo " git worktree prune"
exit 1
fi
# Delete the branch (optional, with confirmation)
echo ""
read -p "Delete the branch '$branch_name'? (y/N) " -n 1 -r
echo ""
if [[ ${REPLY:-} =~ ^[Yy]$ ]]; then
if git branch -D "$branch_name" 2>/dev/null; then
echo -e "${GREEN}✓ Branch deleted${NC}"
else
echo -e "${YELLOW}Branch might not exist or already deleted${NC}"
fi
else
echo "Branch kept: $branch_name"
fi
# Prune worktree references
echo "Pruning worktree references..."
git worktree prune
echo ""
echo -e "${GREEN}✓ Cleanup complete!${NC}"
}
# Main logic
if [[ $# -eq 0 ]]; then
list_worktrees || exit 1
echo ""
echo "Usage: $0 <branch_name>"
echo "Example: $0 feat/GEN-1234-add-auth"
echo ""
echo "Note: Provide the branch name (shown in brackets above)"
else
cleanup_worktree "$1"
fi
SESSION_NAME=$(tmux display-message -p '#S')
SESSION_PATH=$(pwd)
tmux rename-window -t "$SESSION_NAME:1" 'terminal'
tmux neww -t "$SESSION_NAME:2" -n 'claude' -c "$SESSION_PATH"
tmux send-keys -t "$SESSION_NAME:2" 'clear && claude' C-m
tmux neww -t "$SESSION_NAME:3" -n 'nvim' -c "$SESSION_PATH"
tmux send-keys -t "$SESSION_NAME:3" 'set -a && source .env.test && set +a && clear && nvim .' C-m
tmux neww -t "$SESSION_NAME:4" -n 'script' -c "$SESSION_PATH"
tmux send-keys -t "$SESSION_NAME:4" 'export UV_ENV_FILE=.env && clear' C-m
tmux select-window -t "$SESSION_NAME:1"
tmux send-keys -t "$SESSION_NAME:1" 'clear' C-m
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment