Skip to content

Instantly share code, notes, and snippets.

@gadenbuie
Last active February 6, 2026 20:43
Show Gist options
  • Select an option

  • Save gadenbuie/6a9d1b8088f6bc9154b6c534896bbd25 to your computer and use it in GitHub Desktop.

Select an option

Save gadenbuie/6a9d1b8088f6bc9154b6c534896bbd25 to your computer and use it in GitHub Desktop.
tiny bash script to quickly spin up a new worktree and open a new ide session
#!/usr/bin/env bash
#
# Source: https://gist.github.com/gadenbuie/6a9d1b8088f6bc9154b6c534896bbd25
set -e
# ==============================================================================
# Color and formatting setup (respects NO_COLOR)
# ==============================================================================
setup_colors() {
if [[ -z "${NO_COLOR:-}" && -t 1 ]]; then
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
GREEN='\033[32m'
BLUE='\033[34m'
YELLOW='\033[33m'
RED='\033[31m'
CYAN='\033[36m'
MAGENTA='\033[35m'
else
BOLD=''
DIM=''
RESET=''
GREEN=''
BLUE=''
YELLOW=''
CYAN=''
MAGENTA=''
fi
}
setup_colors
# ==============================================================================
# Output helpers
# ==============================================================================
header() {
echo ""
echo -e "${BOLD}${BLUE}${1}${RESET}"
}
info() {
echo -e " ${DIM}${RESET} ${1}"
}
success() {
echo -e " ${GREEN}${RESET} ${1}"
}
warn() {
echo -e " ${YELLOW}!${RESET} ${1}"
}
danger() {
echo -e " ${RED}${RESET} ${1}"
}
step() {
echo -e " ${CYAN}${RESET} ${1}"
}
clear_last_line() {
if [[ -z "${NO_COLOR:-}" && -t 1 ]]; then
echo -en "\033[A\033[2K\r"
fi
}
step_done() {
clear_last_line
success "$1"
}
step_fail() {
clear_last_line
danger "${1}"
}
path_output() {
echo -e " ${MAGENTA} ${1}${RESET}"
}
run_cmd() {
if [[ "$VERBOSE" == true ]]; then
"$@"
else
"$@" > /dev/null 2>&1
fi
}
# ==============================================================================
# Help
# ==============================================================================
help() {
echo -e "${BOLD}Usage:${RESET} create-worktree <branch-name> [options]"
echo -e " create-worktree remove [worktree-name] [options]"
echo ""
echo "Creates a git worktree with a new branch."
echo ""
echo -e "${BOLD}Subcommands:${RESET}"
echo " remove Remove a worktree (interactive picker if no name given)"
echo ""
echo -e "${BOLD}Options:${RESET}"
echo " --base <branch> Base branch to create from (default: current branch)"
echo " --open [mode] How to open the worktree folder:"
echo " auto - detect editor from environment (default)"
echo " false - don't open"
echo " <cmd> - use specified command (e.g., 'code', 'cursor')"
echo " (bare) - use 'open .' (Finder on macOS)"
echo " --no-setup Skip running setup commands (npm, uv, make)"
echo " -v, --verbose Show command output"
echo " -h, --help Show this help message"
echo ""
echo -e "${BOLD}To remove a worktree:${RESET}"
echo " create-worktree remove # interactive picker"
echo " create-worktree remove <name> # by name"
echo " create-worktree remove <name> --delete-branch"
}
auto_detect_editor() {
# Check for Positron first, since it is based on VS Code
if [[ "${POSITRON:-}" == "1" ]]; then
echo "positron"
return
fi
# Check for Cursor (VS Code fork) - has its own env vars
if [[ -n "${CURSOR_TRACE_ID:-}" || -n "${CURSOR_SESSION_ID:-}" ]]; then
echo "cursor"
return
fi
# Check for Windsurf (Codeium's VS Code fork)
if [[ -n "${WINDSURF:-}" || -n "${CODEIUM_WIND_SURF:-}" ]]; then
echo "windsurf"
return
fi
# Check for Zed
if [[ -n "${ZED_SESSION:-}" || "${TERM_PROGRAM:-}" == "zed" ]]; then
echo "zed"
return
fi
# Check TERM_PROGRAM (set by many terminal-integrated editors)
# Note: Cursor/Windsurf may report as "vscode", so check them first above
case "${TERM_PROGRAM:-}" in
vscode)
# Could be VS Code or a fork - check for Insiders
if [[ "${VSCODE_GIT_IPC_HANDLE:-}" == *"Code - Insiders"* ]]; then
echo "code-insiders"
else
echo "code"
fi
return
;;
esac
# Fallback: check for VS Code via other env vars
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_PID:-}" ]]; then
if [[ "${VSCODE_GIT_IPC_HANDLE:-}" == *"Code - Insiders"* ]]; then
echo "code-insiders"
else
echo "code"
fi
return
fi
# No editor detected
echo ""
}
# ==============================================================================
# Remove subcommand
# ==============================================================================
help_remove() {
echo -e "${BOLD}Usage:${RESET} create-worktree remove [worktree-name] [options]"
echo ""
echo "Removes a git worktree and optionally deletes its branch."
echo ""
echo -e "${BOLD}Options:${RESET}"
echo " --delete-branch Also delete the associated branch"
echo " -v, --verbose Show command output"
echo " -h, --help Show this help message"
echo ""
echo "If no worktree name is given, an interactive picker is shown."
}
do_remove() {
local REMOVE_NAME=""
local DELETE_BRANCH=false
while [[ $# -gt 0 ]]; do
case $1 in
--delete-branch)
DELETE_BRANCH=true
shift
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
help_remove
exit 0
;;
-*)
warn "Unknown option: $1"
echo ""
help_remove
exit 1
;;
*)
if [[ -z "$REMOVE_NAME" ]]; then
REMOVE_NAME="$1"
else
warn "Unexpected argument: $1"
echo ""
help_remove
exit 1
fi
shift
;;
esac
done
# Get repo root and name
local REPO_ROOT REPO_NAME WORKTREES_DIR
REPO_ROOT=$(git rev-parse --show-toplevel)
REPO_NAME=$(basename "$REPO_ROOT")
WORKTREES_DIR="$(dirname "$REPO_ROOT")/${REPO_NAME}.worktrees"
# Parse worktree list (excluding the main working tree)
local worktree_paths=()
local worktree_branches=()
local current_path="" current_branch=""
while IFS= read -r line; do
if [[ "$line" == "worktree "* ]]; then
current_path="${line#worktree }"
elif [[ "$line" == "branch "* ]]; then
current_branch="${line#branch refs/heads/}"
elif [[ -z "$line" ]]; then
if [[ -n "$current_path" && "$current_path" != "$REPO_ROOT" ]]; then
worktree_paths+=("$current_path")
worktree_branches+=("${current_branch:-}")
fi
current_path=""
current_branch=""
fi
done < <(git worktree list --porcelain)
if [[ ${#worktree_paths[@]} -eq 0 ]]; then
info "No worktrees found (besides the main working tree)"
exit 0
fi
# Helper: get display name for a worktree path
_wt_display_name() {
if [[ "$1" == "$WORKTREES_DIR/"* ]]; then
echo "${1#$WORKTREES_DIR/}"
else
echo "$1"
fi
}
local selected_idx=-1
if [[ -n "$REMOVE_NAME" ]]; then
# Match by relative name or basename
for i in "${!worktree_paths[@]}"; do
local rel_name
rel_name=$(_wt_display_name "${worktree_paths[$i]}")
if [[ "$rel_name" == "$REMOVE_NAME" || "$(basename "${worktree_paths[$i]}")" == "$REMOVE_NAME" ]]; then
selected_idx=$i
break
fi
done
if [[ $selected_idx -eq -1 ]]; then
danger "Worktree '${REMOVE_NAME}' not found"
echo ""
info "Available worktrees:"
for i in "${!worktree_paths[@]}"; do
local rel_name branch_info
rel_name=$(_wt_display_name "${worktree_paths[$i]}")
branch_info="${worktree_branches[$i]}"
if [[ -n "$branch_info" ]]; then
echo -e " ${rel_name} ${DIM}(${branch_info})${RESET}"
else
echo " ${rel_name}"
fi
done
exit 1
fi
else
# Interactive picker
header "Select a worktree to remove"
echo ""
for i in "${!worktree_paths[@]}"; do
local rel_name branch_info
rel_name=$(_wt_display_name "${worktree_paths[$i]}")
branch_info="${worktree_branches[$i]}"
if [[ -n "$branch_info" ]]; then
echo -e " ${BOLD}$((i + 1))${RESET}) ${rel_name} ${DIM}(${branch_info})${RESET}"
else
echo -e " ${BOLD}$((i + 1))${RESET}) ${rel_name} ${DIM}(detached)${RESET}"
fi
done
echo ""
local choice
while true; do
echo -en " ${CYAN}Pick a worktree [1-${#worktree_paths[@]}]:${RESET} "
read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#worktree_paths[@]} )); then
selected_idx=$((choice - 1))
break
fi
warn "Invalid selection, try again"
done
fi
local selected_path="${worktree_paths[$selected_idx]}"
local selected_branch="${worktree_branches[$selected_idx]}"
local selected_name
selected_name=$(_wt_display_name "$selected_path")
# Remove the worktree
header "Removing worktree '${selected_name}'"
step "git worktree remove \"${selected_name}\""
if run_cmd git worktree remove "$selected_path"; then
step_done "Worktree removed"
else
step_fail "Could not remove worktree (it may have uncommitted changes)"
info "Use ${CYAN}git worktree remove --force \"$selected_path\"${RESET} to force removal"
exit 1
fi
# Handle branch deletion
if [[ -n "$selected_branch" ]]; then
if [[ "$DELETE_BRANCH" == true ]]; then
step "Deleting branch '${selected_branch}'"
if run_cmd git branch -D "$selected_branch"; then
step_done "Branch '${selected_branch}' deleted"
else
step_fail "Could not delete branch '${selected_branch}'"
fi
else
echo ""
echo -en " Delete branch '${selected_branch}'? ${DIM}[y/N]${RESET} "
read -r confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
step "Deleting branch '${selected_branch}'"
if git branch -d "$selected_branch" > /dev/null 2>&1; then
step_done "Branch '${selected_branch}' deleted"
else
step_fail "Branch '${selected_branch}' is not fully merged"
echo -en " Force delete? ${DIM}[y/N]${RESET} "
read -r force_confirm
if [[ "$force_confirm" =~ ^[Yy]$ ]]; then
step "Force deleting branch '${selected_branch}'"
if run_cmd git branch -D "$selected_branch"; then
step_done "Branch '${selected_branch}' deleted"
else
step_fail "Could not delete branch '${selected_branch}'"
fi
else
info "Branch '${selected_branch}' kept"
fi
fi
else
info "Branch '${selected_branch}' kept"
fi
fi
fi
header "Done"
echo ""
}
# ==============================================================================
# Subcommand dispatch
# ==============================================================================
VERBOSE=false
if [[ "${1:-}" == "remove" ]]; then
shift
do_remove "$@"
exit 0
fi
# ==============================================================================
# Parse arguments
# ==============================================================================
BRANCH_NAME=""
BASE_BRANCH=""
OPEN_MODE="auto"
RUN_SETUP=true
while [[ $# -gt 0 ]]; do
case $1 in
--base)
BASE_BRANCH="$2"
shift 2
;;
--no-setup)
RUN_SETUP=false
shift
;;
--open)
# Check if next arg exists and is not a flag
if [[ -z "${2:-}" || "$2" == -* ]]; then
# Bare --open flag
OPEN_MODE="finder"
shift
else
OPEN_MODE="$2"
shift 2
fi
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
help
exit 0
;;
-*)
warn "Unknown option: $1"
echo ""
help
exit 1
;;
*)
if [[ -z "$BRANCH_NAME" ]]; then
BRANCH_NAME="$1"
else
warn "Unexpected argument: $1"
echo ""
help
exit 1
fi
shift
;;
esac
done
if [[ -z "$BRANCH_NAME" ]]; then
help
exit 1
fi
# ==============================================================================
# Main logic
# ==============================================================================
# Get repo root and name
REPO_ROOT=$(git rev-parse --show-toplevel)
REPO_NAME=$(basename "$REPO_ROOT")
# Default base branch to current branch
if [[ -z "$BASE_BRANCH" ]]; then
BASE_BRANCH=$(git branch --show-current)
fi
# Calculate worktree path
WORKTREE_DIR="$(dirname "$REPO_ROOT")/${REPO_NAME}.worktrees/${BRANCH_NAME}"
header "Creating worktree for ${BRANCH_NAME}"
info "Repository: ${REPO_NAME}"
info "Base branch: ${BASE_BRANCH}"
# Create the branch from base (if it doesn't already exist)
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
warn "Branch '${BRANCH_NAME}' already exists, using existing branch"
else
step "Creating branch '${BRANCH_NAME}' from '${BASE_BRANCH}'"
run_cmd git branch "$BRANCH_NAME" "$BASE_BRANCH"
step_done "Branch created"
fi
# Create the worktree
step "Creating worktree directory"
mkdir -p "$(dirname "$WORKTREE_DIR")"
if [[ "$VERBOSE" == true ]]; then
git worktree add "$WORKTREE_DIR" "$BRANCH_NAME"
else
git worktree add "$WORKTREE_DIR" "$BRANCH_NAME" 2>&1 | grep -v "^Preparing" || true
fi
step_done "Worktree created"
path_output "$WORKTREE_DIR"
# Symlink _dev if it exists
if [[ -d "$REPO_ROOT/_dev" ]]; then
step "Symlinking _dev folder"
ln -s "$REPO_ROOT/_dev" "$WORKTREE_DIR/_dev"
step_done "Symlinked _dev"
fi
# Symlink .claude if it exists in the source repo
if [[ -d "$REPO_ROOT/.claude" ]]; then
if [[ ! -d "$WORKTREE_DIR/.claude" ]]; then
# .claude folder doesn't exist in worktree, symlink the entire folder
step "Symlinking .claude folder"
ln -s "$REPO_ROOT/.claude" "$WORKTREE_DIR/.claude"
step_done "Symlinked .claude"
else
# .claude exists in worktree, check for settings.local.json
if [[ -f "$REPO_ROOT/.claude/settings.local.json" ]] && [[ ! -f "$WORKTREE_DIR/.claude/settings.local.json" ]]; then
step "Symlinking .claude/settings.local.json"
ln -s "$REPO_ROOT/.claude/settings.local.json" "$WORKTREE_DIR/.claude/settings.local.json"
step_done "Symlinked settings.local.json"
fi
fi
fi
# Change to worktree directory
cd "$WORKTREE_DIR"
# Run setup commands if applicable
if [[ "$RUN_SETUP" == true ]]; then
SETUP_RAN=false
if [[ -f "package.json" ]] && command -v npm &> /dev/null; then
header "Running project setup"
step "npm install"
run_cmd npm install
step_done "npm dependencies installed"
SETUP_RAN=true
fi
if [[ -f "pyproject.toml" ]] && command -v uv &> /dev/null; then
[[ "$SETUP_RAN" == false ]] && header "Running project setup"
step "uv sync --all-groups"
run_cmd uv sync --all-groups
step_done "Python dependencies installed"
SETUP_RAN=true
fi
if [[ -f "Makefile" ]] && grep -q '^setup:' Makefile; then
[[ "$SETUP_RAN" == false ]] && header "Running project setup"
step "make setup"
run_cmd make setup
step_done "Make setup complete"
SETUP_RAN=true
fi
fi
# Determine open command
OPEN_CMD=""
case "$OPEN_MODE" in
false)
# Don't open
;;
auto)
EDITOR=$(auto_detect_editor)
if [[ -n "$EDITOR" ]]; then
OPEN_CMD="$EDITOR ."
fi
;;
finder)
OPEN_CMD="open ."
;;
*)
# Custom command
OPEN_CMD="$OPEN_MODE ."
;;
esac
if [[ -n "$OPEN_CMD" ]]; then
header "Opening worktree"
step "$OPEN_CMD"
$OPEN_CMD
step_done "Opened"
fi
# ==============================================================================
# Summary
# ==============================================================================
header "Done"
echo ""
echo -e " ${DIM}To enter the worktree:${RESET}"
echo -e " ${CYAN}cd \"$WORKTREE_DIR\"${RESET}"
echo ""
echo -e " ${DIM}To delete this worktree later:${RESET}"
echo -e " ${CYAN}create-worktree remove ${BRANCH_NAME}${RESET}"
echo ""
@gadenbuie
Copy link
Author

gadenbuie commented Jan 22, 2026

Installation

# Download the script                                                                       
curl -o ~/.local/bin/create-worktree https://gist.githubusercontent.com/gadenbuie/6a9d1b8088
f6bc9154b6c534896bbd25/raw/create-worktree

# Make it executable
chmod +x ~/.local/bin/create-worktree

Make sure ~/.local/bin is in your PATH. Add this to your shell profile if needed:

export PATH="$HOME/.local/bin:$PATH"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment