Skip to content

Instantly share code, notes, and snippets.

@sorrycc
Created March 26, 2026 15:03
Show Gist options
  • Select an option

  • Save sorrycc/f403e2f7966708c1accb472b6510148b to your computer and use it in GitHub Desktop.

Select an option

Save sorrycc/f403e2f7966708c1accb472b6510148b to your computer and use it in GitHub Desktop.
Claude Code Version Manager - list, install, and remove Claude Code versions interactively
#!/bin/bash
set -euo pipefail
GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
GCS_API="https://storage.googleapis.com/storage/v1/b/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/o"
VERSIONS_DIR="$HOME/.local/share/claude/versions"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
# Detect platform
detect_platform() {
local os arch
case "$(uname -s)" in
Darwin) os="darwin" ;;
Linux) os="linux" ;;
*) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac
case "$(uname -m)" in
x86_64|amd64) arch="x64" ;;
arm64|aarch64) arch="arm64" ;;
*) echo "Unsupported arch: $(uname -m)" >&2; exit 1 ;;
esac
# Rosetta 2 detection
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
arch="arm64"
fi
fi
# musl detection
if [ "$os" = "linux" ]; then
if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then
echo "linux-${arch}-musl"
return
fi
fi
echo "${os}-${arch}"
}
PLATFORM=$(detect_platform)
# Fetch all remote versions from GCS bucket
fetch_remote_versions() {
local url="${GCS_API}?prefix=claude-code-releases/2.&delimiter=/&maxResults=500"
local json
json=$(curl -fsSL "$url") || { echo "Failed to fetch version list" >&2; return 1; }
echo "$json" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for p in data.get('prefixes', []):
print(p.replace('claude-code-releases/', '').rstrip('/'))
" | sort -V
}
# List local versions
list_local_versions() {
if [ -d "$VERSIONS_DIR" ]; then
for f in "$VERSIONS_DIR"/*; do
[ -f "$f" ] && basename "$f"
done | sort -V
fi
}
# Get current active version
get_active_version() {
local claude_bin
claude_bin=$(command -v claude 2>/dev/null) || return
"$claude_bin" --version 2>/dev/null | head -1 | awk '{print $1}'
}
# Install a specific version
install_version() {
local version="$1"
local dest="$VERSIONS_DIR/$version"
if [ -f "$dest" ]; then
echo -e "${YELLOW}Version $version is already installed.${RESET}"
return 0
fi
echo -e "${BLUE}Fetching manifest for $version...${RESET}"
local manifest
manifest=$(curl -fsSL "$GCS_BUCKET/$version/manifest.json") || {
echo -e "${RED}Failed to fetch manifest. Version $version may not exist.${RESET}" >&2
return 1
}
local checksum
checksum=$(echo "$manifest" | python3 -c "
import json, sys
data = json.load(sys.stdin)
p = data.get('platforms', {}).get('$PLATFORM', {})
print(p.get('checksum', ''))
")
if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then
echo -e "${RED}Platform $PLATFORM not found in manifest for $version.${RESET}" >&2
return 1
fi
local size
size=$(echo "$manifest" | python3 -c "
import json, sys
data = json.load(sys.stdin)
p = data.get('platforms', {}).get('$PLATFORM', {})
s = p.get('size', 0)
print(f'{s / 1048576:.1f}MB' if s else 'unknown size')
")
echo -e "${BLUE}Downloading claude $version ($size) for $PLATFORM...${RESET}"
mkdir -p "$VERSIONS_DIR"
local tmp="$VERSIONS_DIR/.claude-$version.tmp"
if ! curl -fSL --progress-bar -o "$tmp" "$GCS_BUCKET/$version/$PLATFORM/claude"; then
echo -e "${RED}Download failed.${RESET}" >&2
rm -f "$tmp"
return 1
fi
echo -e "${BLUE}Verifying checksum...${RESET}"
local actual
if [ "$(uname -s)" = "Darwin" ]; then
actual=$(shasum -a 256 "$tmp" | cut -d' ' -f1)
else
actual=$(sha256sum "$tmp" | cut -d' ' -f1)
fi
if [ "$actual" != "$checksum" ]; then
echo -e "${RED}Checksum mismatch!${RESET}"
echo -e " expected: $checksum"
echo -e " actual: $actual"
rm -f "$tmp"
return 1
fi
mv "$tmp" "$dest"
chmod +x "$dest"
echo -e "${GREEN}✓ Installed claude $version${RESET}"
}
# Remove a local version
remove_version() {
local version="$1"
local dest="$VERSIONS_DIR/$version"
if [ ! -f "$dest" ]; then
echo -e "${RED}Version $version is not installed.${RESET}"
return 1
fi
local active
active=$(get_active_version)
if [ "$version" = "$active" ]; then
echo -e "${YELLOW}Warning: $version appears to be the active version.${RESET}"
read -rp "Remove anyway? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || return 0
fi
rm -f "$dest"
echo -e "${GREEN}✓ Removed claude $version${RESET}"
}
# Display help
usage() {
cat <<EOF
${BOLD}Claude Code Version Manager${RESET}
${BOLD}Usage:${RESET} $(basename "$0") [command] [args]
${BOLD}Commands:${RESET}
list List remote versions (latest 20) and local installs
list --all List all remote versions
install [VERSION] Install a version (interactive if no version given)
remove [VERSION] Remove a local version
help Show this help
${BOLD}Examples:${RESET}
$(basename "$0") # Interactive mode
$(basename "$0") list # Show versions
$(basename "$0") install 2.1.77 # Install specific version
${DIM}Platform: $PLATFORM | Versions dir: $VERSIONS_DIR${RESET}
EOF
}
# Interactive menu
interactive_menu() {
while true; do
echo ""
echo -e "${BOLD}Claude Code Version Manager${RESET} ${DIM}[$PLATFORM]${RESET}"
echo -e "${DIM}─────────────────────────────────────────${RESET}"
echo -e " ${BOLD}1)${RESET} List versions"
echo -e " ${BOLD}2)${RESET} Install a version"
echo -e " ${BOLD}3)${RESET} Remove a version"
echo -e " ${BOLD}q)${RESET} Quit"
echo ""
read -rp "Choose [1-3/q]: " choice
case "$choice" in
1) cmd_list false ;;
2) cmd_install_interactive ;;
3) cmd_remove_interactive ;;
q|Q) echo "Bye."; exit 0 ;;
*) echo -e "${RED}Invalid choice.${RESET}" ;;
esac
done
}
# List command
cmd_list() {
local show_all="${1:-false}"
echo -e "\n${BLUE}Fetching remote versions...${RESET}"
local remote_versions
remote_versions=$(fetch_remote_versions) || return 1
local total
total=$(echo "$remote_versions" | wc -l | tr -d ' ')
local local_versions
local_versions=$(list_local_versions)
local active
active=$(get_active_version)
if [ "$show_all" = "false" ]; then
remote_versions=$(echo "$remote_versions" | tail -20)
echo -e "\n${BOLD}Available versions${RESET} ${DIM}(latest 20 of $total, use --all for full list)${RESET}\n"
else
echo -e "\n${BOLD}All available versions${RESET} ${DIM}($total total)${RESET}\n"
fi
while IFS= read -r v; do
local marker=" "
local color=""
local suffix=""
if echo "$local_versions" | grep -qx "$v"; then
marker="${GREEN}●${RESET} "
suffix=" ${DIM}[installed]${RESET}"
fi
if [ "$v" = "$active" ]; then
marker="${CYAN}▸${RESET} "
suffix=" ${CYAN}[active]${RESET}"
fi
echo -e " ${marker}${v}${suffix}"
done <<< "$remote_versions"
if [ -n "$local_versions" ]; then
echo ""
echo -e "${DIM}● = installed ▸ = active${RESET}"
fi
}
# Interactive install
cmd_install_interactive() {
echo -e "\n${BLUE}Fetching remote versions...${RESET}"
local remote_versions
remote_versions=$(fetch_remote_versions) || return 1
local local_versions
local_versions=$(list_local_versions)
# Show last 20 versions with numbers
local versions_arr=()
while IFS= read -r v; do
versions_arr+=("$v")
done <<< "$(echo "$remote_versions" | tail -20)"
echo -e "\n${BOLD}Select a version to install:${RESET}\n"
for i in "${!versions_arr[@]}"; do
local v="${versions_arr[$i]}"
local suffix=""
if echo "$local_versions" | grep -qx "$v"; then
suffix=" ${DIM}[installed]${RESET}"
fi
printf " ${BOLD}%2d)${RESET} %s%b\n" $((i + 1)) "$v" "$suffix"
done
echo ""
read -rp "Enter number or version string (q to cancel): " input
[[ "$input" =~ ^[Qq]$ ]] && return 0
local target
if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le "${#versions_arr[@]}" ]; then
target="${versions_arr[$((input - 1))]}"
elif [[ "$input" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
target="$input"
else
echo -e "${RED}Invalid selection.${RESET}"
return 1
fi
install_version "$target"
}
# Interactive remove
cmd_remove_interactive() {
local local_versions
local_versions=$(list_local_versions)
if [ -z "$local_versions" ]; then
echo -e "\n${YELLOW}No local versions installed.${RESET}"
return 0
fi
local versions_arr=()
while IFS= read -r v; do
versions_arr+=("$v")
done <<< "$local_versions"
local active
active=$(get_active_version)
echo -e "\n${BOLD}Installed versions:${RESET}\n"
for i in "${!versions_arr[@]}"; do
local v="${versions_arr[$i]}"
local suffix=""
if [ "$v" = "$active" ]; then
suffix=" ${CYAN}[active]${RESET}"
fi
local size
size=$(du -h "$VERSIONS_DIR/$v" 2>/dev/null | awk '{print $1}')
printf " ${BOLD}%2d)${RESET} %s ${DIM}%s${RESET}%b\n" $((i + 1)) "$v" "$size" "$suffix"
done
echo ""
read -rp "Enter number to remove (q to cancel): " input
[[ "$input" =~ ^[Qq]$ ]] && return 0
if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le "${#versions_arr[@]}" ]; then
remove_version "${versions_arr[$((input - 1))]}"
else
echo -e "${RED}Invalid selection.${RESET}"
return 1
fi
}
# Main
case "${1:-}" in
list)
[[ "${2:-}" = "--all" ]] && cmd_list true || cmd_list false
;;
install)
if [ -n "${2:-}" ]; then
install_version "$2"
else
cmd_install_interactive
fi
;;
remove)
if [ -n "${2:-}" ]; then
remove_version "$2"
else
cmd_remove_interactive
fi
;;
help|--help|-h)
usage
;;
"")
interactive_menu
;;
*)
echo -e "${RED}Unknown command: $1${RESET}" >&2
usage
exit 1
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment