Content is user-generated and unverified.
#!/usr/bin/env bash # ============================================================================= # setup-claude-vm.sh # Prepares an Ubuntu 24.04 VM for Claude agent/coding sessions. # Idempotent: safe to re-run; updates existing installations. # Run as a regular user with sudo access (NOT as root). # ============================================================================= set -euo pipefail GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; BLUE='\033[0;34m'; NC='\033[0m' log() { echo -e "${GREEN}[+]${NC} $1"; } info() { echo -e "${BLUE}[~]${NC} $1"; } section() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; \ echo -e "${CYAN} $1${NC}"; \ echo -e "${CYAN}══════════════════════════════════════════${NC}"; } warn() { echo -e "${YELLOW}[!]${NC} $1"; } die() { echo -e "${RED}[✗]${NC} $1"; exit 1; } [[ $EUID -eq 0 ]] && die "Do not run as root. Use a regular user with sudo access." # Track what actually changed for the summary CHANGED=() mark_changed() { CHANGED+=("$1"); } # ============================================================================= # 1. SYSTEM PACKAGES (including tmux and screen) # ============================================================================= section "1. System packages" log "Updating apt package lists..." sudo apt-get update -qq log "Installing / upgrading system packages..." sudo apt-get install -y \ build-essential \ curl wget git \ python3 python3-pip \ unzip zip jq \ ca-certificates \ gnupg lsb-release \ software-properties-common \ mercurial bison \ gcc make \ lsof htop \ tmux screen \ vim nano # Upgrade only already-installed packages (safe, no new installs) sudo apt-get upgrade -y mark_changed "system packages (apt upgraded)" log "System packages up to date." # ============================================================================= # 2. DOCKER (official repository) # ============================================================================= section "2. Docker (official repository)" if ! command -v docker &>/dev/null; then log "Docker not found — installing from official repository..." sudo apt-get remove -y docker.io docker-doc docker-compose docker-compose-v2 \ podman-docker containerd runc 2>/dev/null || true sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -qq sudo apt-get install -y \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin sudo usermod -aG docker "$USER" sudo systemctl enable --now docker mark_changed "Docker (fresh install)" log "Docker $(docker --version) installed." else info "Docker already installed — upgrading..." sudo apt-get install -y --only-upgrade \ docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin 2>/dev/null || true mark_changed "Docker (upgraded)" log "Docker $(docker --version) up to date." fi # ============================================================================= # 3. NVM — update or install # Updating NVM: re-running the install script is the official method. # It detects ~/.nvm and does a git pull instead of a fresh clone. # ============================================================================= section "3. NVM" NVM_LATEST=$(curl -fsSL https://api.github.com/repos/nvm-sh/nvm/releases/latest \ | jq -r '.tag_name') if [ -d "$HOME/.nvm" ]; then NVM_CURRENT=$(cat "$HOME/.nvm/package.json" 2>/dev/null | jq -r '.version // "unknown"' || echo "unknown") info "NVM already installed (v${NVM_CURRENT}) — updating to ${NVM_LATEST}..." else info "NVM not found — installing ${NVM_LATEST}..." fi curl -o- "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_LATEST}/install.sh" | bash mark_changed "NVM ${NVM_LATEST}" export NVM_DIR="$HOME/.nvm" # shellcheck source=/dev/null [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" log "NVM $(nvm --version) ready." # ============================================================================= # 4. NODE.JS LTS — install latest LTS or upgrade if newer one is available # nvm install --lts --reinstall-packages-from=default migrates global packages. # ============================================================================= section "4. Node.js LTS" CURRENT_NODE=$(node --version 2>/dev/null || echo "none") LATEST_LTS=$(nvm version-remote --lts 2>/dev/null || echo "unknown") if [ "$CURRENT_NODE" = "$LATEST_LTS" ]; then info "Node.js ${CURRENT_NODE} is already the latest LTS — skipping." else if [ "$CURRENT_NODE" = "none" ]; then log "Installing Node.js LTS ${LATEST_LTS}..." nvm install --lts else log "Upgrading Node.js from ${CURRENT_NODE} to ${LATEST_LTS} (migrating global packages)..." nvm install --lts --reinstall-packages-from=default fi mark_changed "Node.js ${LATEST_LTS}" fi nvm use --lts nvm alias default 'lts/*' log "Node $(node --version), npm $(npm --version)" # ============================================================================= # 5. GVM — update or install # GVM lives in ~/.gvm which is a plain git checkout. # Updating: git pull on the repo. New Go versions are installed separately. # ============================================================================= section "5. GVM" if [ -d "$HOME/.gvm" ]; then info "GVM already installed — pulling latest changes..." (cd "$HOME/.gvm" && git pull --quiet origin master 2>/dev/null) \ && log "GVM updated." \ || warn "GVM git pull failed — continuing with existing version." mark_changed "GVM (git pull)" else info "GVM not found — installing..." bash < <(curl -fsSL https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) mark_changed "GVM (fresh install)" fi # shellcheck source=/dev/null source "$HOME/.gvm/scripts/gvm" # ============================================================================= # 6. GO — install latest stable if not already present # ============================================================================= section "6. Go (latest stable)" GO_LATEST=$(curl -fsSL 'https://go.dev/dl/?mode=json' | jq -r '.[0].version') # gvm list prints installed versions; grep for the exact version string if gvm list 2>/dev/null | grep -qw "${GO_LATEST}"; then info "Go ${GO_LATEST} already installed — switching to it as default." else log "Installing ${GO_LATEST} (binary)..." gvm install "${GO_LATEST}" --binary mark_changed "Go ${GO_LATEST}" fi gvm use "${GO_LATEST}" --default log "$(go version)" # ============================================================================= # 7. UV — update or install # uv has a built-in self-update command. # ============================================================================= section "7. uv + Python" export PATH="$HOME/.local/bin:$PATH" if command -v uv &>/dev/null; then UV_BEFORE=$(uv --version) info "uv already installed (${UV_BEFORE}) — updating..." uv self update UV_AFTER=$(uv --version) if [ "$UV_BEFORE" != "$UV_AFTER" ]; then mark_changed "uv ${UV_AFTER}" log "uv updated: ${UV_BEFORE} → ${UV_AFTER}" else info "uv is already up to date." fi else log "Installing uv..." curl -LsSf https://astral.sh/uv/install.sh | sh mark_changed "uv (fresh install)" fi log "Installing / updating latest stable CPython via uv..." uv python install log "$(uv --version)" uv python list # ============================================================================= # 8. CLAUDE CODE — native Bun installer (idempotent, handles updates) # Re-running the official install script updates if a newer version is available. # ============================================================================= section "8. Claude Code" CLAUDE_BEFORE=$(claude --version 2>/dev/null || echo "not installed") log "Running native Claude Code installer (installs or updates)..." curl -fsSL https://claude.ai/install.sh | bash export PATH="$HOME/.local/bin:$PATH" CLAUDE_AFTER=$(claude --version 2>/dev/null || echo "unknown") if [ "$CLAUDE_BEFORE" != "$CLAUDE_AFTER" ]; then mark_changed "Claude Code ${CLAUDE_AFTER}" log "Claude Code: ${CLAUDE_BEFORE} → ${CLAUDE_AFTER}" else info "Claude Code ${CLAUDE_AFTER} already up to date." fi # ============================================================================= # 9. AUTOMATIC UPDATES (unattended-upgrades) # Config is written every run — it's idempotent, same content each time. # ============================================================================= section "9. Automatic security updates" sudo apt-get install -y unattended-upgrades apt-listchanges update-notifier-common sudo tee /etc/apt/apt.conf.d/50unattended-upgrades > /dev/null << 'EOF' Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}"; "${distro_id}:${distro_codename}-security"; "${distro_id}ESMApps:${distro_codename}-apps-security"; "${distro_id}ESM:${distro_codename}-infra-security"; "${distro_id}:${distro_codename}-updates"; }; Unattended-Upgrade::Package-Blacklist {}; Unattended-Upgrade::DevRelease "false"; Unattended-Upgrade::AutoFixInterruptedDpkg "true"; Unattended-Upgrade::MinimalSteps "true"; Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true"; Unattended-Upgrade::Automatic-Reboot "false"; Unattended-Upgrade::Automatic-Reboot-WithUsers "false"; EOF sudo tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null << 'EOF' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Download-Upgradeable-Packages "1"; APT::Periodic::AutocleanInterval "7"; APT::Periodic::Unattended-Upgrade "1"; EOF sudo systemctl enable --now unattended-upgrades log "Automatic updates config applied." # ============================================================================= # 10. REBOOT-REQUIRED NOTIFICATION ON LOGIN # ============================================================================= section "10. Reboot-required login notification" sudo tee /etc/update-motd.d/98-reboot-required > /dev/null << 'EOF' #!/bin/sh if [ -f /run/reboot-required ]; then echo "" echo "╔══════════════════════════════════════════════════════╗" echo "║ *** REBOOT REQUIRED *** ║" echo "║ A system restart is needed to apply updates. ║" if [ -f /run/reboot-required.pkgs ]; then echo "║ Packages requiring reboot: ║" while IFS= read -r pkg; do printf "║ - %-48s║\n" "$pkg" done < /run/reboot-required.pkgs fi echo "║ Run: sudo reboot ║" echo "╚══════════════════════════════════════════════════════╝" echo "" fi EOF sudo chmod +x /etc/update-motd.d/98-reboot-required BASHRC_REBOOT_MARKER="# reboot-required check" if ! grep -q "$BASHRC_REBOOT_MARKER" "$HOME/.bashrc" 2>/dev/null; then cat >> "$HOME/.bashrc" << 'BASHEOF' # reboot-required check if [ -f /run/reboot-required ]; then echo -e "\033[1;31m[!] REBOOT REQUIRED — run: sudo reboot\033[0m" fi BASHEOF fi log "Reboot-required notification configured." # ============================================================================= # 11. SHELL INITIALISATION # Markers prevent duplicate entries on re-runs. # ============================================================================= section "11. Shell initialisation" sudo tee /etc/profile.d/claude-tools.sh > /dev/null << 'EOF' # Claude VM — tool initialisation (sourced by all interactive login shells) # NVM export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # GVM [ -s "$HOME/.gvm/scripts/gvm" ] && source "$HOME/.gvm/scripts/gvm" # uv + local bins (includes Claude Code native binary) export PATH="$HOME/.local/bin:$PATH" EOF BASHRC_MARKER="# claude-tools init" if ! grep -q "$BASHRC_MARKER" "$HOME/.bashrc" 2>/dev/null; then cat >> "$HOME/.bashrc" << 'BASHEOF' # claude-tools init export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$HOME/.gvm/scripts/gvm" ] && source "$HOME/.gvm/scripts/gvm" export PATH="$HOME/.local/bin:$PATH" BASHEOF log "Shell init written to ~/.bashrc." else info "Shell init already in ~/.bashrc — skipping." fi # ============================================================================= # 12. GLOBAL CLAUDE.md (regenerated on every run with current versions) # ============================================================================= section "12. Global CLAUDE.md" mkdir -p "$HOME/.claude" GOVERSION=$(go version 2>/dev/null || echo "unknown") NODEVERSION=$(node --version 2>/dev/null || echo "unknown") NPMVERSION=$(npm --version 2>/dev/null || echo "unknown") UVVERSION=$(uv --version 2>/dev/null || echo "unknown") PYTHONLATEST=$(uv python list 2>/dev/null | grep -oP 'cpython-\K[0-9.]+' | sort -V | tail -1 || echo "unknown") DOCKERVERSION=$(docker --version 2>/dev/null || echo "unknown") COMPOSEVERSION=$(docker compose version 2>/dev/null || echo "unknown") CLAUDEVERSION=$(claude --version 2>/dev/null || echo "run: claude auth") cat > "$HOME/.claude/CLAUDE.md" << EOF # Claude Agent Environment — $(hostname) Generated: $(date -u '+%Y-%m-%d %H:%M UTC') by setup-claude-vm.sh ## Operating System - Distribution: Ubuntu 24.04 LTS (Noble Numbat) - Kernel: $(uname -r) - Architecture: $(uname -m) - User: $(whoami) - Home: $HOME ## Claude Code (this tool) - Version: ${CLAUDEVERSION} - Binary: ~/.local/bin/claude (native Bun installer, auto-updates on launch) - Authenticate: \`claude auth\` - Interactive: \`claude\` - One-shot: \`claude -p "your prompt"\` - This file is the global CLAUDE.md, read before any project-level CLAUDE.md ## Available Runtimes ### Go - Version manager: GVM (~/.gvm/) — updated via: cd ~/.gvm && git pull - Active: ${GOVERSION} - Switch version: \`gvm use go<version>\` - List installed: \`gvm list\` - Install new: \`gvm install go<version> --binary\` - GOPATH: \$(go env GOPATH) - GOROOT: \$(go env GOROOT) ### Node.js - Version manager: NVM (~/.nvm/) — updated via: re-run install script - Active: ${NODEVERSION} / npm ${NPMVERSION} - Switch version: \`nvm use <version>\` - List installed: \`nvm list\` - Install new: \`nvm install <version>\` - Migrate globals: \`nvm install <version> --reinstall-packages-from=default\` - Global npm installs work without sudo (user-space under NVM) ### Python - Manager: uv (~/.local/bin/uv) — updated via: uv self update - uv version: ${UVVERSION} - Latest managed CPython: ${PYTHONLATEST} - Create venv: \`uv venv .venv\` - Activate venv: \`source .venv/bin/activate\` - Install pkg: \`uv pip install <pkg>\` - Run script: \`uv run script.py\` - System Python: /usr/bin/python3 — DO NOT MODIFY (used by apt/system tools) ### Docker - Version: ${DOCKERVERSION} - Compose: ${COMPOSEVERSION} - Compose command: \`docker compose\` (plugin syntax, NOT docker-compose) - User $(whoami) is in the docker group — needs re-login to use without sudo - Buildx: \`docker buildx build --platform linux/amd64,linux/arm64 .\` - Daemon: \`sudo systemctl start|stop|status docker\` ## Terminal Multiplexers - tmux $(tmux -V): \`tmux new -s <n>\` | attach: \`tmux attach -t <n>\` | list: \`tmux ls\` - screen $(screen --version | head -1 | awk '{print $1,$2,$3}'): \`screen -S <n>\` | attach: \`screen -r <n>\` - Recommended for long-running agent tasks — survives SSH disconnections ## Shell Initialisation Tools are sourced via: - /etc/profile.d/claude-tools.sh (all login shells) - ~/.bashrc (interactive bash, tmux/screen panes) Non-interactive scripts must explicitly source ~/.bashrc or manually set NVM_DIR, source gvm scripts, and add ~/.local/bin to PATH. ## Updating this environment Re-run setup-claude-vm.sh — it is idempotent and will: - apt upgrade all system packages - Pull latest NVM (git pull via official installer) - Pull latest GVM (git pull on ~/.gvm) - Install newer Node.js LTS if available (migrates global npm packages) - Install newer Go if available via go.dev API - uv self update - Update Claude Code via native installer - Regenerate this CLAUDE.md with current versions ## Automatic Updates - Security and system updates applied nightly via unattended-upgrades - Auto-reboot DISABLED — reboot manually when prompted - Check pending reboot: \`cat /run/reboot-required.pkgs\` - Check pending upgrades: \`sudo apt list --upgradable\` - Apply reboot: \`sudo reboot\` ## Package Managers Summary | Runtime | Manager | Notes | |-----------|-------------------|-------------------------------------| | System | apt / dpkg | sudo required | | Go | go mod | standard modules | | Node | npm | no sudo needed (NVM user-space) | | Python | uv pip / uv add | never pip on system Python | | Containers| docker compose | plugin syntax | ## Sudo Access - User $(whoami) has sudo access - Use \`sudo apt install\` for system-level packages ## Agent Session Notes - uv for all Python work; never pip install on system Python - For Go projects: check go.mod required version, switch with gvm if needed - NVM/GVM not available in non-interactive shells without explicit sourcing - Docker without sudo requires re-login after initial setup - Use tmux/screen for tasks that may outlive a connection - Verify tool availability: \`which <tool>\` or \`command -v <tool>\` EOF log "CLAUDE.md regenerated at $HOME/.claude/CLAUDE.md" # ============================================================================= # DONE — Summary # ============================================================================= GOVERSION_SHORT=$(go version 2>/dev/null | awk '{print $3}' || echo "unknown") echo "" echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Done! ║${NC}" echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}" echo "" printf " %-20s %s\n" "Node.js:" "$(node --version) via NVM ${NVM_LATEST}" printf " %-20s %s\n" "Go:" "${GOVERSION_SHORT} via GVM" printf " %-20s %s\n" "Python:" "CPython ${PYTHONLATEST} via $(uv --version)" printf " %-20s %s\n" "Docker:" "$(docker --version | awk '{print $3}' | tr -d ',')" printf " %-20s %s\n" "Claude Code:" "$(claude --version 2>/dev/null || echo 'installed')" printf " %-20s %s\n" "tmux:" "$(tmux -V)" printf " %-20s %s\n" "CLAUDE.md:" "$HOME/.claude/CLAUDE.md" echo "" if [ ${#CHANGED[@]} -gt 0 ]; then echo -e "${CYAN} Changes applied this run:${NC}" for item in "${CHANGED[@]}"; do echo " • $item" done echo "" fi warn "NEXT STEPS (first run only):" warn " 1. Log out and back in → docker group takes effect (no sudo needed)" warn " 2. Run: claude auth → authenticate Claude Code" warn " 3. Run: source ~/.bashrc → activate tools in this current session"
Content is user-generated and unverified.
    setup-claude-vm.sh: Ubuntu VM Setup for Claude Agent Development | Claude