Skip to content

Instantly share code, notes, and snippets.

@wojtha
Last active February 4, 2026 11:27
Show Gist options
  • Select an option

  • Save wojtha/bc41e6bdddbb78183c0d25bded21a291 to your computer and use it in GitHub Desktop.

Select an option

Save wojtha/bc41e6bdddbb78183c0d25bded21a291 to your computer and use it in GitHub Desktop.
cwm - Claude Worktree Manager | Installation: curl -fsSL "https://gist.githubusercontent.com/wojtha/bc41e6bdddbb78183c0d25bded21a291/raw/install.sh?$(date +%s)" | bash
#!/usr/bin/env bash
# cwm - Claude Worktree Manager
# Worktrees are created under .claude/worktrees/ in each repository
#
# This script is designed to be sourced via a shell function for cd to work:
# cwm() { source ~/.local/bin/cwm.sh "$@"; }
# Run everything in a subshell to avoid polluting the parent shell's state
_cwm_result=$(
set -euo pipefail
CWM_VERSION="1.0.0"
CWM_DATE="2026-02-04"
# Colors and output helpers
err() { echo -e "\033[0;31m✗ $*\033[0m" >&2; }
ok() { echo -e "\033[0;32m✓ $*\033[0m" >&2; }
warn() { echo -e "\033[0;33m⚠ $*\033[0m" >&2; }
info() { echo -e "\033[0;34m$*\033[0m" >&2; }
# Core helpers
_cwm_main_repo() { git rev-parse --git-common-dir 2>/dev/null | xargs dirname; }
_cwm_default_branch() { git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main"; }
_cwm_current_branch() { git branch --show-current 2>/dev/null; }
_cwm_sanitize() { echo "$1" | tr '/' '-'; }
_cwm_in_worktree() {
[[ "$(git rev-parse --git-dir 2>/dev/null)" != "$(git rev-parse --git-common-dir 2>/dev/null)" ]]
}
# Find branch from sanitized name
_cwm_find_branch() {
local input="$1" with_slash="${1/-/\/}"
git show-ref --verify --quiet "refs/heads/$with_slash" 2>/dev/null && echo "$with_slash" && return
git show-ref --verify --quiet "refs/heads/$input" 2>/dev/null && echo "$input" && return
git for-each-ref --format='%(refname:short)' refs/heads/ | while read -r b; do
[[ "$(_cwm_sanitize "$b")" == "$input" ]] && echo "$b" && return
done
}
# Get worktree path for branch from git worktree list
_cwm_get_worktree_path() {
git worktree list --porcelain | awk -v b="$1" '
/^worktree / { path = substr($0, 10) }
/^branch refs\/heads\// { if (substr($0, 19) == b) print path }
'
}
_cwm_worktree_path() { echo "$(_cwm_main_repo)/.claude/worktrees/$(_cwm_sanitize "$1")"; }
_cwm_has_unpushed() {
local upstream
upstream="$(git rev-parse --abbrev-ref "$1@{upstream}" 2>/dev/null)" || return 1
[[ -n "$(git log "$upstream..$1" --oneline 2>/dev/null)" ]]
}
_cwm_ensure_gitignore() {
local gitignore="$(_cwm_main_repo)/.gitignore"
grep -q "^\.claude/worktrees" "$gitignore" 2>/dev/null || echo ".claude/worktrees" >> "$gitignore"
}
# Resolve branch: use input, find it, or fall back to input
_cwm_resolve_branch() {
local input="$1" found
found="$(_cwm_find_branch "$input")"
echo "${found:-$input}"
}
# Get branch for current context (arg or current worktree)
_cwm_get_branch_arg() {
local branch="$1" cmd="$2"
if [[ -z "$branch" ]]; then
if _cwm_in_worktree; then
_cwm_current_branch
else
err "Not in a worktree. Specify branch name: cwm $cmd <branch>"
return 1
fi
else
echo "$branch"
fi
}
_cwm_help() {
cat >&2 <<EOF
cwm - Claude Worktree Manager v${CWM_VERSION} (${CWM_DATE})
Usage:
cwm Show this help
cwm install Add cwm function to shell rc
cwm list List all worktrees
cwm create Create worktree for current branch (or prompt)
cwm create NAME Create worktree for branch NAME
cwm remove Remove current worktree (checks for changes)
cwm force-remove Force remove worktree (warns but proceeds)
cwm cd NAME Change to worktree for branch NAME
cwm status Show git status of current worktree
Aliases: create=new=add, remove=rm, force-remove=rmf, list=ls, cd=go, status=st
Worktrees are stored in .claude/worktrees/<branch-name>
Branch slashes are converted to dashes (vk/feature → vk-feature)
EOF
}
_cwm_list() {
info "Worktrees in $(_cwm_main_repo):"
echo >&2
git worktree list | while IFS= read -r line; do
local wt_path="${line%% *}" wt_branch="${line##*\[}" wt_status="clean"
wt_branch="${wt_branch%\]}"
if [[ -d "$wt_path" ]]; then
if ! git -C "$wt_path" diff --quiet 2>/dev/null || ! git -C "$wt_path" diff --cached --quiet 2>/dev/null; then
wt_status="dirty"
elif [[ -n "$(git -C "$wt_path" ls-files --others --exclude-standard 2>/dev/null)" ]]; then
wt_status="untracked"
fi
fi
printf " %-30s %s (%s)\n" "$wt_branch" "$wt_path" "$wt_status" >&2
done
}
_cwm_create() {
local branch="$1" main_repo default_branch current_branch
main_repo="$(_cwm_main_repo)"
default_branch="$(_cwm_default_branch)"
current_branch="$(_cwm_current_branch)"
if [[ -z "$branch" ]]; then
if [[ "$current_branch" == "$default_branch" ]]; then
printf "Enter new branch name: " >&2
read -r branch </dev/tty 2>/dev/null || branch=""
[[ -z "$branch" ]] && err "Branch name required" && return 1
else
branch="$current_branch"
fi
fi
# Check if already checked out
local existing_path
existing_path="$(_cwm_get_worktree_path "$branch")"
if [[ -n "$existing_path" ]]; then
warn "Branch '$branch' is already checked out at: $existing_path"
printf "Go there instead? [Y/n]: " >&2
read -r go_there </dev/tty 2>/dev/null || go_there=""
[[ "$go_there" != "n" && "$go_there" != "N" ]] && echo "CD:$existing_path" && return 0
return 1
fi
local worktree_path="$(_cwm_worktree_path "$branch")"
[[ -d "$worktree_path" ]] && err "Worktree already exists: $worktree_path" && return 1
mkdir -p "$main_repo/.claude/worktrees"
_cwm_ensure_gitignore
if ! git show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
info "Branch '$branch' does not exist."
printf "Base branch [$default_branch]: " >&2
read -r base_branch </dev/tty 2>/dev/null || base_branch=""
git branch "$branch" "${base_branch:-$default_branch}" >&2
fi
info "Creating worktree at $worktree_path..."
git worktree add "$worktree_path" "$branch" >&2
ok "Created worktree for '$branch'"
echo "CD:$worktree_path"
}
_cwm_remove() {
local branch force="${2:-}" main_repo
branch="$(_cwm_get_branch_arg "$1" "remove")" || return 1
branch="$(_cwm_resolve_branch "$branch")"
main_repo="$(_cwm_main_repo)"
local worktree_path="$(_cwm_worktree_path "$branch")"
[[ ! -d "$worktree_path" ]] && err "Worktree not found: $worktree_path" && return 1
# Check for uncommitted work
local has_changes=false has_untracked=false
if ! git -C "$worktree_path" diff --quiet 2>/dev/null || ! git -C "$worktree_path" diff --cached --quiet 2>/dev/null; then
has_changes=true
fi
if [[ -n "$(git -C "$worktree_path" ls-files --others --exclude-standard 2>/dev/null)" ]]; then
has_untracked=true
fi
if [[ "$force" != "force" ]] && { $has_changes || $has_untracked; }; then
err "Worktree has uncommitted changes or untracked files!"
info "Run 'cwm status $(_cwm_sanitize "$branch")' to see the changes"
info "Use 'cwm force-remove' to remove anyway"
return 2
fi
$has_changes && warn "Worktree has uncommitted changes (forcing anyway)"
$has_untracked && warn "Worktree has untracked files (forcing anyway)"
_cwm_has_unpushed "$branch" && warn "Branch has unpushed commits"
info "Removing worktree '$(_cwm_sanitize "$branch")'..."
git -C "$main_repo" worktree remove --force "$worktree_path" >&2
printf "Delete branch '$branch' too? [y/N]: " >&2
read -r delete_branch </dev/tty 2>/dev/null || delete_branch=""
if [[ "$delete_branch" == "y" || "$delete_branch" == "Y" ]]; then
if _cwm_has_unpushed "$branch"; then
warn "Branch has unpushed commits!"
printf "Delete anyway? [y/N]: " >&2
read -r confirm </dev/tty 2>/dev/null || confirm=""
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && ok "Worktree removed (branch kept)" && echo "CD:$main_repo" && return 0
fi
git -C "$main_repo" branch -D "$branch" >&2
ok "Worktree and branch removed"
else
ok "Worktree removed (branch kept)"
fi
echo "CD:$main_repo"
}
_cwm_cd() {
local branch="$1"
[[ -z "$branch" ]] && err "Usage: cwm cd <branch>" && return 1
local main_repo="$(_cwm_main_repo)" default_branch="$(_cwm_default_branch)"
[[ "$branch" == "$default_branch" || "$branch" == "main" || "$branch" == "master" ]] && echo "CD:$main_repo" && return
branch="$(_cwm_resolve_branch "$branch")"
local worktree_path="$(_cwm_get_worktree_path "$branch")"
if [[ -z "$worktree_path" || ! -d "$worktree_path" ]]; then
err "No worktree found for branch: $branch"
err "Create it with: cwm create $branch"
return 1
fi
echo "CD:$worktree_path"
}
_cwm_status() {
local branch
branch="$(_cwm_get_branch_arg "$1" "status")" || return 1
branch="$(_cwm_resolve_branch "$branch")"
local worktree_path="$(_cwm_worktree_path "$branch")"
[[ ! -d "$worktree_path" ]] && err "Worktree not found: $worktree_path" && return 1
info "Status of worktree: $branch"
info "Path: $worktree_path"
echo >&2
git -C "$worktree_path" status --short --branch >&2
}
_cwm_install() {
local shell_rc="$HOME/.bashrc"
[[ -n "${ZSH_VERSION:-}" || "$SHELL" == */zsh ]] && shell_rc="$HOME/.zshrc"
if grep -q 'cwm().*source.*cwm.sh' "$shell_rc" 2>/dev/null; then
info "Already installed in $shell_rc"
return 0
fi
echo -e "\n# Claude Worktree Manager\ncwm() { source ~/.local/bin/cwm.sh \"\$@\"; }" >> "$shell_rc"
ok "Installed to $shell_rc"
info "Run: source $shell_rc"
}
_cwm_main() {
local cmd="${1:-}"
shift 2>/dev/null || true
case "$cmd" in
install) _cwm_install; return ;;
""|help|-h|--help) _cwm_help; return ;;
esac
git rev-parse --git-dir >/dev/null 2>&1 || { err "Not in a git repository"; return 1; }
case "$cmd" in
list|ls) _cwm_list ;;
create|new|add) _cwm_create "${1:-}" ;;
remove|rm) _cwm_remove "${1:-}" ;;
force-remove|rmf) _cwm_remove "${1:-}" "force" ;;
cd|go) _cwm_cd "${1:-}" ;;
status|st) _cwm_status "${1:-}" ;;
*) err "Unknown command: $cmd"; _cwm_help; return 1 ;;
esac
}
_cwm_main "$@"
)
_cwm_exit_code=$?
# Handle CD:path directive
if [[ "$_cwm_result" == CD:* ]]; then
_cwm_target="${_cwm_result#CD:}"
cd "$_cwm_target" || true
echo -e "\033[0;34mChanged to: $_cwm_target\033[0m" >&2
fi
unset _cwm_result _cwm_target
return $_cwm_exit_code 2>/dev/null || exit $_cwm_exit_code
#!/usr/bin/env bash
set -euo pipefail
# cwm installer - Claude Worktree Manager
# Usage: curl -fsSL "https://gist.githubusercontent.com/wojtha/bc41e6bdddbb78183c0d25bded21a291/raw/install.sh?$(date +%s)" | bash
GIST_RAW_URL="https://gist.githubusercontent.com/wojtha/bc41e6bdddbb78183c0d25bded21a291/raw"
INSTALL_DIR="${CWM_INSTALL_DIR:-$HOME/.local/bin}"
SCRIPT_NAME="cwm.sh"
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
err() { echo -e "${RED}✗ $*${NC}" >&2; exit 1; }
ok() { echo -e "${GREEN}✓ $*${NC}"; }
info() { echo -e "${BLUE}$*${NC}"; }
main() {
info "Installing cwm - Claude Worktree Manager..."
# Create install directory
mkdir -p "$INSTALL_DIR"
# Download the script (with cache-busting timestamp)
info "Downloading cwm.sh..."
if ! curl -sSL "${GIST_RAW_URL}/${SCRIPT_NAME}?$(date +%s)" -o "$INSTALL_DIR/$SCRIPT_NAME"; then
err "Failed to download cwm.sh"
fi
chmod +x "$INSTALL_DIR/$SCRIPT_NAME"
ok "Downloaded to $INSTALL_DIR/$SCRIPT_NAME"
# Add shell function
local shell_rc func_def
if [[ -n "${ZSH_VERSION:-}" ]] || [[ "${SHELL:-}" == */zsh ]]; then
shell_rc="$HOME/.zshrc"
else
shell_rc="$HOME/.bashrc"
fi
func_def='cwm() { source ~/.local/bin/cwm.sh "$@"; }'
if grep -q 'cwm().*source.*cwm.sh' "$shell_rc" 2>/dev/null; then
info "Shell function already in $shell_rc"
else
{
echo ""
echo "# Claude Worktree Manager"
echo "$func_def"
} >> "$shell_rc"
ok "Added shell function to $shell_rc"
fi
echo ""
ok "Installation complete!"
echo ""
info "To start using cwm, run:"
echo " source $shell_rc"
echo ""
info "Or open a new terminal, then run:"
echo " cwm help"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment