#!/bin/bash set -e # Parse command line arguments TARGET="$1" # Optional target parameter # Validate target if provided if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then echo "Usage: $0 [stable|latest|VERSION]" >&2 exit 1 fi # Refuse to run under sudo from a regular user's shell. This installer puts # everything under $HOME, which under sudo typically resolves to root's home: # the binary lands in /root/.local/bin (or is left root-owned in the user's # home, depending on the distro's sudo configuration), and the 'claude' # command is then not found in the user's own shell. Plain root with no sudo # (containers, CI, root-only systems) is unaffected by this check. if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && [ -z "${CLAUDE_INSTALL_ALLOW_SUDO:-}" ]; then echo "Error: do not run this installer with sudo." >&2 echo "" >&2 echo "Claude Code installs into your home directory and does not need root access." >&2 echo "With sudo, the installation would go into root's home directory instead of" >&2 echo "yours, and the 'claude' command would not work from your own shell." >&2 echo "" >&2 echo "Please re-run the same command without sudo, e.g.:" >&2 # pinned-dep-allow: display-only guidance text in an error message, not an executed install; install.sh is Anthropic's own installer echo " curl -fsSL https://claude.ai/install.sh | bash" >&2 echo "" >&2 echo "To intentionally install Claude Code for the root user, re-run with" >&2 echo "CLAUDE_INSTALL_ALLOW_SUDO=1 set in the installer's environment, e.g.:" >&2 # pinned-dep-allow: display-only guidance text in an error message, not an executed install; install.sh is Anthropic's own installer echo " curl -fsSL https://claude.ai/install.sh | sudo CLAUDE_INSTALL_ALLOW_SUDO=1 bash" >&2 exit 1 fi DOWNLOAD_BASE_URL="https://downloads.claude.ai/claude-code-releases" DOWNLOAD_DIR="$HOME/.claude/downloads" # Check for required dependencies DOWNLOADER="" if command -v curl >/dev/null 2>&1; then DOWNLOADER="curl" elif command -v wget >/dev/null 2>&1; then DOWNLOADER="wget" else echo "Either curl or wget is required but neither is installed" >&2 exit 1 fi # Check if jq is available (optional) HAS_JQ=false if command -v jq >/dev/null 2>&1; then HAS_JQ=true fi # Download function that works with both curl and wget download_file() { local url="$1" local output="$2" if [ "$DOWNLOADER" = "curl" ]; then if [ -n "$output" ]; then curl -fsSL -o "$output" "$url" else curl -fsSL "$url" fi elif [ "$DOWNLOADER" = "wget" ]; then if [ -n "$output" ]; then wget -q -O "$output" "$url" else wget -q -O - "$url" fi else return 1 fi } # Simple JSON parser for extracting checksum when jq is not available get_checksum_from_manifest() { local json="$1" local platform="$2" # Normalize JSON to single line and extract checksum json=$(echo "$json" | tr -d '\n\r\t' | sed 's/ \+/ /g') # Extract checksum for platform using bash regex if [[ $json =~ \"$platform\"[^}]*\"checksum\"[[:space:]]*:[[:space:]]*\"([a-f0-9]{64})\" ]]; then echo "${BASH_REMATCH[1]}" return 0 fi return 1 } # Detect platform case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; MINGW*|MSYS*|CYGWIN*) echo "Windows is not supported by this script. See https://code.claude.com/docs for installation options." >&2; exit 1 ;; *) echo "Unsupported operating system: $(uname -s). See https://code.claude.com/docs for supported platforms." >&2; exit 1 ;; esac case "$(uname -m)" in x86_64|amd64) arch="x64" ;; arm64|aarch64) arch="arm64" ;; *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; esac # Detect Rosetta 2 on macOS: if the shell is running as x64 under Rosetta on an ARM Mac, # download the native arm64 binary instead of the x64 one if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then arch="arm64" fi fi # Check for musl on Linux and adjust platform accordingly 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 platform="linux-${arch}-musl" else platform="linux-${arch}" fi else platform="${os}-${arch}" fi mkdir -p "$DOWNLOAD_DIR" # Always download latest version (which has the most up-to-date installer) version=$(download_file "$DOWNLOAD_BASE_URL/latest") # Reject non-version content (e.g. an HTML error page) before it reaches the manifest URL if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then echo "Failed to get a valid version from downloads.claude.ai (got unexpected content)." >&2 echo "This can happen if the download service is unreachable or not available in your region - see https://www.anthropic.com/supported-countries" >&2 exit 1 fi # Download manifest and extract checksum manifest_json=$(download_file "$DOWNLOAD_BASE_URL/$version/manifest.json") # Use jq if available, otherwise fall back to pure bash parsing if [ "$HAS_JQ" = true ]; then checksum=$(echo "$manifest_json" | jq -r ".platforms[\"$platform\"].checksum // empty") else checksum=$(get_checksum_from_manifest "$manifest_json" "$platform") fi # Validate checksum format (SHA256 = 64 hex characters) if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then echo "Platform $platform not found in manifest" >&2 exit 1 fi # Download and verify binary_path="$DOWNLOAD_DIR/claude-$version-$platform" if ! download_file "$DOWNLOAD_BASE_URL/$version/$platform/claude" "$binary_path"; then echo "Download failed" >&2 rm -f "$binary_path" exit 1 fi # Pick the right checksum tool if [ "$os" = "darwin" ]; then actual=$(shasum -a 256 "$binary_path" | cut -d' ' -f1) else actual=$(sha256sum "$binary_path" | cut -d' ' -f1) fi if [ "$actual" != "$checksum" ]; then echo "Checksum verification failed" >&2 rm -f "$binary_path" exit 1 fi chmod +x "$binary_path" # Run claude install to set up launcher and shell integration echo "Setting up Claude Code..." install_code=0 "$binary_path" install ${TARGET:+"$TARGET"} || install_code=$? # Clean up downloaded file rm -f "$binary_path" if [ "$install_code" -ne 0 ]; then # A signal death mid-install kills the binary's TUI with no chance to # restore the terminal, leaving the user's shell in raw mode (typed # characters stop echoing). Restore it before printing anything. if [ "$install_code" -ge 128 ] && [ -t 0 ]; then stty sane 2>/dev/null || true fi # Red when stderr is a terminal, so the explanation stands out from the # surrounding install output; plain when piped or captured red="" reset="" if [ -t 2 ]; then red=$'\033[31m' reset=$'\033[0m' fi # Signal deaths (exit code 128+N) print nothing of their own — bash shows # only e.g. "Killed". 137 = SIGKILL, which on Linux is almost always the # kernel OOM killer on small hosts; macOS has no equivalent OOM kill, so # the out-of-memory explanation is Linux-only. if [ "$install_code" -eq 137 ] && [ "$os" = "linux" ]; then echo "${red}Installation was killed before it could finish (exit code 137). This usually means the system ran out of memory.${reset}" >&2 echo "${red}Claude Code needs roughly 512MB of free memory to install. Free up memory, then run this script again.${reset}" >&2 elif [ "$install_code" -ge 128 ]; then echo "${red}Installation was killed before it could finish (exit code $install_code).${reset}" >&2 fi exit "$install_code" fi echo "" echo "✅ Installation complete!" echo ""