https://claude.ai/install.sh

1) Overview

This script downloads a prebuilt Claude Code CLI binary for your OS/CPU from a Google Cloud Storage bucket, verifies it with a SHA-256 checksum from a manifest, marks it executable, then runs the binary’s own install subcommand to perform the actual installation/shell integration. Finally it deletes the downloaded binary.


2) Step-by-step breakdown (by section / function)

Strict mode / argument handling

  • set -e (line 3): aborts the script on most command failures.
  • TARGET="$1" (line 6): optional argument passed through to the CLI installer (stable|latest|VERSION).
  • Target validation (lines 9–13): if TARGET is provided, it must match:
  • stable or latest, or
  • semver like 1.2.3 or 1.2.3-suffix Otherwise it exits with a usage message.

Constants and local download location

  • GCS bucket base URL (line 15):
    https://storage.googleapis.com/.../claude-code-releases
  • Download directory (line 16):
    ~/.claude/downloads

Dependency checks

  • Downloader selection (lines 19–29): requires either curl or wget. Prefers curl if present.
  • Optional jq (lines 32–36): if installed, used to parse JSON safely; otherwise it falls back to a regex parser.

Download helper

  • download_file(url, output) (lines 39–63):
  • Uses curl -fsSL (quiet-ish, fail on HTTP errors, follow redirects) or wget -q.
  • If output is provided, saves to file; otherwise prints to stdout.

JSON checksum extraction (fallback)

  • get_checksum_from_manifest(json, platform) (lines 66–82):
  • Normalizes JSON into one line (line 70).
  • Uses a bash regex to extract a 64-hex-character checksum for a given platform key (line 73).
  • This is less robust than jq but constrained to matching a SHA-256-like pattern.

Platform detection

  • OS detection via uname -s (lines 85–89):
  • Darwindarwin
  • Linuxlinux
  • Other → exit (“Windows is not supported”)
  • Architecture via uname -m (lines 91–94):
  • x86_64/amd64x64
  • arm64/aarch64arm64
  • Rosetta handling (macOS) (lines 99–104):
  • If running under Rosetta (sysctl.proc_translated=1), it switches to downloading arm64 even if uname -m reports x64.
  • musl vs glibc (Linux) (lines 107–114):
  • Detects musl via presence of musl libc files or ldd /bin/ls | grep musl.
  • Sets platform to linux-${arch}-musl or linux-${arch} accordingly.
  • Creates download directory (line 118): mkdir -p ~/.claude/downloads

Version + manifest retrieval

  • Always fetches “latest” version string (line 121):
    version=$(download_file "$GCS_BUCKET/latest")
    Note: Despite the comment and TARGET support, this script always uses the “latest” file to decide what binary to download; TARGET is only passed later to claude install.
  • Downloads manifest.json for that version (line 124):
    .../$version/manifest.json
  • Extract checksum:
  • with jq (line 128): .platforms["$platform"].checksum
  • otherwise with get_checksum_from_manifest (line 130)

Checksum validation and binary download

  • Verifies checksum looks like SHA-256 hex (lines 134–138). If not, exits “Platform … not found in manifest”.
  • Downloads the binary to a versioned path (lines 141–147):
    ~/.claude/downloads/claude-$version-$platform from
    .../$version/$platform/claude
  • Computes SHA-256:
  • macOS: shasum -a 256 (line 153)
  • Linux: sha256sum (line 155)
  • Compares computed vs manifest checksum (lines 158–163); deletes file and exits on mismatch.
  • Marks executable (line 166): chmod +x

Executes the downloaded installer

  • Runs: "$binary_path" install ${TARGET:+"$TARGET"} (line 170)
  • This is the key action: it executes newly-downloaded code and delegates all “real installation” tasks to it.
  • Deletes the downloaded binary after install (line 173).
  • Prints completion message (lines 175–178). (The checkmark glyph appears mis-encoded as ✠in your pasted script.)

3) What it installs/modifies

Directly, this shell script: - Creates directory: - ~/.claude/downloads (line 118) - Writes and then deletes a downloaded binary: - ~/.claude/downloads/claude-$version-$platform (lines 141–147, 173) - Executes that binary’s install command (line 170)

Everything else depends on what claude <version> install does, which is not shown here. Based on the wording (“launcher and shell integration”, line 168), likely changes include: - Installing a launcher somewhere on PATH (commonly ~/.local/bin, /usr/local/bin, etc.) - Modifying shell init files (e.g., ~/.bashrc, ~/.zshrc, ~/.profile) to add PATH entries or completions - Creating additional ~/.claude/... state/config directories

But those are not visible in this script, because it hands off installation to the downloaded binary.


4) Network activity

All network requests go to Google Cloud Storage under the bucket base (line 15):

  1. GET https://storage.googleapis.com/.../claude-code-releases/latest (line 121)
    Returns a version string.
  2. GET https://storage.googleapis.com/.../claude-code-releases/$version/manifest.json (line 124)
    Returns platform checksums.
  3. GET https://storage.googleapis.com/.../claude-code-releases/$version/$platform/claude (line 145)
    Downloads the executable.

No explicit data upload is performed by this script. However, once it runs the downloaded binary (line 170), that binary may perform additional network activity.


5) Permissions & privilege escalation

  • This script does not invoke sudo, does not request root, and writes only under $HOME (lines 16, 118, 141).
  • It executes an unprivileged install step by running the downloaded binary (line 170).

Whether elevated privileges are needed depends on what the claude install subcommand does: - If it installs into system directories like /usr/local/bin, it may prompt for sudo internally or instruct the user to do so. - From this script alone, no privilege escalation is performed.


6) Potential concerns (security notes)

  1. Executes downloaded code
  2. The main risk is line 170: it runs a binary fetched over the network. This is the core “curl | bash” concern, even with checksum verification.

  3. Checksum trust model relies on the same origin

  4. The script downloads both the binary and its checksum manifest from the same bucket (lines 124, 145).
  5. If the bucket or serving path is compromised, an attacker could replace both the binary and manifest so the checksum still “verifies”.

  6. TARGET does not control what binary is downloaded

  7. Even if you pass stable or a version, the script still downloads whatever latest points to (line 121). TARGET only affects the behavior of the installer binary once executed (line 170).
  8. This may be surprising if you expected TARGET to pin the downloaded version.

  9. Fallback JSON parsing without jq is brittle

  10. The regex parser (function get_checksum_from_manifest, lines 66–82) can fail on unexpected JSON formatting/ordering. It’s not obviously exploitable by itself given it only extracts a 64-hex checksum, but it is less reliable than jq.

  11. ldd /bin/ls invocation

  12. On some minimal systems ldd may behave oddly or be absent; errors are piped and grepped (line 109). It’s not a direct security issue, but it’s an extra moving part.

  13. No signature verification / provenance

  14. There is no GPG/sigstore/cosign verification. Only a checksum from a remote manifest.

7) Verdict

This is a typical bootstrap installer: detect platform → download manifest → download binary → verify SHA-256 → execute the binary’s installer. There are no obvious malicious behaviors in the shell script itself, and it doesn’t use sudo directly.

The main red flag is inherent to the pattern: it executes a network-downloaded binary, and the checksum is fetched from the same remote location as the binary, so it’s not a strong independent authenticity guarantee. The actual security posture ultimately depends on the integrity and behavior of the downloaded claude binary’s install subcommand.

View raw script
#!/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

GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/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" ;;
    *) echo "Windows is not supported" >&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 "$GCS_BUCKET/latest")

# Download manifest and extract checksum
manifest_json=$(download_file "$GCS_BUCKET/$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 "$GCS_BUCKET/$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..."
"$binary_path" install ${TARGET:+"$TARGET"}

# Clean up downloaded file
rm -f "$binary_path"

echo ""
echo "✅ Installation complete!"
echo ""