Created
July 10, 2026 22:17
-
-
Save zcutlip/410c009f183e6b6a09ff42a3e4bd3ef8 to your computer and use it in GitHub Desktop.
Stage 0 bootstrap script for provisioning a fresh Mac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # bootstrap0.sh - Stage 0 bootstrap for fresh Mac | |
| # | |
| # Breaks the SSH/1Password/dotfiles dependency cycle by front-loading | |
| # 1Password installation before any private git clones. This script is | |
| # public-safe — it contains only an opaque UUID-based 1Password reference, | |
| # no git server addresses or paths. | |
| # | |
| # Usage: | |
| # curl -fsSL https://raw.githubusercontent.com/<you>/bootstrap/main/bootstrap0.sh | bash | |
| # OR | |
| # bash bootstrap0.sh | |
| # | |
| set -e # Exit on error | |
| set -u # Exit on undefined variable | |
| # Colors for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| # Configuration | |
| # This is a UUID-based op:// reference to a 1Password secure note. | |
| # It's opaque and useless without being signed into the account. | |
| # To set it up: | |
| # 1. Create a secure note in 1Password with the body: | |
| # DOTFILES_REPO="git@<host>:/<path>/dotfiles.git" | |
| # 2. Get its UUID reference and paste it here: | |
| # op item get "dotfiles-bootstrap" --format json | jq -r '"\(.vault.id)/\(.id)"' | |
| # 3. The format is: op://<vault-uuid>/<item-uuid>/notesPlain | |
| CONFIG_REF="op://kvj3nhzsnosk57qofp4u45rbsy/4zqr26nxshpln3lpydl5xu3ebi/notesPlain" | |
| DOTFILES_DIR="$HOME/src/dotfiles" | |
| CONFIG_FILE="${BOOTSTRAP0_CONFIG:-$HOME/.config/bootstrap0.conf}" | |
| log_info() { | |
| echo -e "${BLUE}==>${NC} $*" | |
| } | |
| log_success() { | |
| echo -e "${GREEN}✓${NC} $*" | |
| } | |
| log_warn() { | |
| echo -e "${YELLOW}⚠${NC} $*" | |
| } | |
| log_error() { | |
| echo -e "${RED}✗${NC} $*" | |
| } | |
| # Step 1: Install Homebrew if absent | |
| install_homebrew() { | |
| log_info "Checking for Homebrew..." | |
| if command -v brew >/dev/null 2>&1; then | |
| log_success "Homebrew already installed: $(brew --version | head -n1)" | |
| return 0 | |
| fi | |
| log_info "Installing Homebrew..." | |
| /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | |
| # Add brew to PATH for current shell | |
| if [[ $(uname -m) == "arm64" ]]; then | |
| BREW_PREFIX="/opt/homebrew" | |
| else | |
| BREW_PREFIX="/usr/local" | |
| fi | |
| if [[ -x "$BREW_PREFIX/bin/brew" ]]; then | |
| eval "$($BREW_PREFIX/bin/brew shellenv)" | |
| log_success "Homebrew installed and added to PATH" | |
| else | |
| log_error "Homebrew installation failed" | |
| exit 1 | |
| fi | |
| } | |
| # Step 2: Install 1Password + CLI | |
| install_1password() { | |
| log_info "Checking for 1Password..." | |
| local needs_install=false | |
| if [[ ! -d "/Applications/1Password.app" ]]; then | |
| log_info "1Password app not found, will install" | |
| needs_install=true | |
| else | |
| log_success "1Password app already installed" | |
| fi | |
| if ! command -v op >/dev/null 2>&1; then | |
| log_info "1Password CLI not found, will install" | |
| needs_install=true | |
| else | |
| log_success "1Password CLI already installed: $(op --version)" | |
| fi | |
| if [[ "$needs_install" == "true" ]]; then | |
| log_info "Installing 1Password and 1Password CLI via Homebrew..." | |
| brew install --cask 1password 1password-cli | |
| log_success "1Password installation complete" | |
| fi | |
| } | |
| # Step 3: Pause for manual 1Password setup | |
| wait_for_1password_setup() { | |
| echo "" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| log_warn "MANUAL STEP REQUIRED: Set up 1Password SSH Agent & CLI" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "" | |
| echo "Please complete the following steps:" | |
| echo "" | |
| echo " 1. Open 1Password app" | |
| echo " 2. Sign in to your account" | |
| echo " 3. Go to: Settings → Developer" | |
| echo " 4. Enable: ${BLUE}Use the SSH agent${NC}" | |
| echo " (This will offer to add an IdentityAgent line to ~/.ssh/config)" | |
| echo " 5. Enable: ${BLUE}Integrate with 1Password CLI${NC}" | |
| echo " (This allows 'op' to authenticate via the desktop app)" | |
| echo " 6. Ensure your SSH key is unlocked/authorized in 1Password" | |
| echo "" | |
| echo "Once complete, press ENTER to continue..." | |
| # Read from /dev/tty so this works when script is piped via curl | bash | |
| read -r < /dev/tty | |
| } | |
| # Step 4: Load config from 1Password and write to local file | |
| load_config() { | |
| log_info "Loading configuration..." | |
| # Check if config already exists (re-run case) | |
| if [[ -f "$CONFIG_FILE" ]]; then | |
| log_success "Config file already exists: $CONFIG_FILE" | |
| # shellcheck disable=SC1090 | |
| source "$CONFIG_FILE" | |
| if [[ -z "${DOTFILES_REPO:-}" ]]; then | |
| log_error "Config file exists but DOTFILES_REPO is not set" | |
| exit 1 | |
| fi | |
| log_success "Loaded DOTFILES_REPO from existing config" | |
| return 0 | |
| fi | |
| # Fetch config from 1Password | |
| log_info "Fetching config from 1Password..." | |
| if [[ "$CONFIG_REF" == *"REPLACE_WITH"* ]]; then | |
| log_error "CONFIG_REF has not been configured" | |
| log_error "Please edit this script and replace the CONFIG_REF placeholder with your actual 1Password secret reference" | |
| log_error "See the comments at the top of the script for instructions" | |
| exit 1 | |
| fi | |
| local config_content | |
| if ! config_content=$(op read "$CONFIG_REF" 2>&1); then | |
| log_error "Failed to fetch config from 1Password" | |
| log_error "Error: $config_content" | |
| log_error "" | |
| log_error "Please verify:" | |
| log_error " - 1Password CLI integration is enabled (Settings → Developer)" | |
| log_error " - The secure note exists and CONFIG_REF is correct" | |
| log_error " - You are signed into 1Password" | |
| exit 1 | |
| fi | |
| # Write config to file | |
| mkdir -p "$(dirname "$CONFIG_FILE")" | |
| echo "$config_content" > "$CONFIG_FILE" | |
| chmod 600 "$CONFIG_FILE" | |
| log_success "Config written to $CONFIG_FILE (mode 600)" | |
| # Source it | |
| # shellcheck disable=SC1090 | |
| source "$CONFIG_FILE" | |
| if [[ -z "${DOTFILES_REPO:-}" ]]; then | |
| log_error "Config fetched but DOTFILES_REPO is not set" | |
| log_error "Check the content of your 1Password secure note" | |
| exit 1 | |
| fi | |
| log_success "Loaded DOTFILES_REPO from 1Password" | |
| } | |
| # Step 5: Verify SSH auth works | |
| verify_ssh_auth() { | |
| # Derive SSH host from the repo URL | |
| local ssh_host="${DOTFILES_REPO%%:*}" | |
| log_info "Verifying SSH authentication to $ssh_host..." | |
| # Check if ssh-add can list keys from the 1Password agent | |
| if ssh-add -l >/dev/null 2>&1; then | |
| log_success "SSH agent is running with keys loaded" | |
| else | |
| log_warn "No SSH keys found via ssh-add -l, but will try $ssh_host anyway" | |
| fi | |
| # Try to connect to the git server (git servers typically reject shell sessions) | |
| # We expect either a success or a "shell access denied" message, but not "permission denied" | |
| if ssh -T -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "$ssh_host" 2>&1 | grep -qE "denied \(publickey|permission denied"; then | |
| log_error "SSH authentication to $ssh_host failed" | |
| log_error "Please verify:" | |
| log_error " - 1Password SSH agent is enabled" | |
| log_error " - The correct SSH key is unlocked in 1Password" | |
| log_error " - The key is authorized for $ssh_host" | |
| exit 1 | |
| fi | |
| log_success "SSH authentication verified" | |
| } | |
| # Step 6: Clone dotfiles over SSH | |
| clone_dotfiles() { | |
| log_info "Checking for dotfiles at $DOTFILES_DIR..." | |
| if [[ -d "$DOTFILES_DIR" ]]; then | |
| if [[ -d "$DOTFILES_DIR/.git" ]]; then | |
| log_success "Dotfiles already cloned at $DOTFILES_DIR" | |
| # Verify it's the right remote | |
| pushd "$DOTFILES_DIR" >/dev/null | |
| local current_remote | |
| current_remote=$(git remote get-url origin 2>/dev/null || echo "") | |
| if [[ "$current_remote" == "$DOTFILES_REPO" ]]; then | |
| log_success "Remote is correct: $current_remote" | |
| else | |
| log_warn "Remote mismatch. Expected: $DOTFILES_REPO" | |
| log_warn " Got: $current_remote" | |
| log_warn "Run 'git remote set-url origin $DOTFILES_REPO' if needed" | |
| fi | |
| popd >/dev/null | |
| return 0 | |
| else | |
| log_error "$DOTFILES_DIR exists but is not a git repository" | |
| log_error "Please remove or rename it, then re-run this script" | |
| exit 1 | |
| fi | |
| fi | |
| log_info "Cloning dotfiles from $DOTFILES_REPO..." | |
| mkdir -p "$(dirname "$DOTFILES_DIR")" | |
| if git clone "$DOTFILES_REPO" "$DOTFILES_DIR"; then | |
| log_success "Dotfiles cloned successfully" | |
| else | |
| log_error "Failed to clone dotfiles" | |
| exit 1 | |
| fi | |
| } | |
| # Step 7: Print handoff instructions | |
| print_handoff() { | |
| echo "" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| log_success "Stage 0 bootstrap complete!" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "" | |
| echo "Next steps:" | |
| echo "" | |
| echo " 1. cd $DOTFILES_DIR" | |
| echo " 2. Run ONE of:" | |
| echo "" | |
| echo " ${GREEN}make personal-install${NC} # For personal/home use" | |
| echo " ${GREEN}make work-install${NC} # For work machines" | |
| echo "" | |
| echo "After 'make install', your shell configs will be active." | |
| echo "You can then clone other private repos using the 1Password SSH agent." | |
| echo "" | |
| } | |
| # Main execution | |
| main() { | |
| echo "" | |
| log_info "Starting stage-0 bootstrap for fresh Mac" | |
| echo "" | |
| install_homebrew | |
| install_1password | |
| wait_for_1password_setup | |
| load_config | |
| verify_ssh_auth | |
| clone_dotfiles | |
| print_handoff | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment