Skip to content

Instantly share code, notes, and snippets.

@lloyd
Created March 27, 2026 20:36
Show Gist options
  • Select an option

  • Save lloyd/bcfee65caae6a461a26376fa65eaac7f to your computer and use it in GitHub Desktop.

Select an option

Save lloyd/bcfee65caae6a461a26376fa65eaac7f to your computer and use it in GitHub Desktop.
Shell helpers for managing git worktrees + Claude Code sessions
# Worktree + Claude management
# Depends on: kraysee (from kraysee.sh)
# ── helpers ───────────────────────────────────────────────────────
# Detect the default branch for a repo (develop > main > master)
_wt_default_branch() {
local root="${1:-.}"
local branch
branch=$(git -C "$root" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
if [ -z "$branch" ]; then
for b in develop main master; do
if git -C "$root" show-ref --verify --quiet "refs/remotes/origin/$b"; then
branch="$b"
break
fi
done
fi
echo "$branch"
}
# ── wtnew ─────────────────────────────────────────────────────────
# wtnew <name> — create a worktree sibling, new branch, and launch claude
wtnew() {
if [ -z "$1" ]; then
echo "Usage: wtnew <name>"
return 1
fi
local name="$1"
local repo_root
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
echo "Error: not in a git repository"; return 1
}
local default_branch
default_branch=$(_wt_default_branch "$repo_root")
if [ -z "$default_branch" ]; then
echo "Error: could not determine default branch"
return 1
fi
local wt_path="${repo_root}.${name}"
if [ -d "$wt_path" ]; then
echo "Error: already exists: $wt_path"
return 1
fi
echo "Fetching latest..."
git fetch origin "$default_branch" --quiet
echo "Creating worktree at $wt_path (branch '$name' from 'origin/$default_branch')"
git worktree add -b "$name" "$wt_path" "origin/$default_branch" || return 1
cd "$wt_path" || return 1
echo "Now in $wt_path on branch $name"
kraysee
}
# ── wtwut ─────────────────────────────────────────────────────────
# wtwut — show all worktrees with git status details
wtwut() {
git rev-parse --show-toplevel &>/dev/null || {
echo "Error: not in a git repository"; return 1
}
local main_root
main_root=$(git worktree list --porcelain | head -1 | sed 's/worktree //')
local default_branch
default_branch=$(_wt_default_branch "$main_root")
echo "Worktrees for $(basename "$main_root") (default branch: $default_branch):"
echo ""
local wt_path="" branch=""
while IFS= read -r line; do
case "$line" in
"worktree "*)
wt_path="${line#worktree }"
branch=""
;;
"branch "*)
branch="${line#branch refs/heads/}"
;;
"")
if [ -n "$wt_path" ]; then
_wtwut_print "$wt_path" "$branch" "$default_branch" "$main_root"
fi
wt_path=""
branch=""
;;
esac
done < <(git worktree list --porcelain; echo "")
}
_wtwut_print() {
local wt_path="$1" branch="$2" default_branch="$3" main_root="$4"
local changes status_parts=()
# Uncommitted changes
changes=$(git -C "$wt_path" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ "$changes" -gt 0 ]; then
status_parts+=("\033[1;33m${changes} uncommitted\033[0m")
else
status_parts+=("\033[0;32mclean\033[0m")
fi
# Ahead/behind default branch
if [ -n "$default_branch" ] && [ "$branch" != "$default_branch" ]; then
local ab
ab=$(git -C "$wt_path" rev-list --left-right --count "origin/$default_branch...$branch" 2>/dev/null)
if [ -n "$ab" ]; then
local behind_n ahead_n
behind_n=$(echo "$ab" | awk '{print $1}')
ahead_n=$(echo "$ab" | awk '{print $2}')
if [ "$ahead_n" -eq 0 ] 2>/dev/null && [ "$behind_n" -eq 0 ] 2>/dev/null; then
status_parts+=("up to date")
elif [ "$ahead_n" -eq 0 ] 2>/dev/null; then
status_parts+=("\033[0;36mfully merged, ${behind_n} behind\033[0m")
else
local parts=""
[ "$ahead_n" -gt 0 ] 2>/dev/null && parts="${ahead_n} ahead"
[ "$behind_n" -gt 0 ] 2>/dev/null && parts="${parts:+$parts, }${behind_n} behind"
status_parts+=("\033[0;35m${parts}\033[0m")
fi
fi
fi
# Mark if this is the current directory
local marker=" "
if [ "$wt_path" = "$(git rev-parse --show-toplevel 2>/dev/null)" ]; then
marker="*"
fi
local status_str=""
for i in "${!status_parts[@]}"; do
[ "$i" -gt 0 ] && status_str+=", "
status_str+="${status_parts[$i]}"
done
printf " %s %-40s \033[0;34m%-20s\033[0m %b\n" "$marker" "$wt_path" "[$branch]" "$status_str"
# Show compact file changes for dirty worktrees
if [ "$changes" -gt 0 ]; then
git -C "$wt_path" diff --stat=70 --color=always 2>/dev/null | sed '$d' | head -8 | sed 's/^/ /'
git -C "$wt_path" diff --cached --stat=70 --color=always 2>/dev/null | sed '$d' | head -4 | sed 's/^/ /'
local _ut_n
_ut_n=$(git -C "$wt_path" ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
[ "$_ut_n" -gt 0 ] && printf " \033[0;32m+%s untracked\033[0m\n" "$_ut_n"
fi
}
# ── wtbye ─────────────────────────────────────────────────────────
# wtbye [name] — remove a worktree (current one if no name given)
wtbye() {
git rev-parse --show-toplevel &>/dev/null || {
echo "Error: not in a git repository"; return 1
}
local main_root
main_root=$(git worktree list --porcelain | head -1 | sed 's/worktree //')
local wt_path="" branch="" name=""
if [ -n "$1" ]; then
name="$1"
wt_path="${main_root}.${name}"
else
wt_path=$(git rev-parse --show-toplevel 2>/dev/null)
if [ "$wt_path" = "$main_root" ]; then
echo "Error: you're in the main worktree. Provide a name: wtbye <name>"
return 1
fi
fi
# Verify it's a known worktree
if ! git worktree list --porcelain | grep -q "^worktree ${wt_path}$"; then
echo "Error: no worktree at $wt_path"
return 1
fi
# Find the branch for this worktree
local in_target=0
while IFS= read -r line; do
if [ "$line" = "worktree $wt_path" ]; then
in_target=1
elif [[ "$line" == "worktree "* ]] && [ "$in_target" -eq 1 ]; then
break
elif [ "$in_target" -eq 1 ] && [[ "$line" == "branch "* ]]; then
branch="${line#branch refs/heads/}"
break
fi
done < <(git worktree list --porcelain)
# If we're inside the worktree being removed, move out
if [[ "$(pwd)" == "$wt_path"* ]]; then
echo "Moving to main worktree: $main_root"
cd "$main_root" || return 1
fi
echo "Removing worktree: $wt_path"
if ! git worktree remove "$wt_path" 2>/dev/null; then
echo ""
printf " \033[1;33mWorktree has uncommitted changes:\033[0m\n"
git -C "$wt_path" diff --stat=80 --color=always 2>/dev/null | sed 's/^/ /'
git -C "$wt_path" diff --cached --stat=80 --color=always 2>/dev/null | sed 's/^/ /'
local _untracked
_untracked=$(git -C "$wt_path" ls-files --others --exclude-standard 2>/dev/null)
if [ -n "$_untracked" ]; then
echo "$_untracked" | head -5 | while IFS= read -r _f; do
printf " \033[0;32m%s\033[0m (new)\n" "$_f"
done
local _ut_count
_ut_count=$(echo "$_untracked" | wc -l | tr -d ' ')
[ "$_ut_count" -gt 5 ] && echo " ... and $((_ut_count - 5)) more untracked"
fi
echo ""
read -rp "Force remove? [y/N] " answer < /dev/tty
if [[ "$answer" =~ ^[Yy]$ ]]; then
git worktree remove --force "$wt_path" || return 1
else
echo "Aborted."
return 1
fi
fi
if [ -n "$branch" ]; then
if git branch -d "$branch" 2>/dev/null; then
echo "Deleted branch: $branch"
else
read -rp "Branch '$branch' not fully merged. Force delete? [y/N] " answer < /dev/tty
if [[ "$answer" =~ ^[Yy]$ ]]; then
git branch -D "$branch"
echo "Force-deleted branch: $branch"
else
echo "Kept branch: $branch"
fi
fi
fi
echo "Done."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment