Created
March 16, 2026 19:02
-
-
Save timstew/f5e49d8133f45e6eae950b4fcb2d93d8 to your computer and use it in GitHub Desktop.
OpenClaw Mac Mini — Enable Remote Access setup script
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
| #!/bin/bash | |
| # ============================================================================ | |
| # OpenClaw Mac Mini — Enable Remote Access | |
| # ============================================================================ | |
| # This script configures a Mac Mini for headless remote access. | |
| # A non-technical person can run this by double-clicking the file or via curl. | |
| # | |
| # What it does: | |
| # 1. Enables SSH (Remote Login) | |
| # 2. Enables Screen Sharing (VNC/Remote Desktop) | |
| # 3. Configures the firewall to allow remote connections | |
| # 4. Enables automatic restart after power loss | |
| # 5. Installs and launches Tailscale for secure remote access | |
| # 6. Writes a verification report to ~/Desktop | |
| # | |
| # Designed for: Field technicians following GUIDE-1-field-tech-setup.md | |
| # ============================================================================ | |
| # --- Error handling ----------------------------------------------------------- | |
| # We intentionally do NOT use set -euo pipefail. This script is run by | |
| # non-technical users — one failure should not kill the entire process. | |
| # Each step handles its own errors and reports them in the final summary. | |
| ERRORS=() | |
| WARNINGS=() | |
| PASSES=() | |
| CURRENT_USER="" | |
| TAILSCALE_IP="" | |
| REPORT_FILE="" # Set after we determine CURRENT_USER | |
| # --- Helpers ---------------------------------------------------------------- | |
| log() { | |
| echo "$(date '+%H:%M:%S') [OK] $1" | |
| PASSES+=("$1") | |
| } | |
| warn() { | |
| echo "$(date '+%H:%M:%S') [!!] $1" | |
| WARNINGS+=("$1") | |
| } | |
| fail() { | |
| echo "$(date '+%H:%M:%S') [ERR] $1" | |
| ERRORS+=("$1") | |
| } | |
| dialog() { | |
| osascript -e "display dialog \"$1\" with title \"OpenClaw Setup\" buttons {\"OK\"} default button \"OK\"" 2>/dev/null || true | |
| } | |
| dialog_info() { | |
| osascript -e "display notification \"$1\" with title \"OpenClaw Setup\"" 2>/dev/null || true | |
| } | |
| ask_yes_no() { | |
| local result | |
| result=$(osascript -e "display dialog \"$1\" with title \"OpenClaw Setup\" buttons {\"No\", \"Yes\"} default button \"Yes\"" 2>/dev/null) || true | |
| [[ "$result" == *"Yes"* ]] | |
| } | |
| # --- Welcome ---------------------------------------------------------------- | |
| dialog "Welcome to OpenClaw Mac Mini Setup! | |
| This script will configure this Mac for secure remote access. It takes about 2 minutes. | |
| What will happen: | |
| 1. Enable Remote Login (SSH) | |
| 2. Enable Screen Sharing | |
| 3. Configure the firewall | |
| 4. Set power options (auto-restart, no sleep) | |
| 5. Set up Tailscale | |
| Click OK to begin." | |
| echo "" | |
| echo "==============================================" | |
| echo " OpenClaw Mac Mini — Remote Access Setup" | |
| echo "==============================================" | |
| echo "" | |
| # --- Pre-flight checks ------------------------------------------------------ | |
| # Check we're on macOS | |
| if [[ "$(uname)" != "Darwin" ]]; then | |
| dialog "This script only runs on macOS. Exiting." | |
| exit 1 | |
| fi | |
| # Check for admin privileges; request them if needed | |
| if [[ $EUID -ne 0 ]]; then | |
| echo "This script requires administrator privileges." | |
| echo "You will be prompted for your password." | |
| echo "" | |
| exec sudo "$0" "$@" | |
| fi | |
| CURRENT_USER="${SUDO_USER:-$(logname 2>/dev/null || echo $USER)}" | |
| REPORT_FILE="/Users/$CURRENT_USER/Desktop/remote-access-setup-report.txt" | |
| log "Running as administrator (configuring for user: $CURRENT_USER)" | |
| echo "" | |
| # --- 1. Enable Remote Login (SSH) ------------------------------------------ | |
| echo "--- Step 1/5: Enabling Remote Login (SSH) ---" | |
| dialog_info "Step 1/5: Enabling Remote Login (SSH)..." | |
| if systemsetup -getremotelogin 2>/dev/null | grep -q "On"; then | |
| log "Remote Login is already enabled" | |
| else | |
| if systemsetup -setremotelogin on 2>/dev/null; then | |
| log "Remote Login enabled" | |
| elif launchctl load -w /System/Library/LaunchDaemons/ssh.plist 2>/dev/null; then | |
| log "Remote Login enabled via launchctl" | |
| else | |
| fail "Could not enable Remote Login — enable manually: System Settings > General > Sharing > Remote Login" | |
| fi | |
| fi | |
| echo "" | |
| # --- 2. Enable Screen Sharing (VNC / Remote Desktop) ----------------------- | |
| echo "--- Step 2/5: Enabling Screen Sharing ---" | |
| dialog_info "Step 2/5: Enabling Screen Sharing..." | |
| if launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist 2>/dev/null; then | |
| log "Screen Sharing enabled" | |
| else | |
| if defaults write /var/db/launchd.db/com.apple.launchd/overrides.plist com.apple.screensharing -dict Disabled -bool false 2>/dev/null && \ | |
| launchctl load /System/Library/LaunchDaemons/com.apple.screensharing.plist 2>/dev/null; then | |
| log "Screen Sharing enabled" | |
| else | |
| fail "Could not enable Screen Sharing — enable manually: System Settings > General > Sharing > Screen Sharing" | |
| fi | |
| fi | |
| echo "" | |
| # --- 3. Configure Firewall ------------------------------------------------- | |
| echo "--- Step 3/5: Configuring Firewall ---" | |
| dialog_info "Step 3/5: Configuring Firewall..." | |
| # Enable the firewall | |
| FIREWALL_STATE=$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null || echo "unknown") | |
| if echo "$FIREWALL_STATE" | grep -q "enabled"; then | |
| log "Firewall is already enabled" | |
| else | |
| if /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on 2>/dev/null; then | |
| log "Firewall enabled" | |
| else | |
| warn "Could not enable firewall — check System Settings > Network > Firewall" | |
| fi | |
| fi | |
| # CRITICAL: Ensure "Block all incoming connections" is OFF | |
| if /usr/libexec/ApplicationFirewall/socketfilterfw --setblockall off 2>/dev/null; then | |
| log "\"Block all incoming connections\" is disabled (required for SSH)" | |
| else | |
| warn "Could not disable \"Block all incoming\" — check System Settings > Network > Firewall > Options" | |
| fi | |
| # Allow sshd through the firewall | |
| SSHD_PATH="/usr/sbin/sshd" | |
| if [[ -f "$SSHD_PATH" ]]; then | |
| /usr/libexec/ApplicationFirewall/socketfilterfw --add "$SSHD_PATH" 2>/dev/null || true | |
| if /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp "$SSHD_PATH" 2>/dev/null; then | |
| log "Firewall: sshd allowed" | |
| else | |
| warn "Could not configure sshd firewall rule" | |
| fi | |
| fi | |
| # Allow sshd-session (handles post-authentication; blocking it drops SSH mid-handshake) | |
| for SSHD_SESSION_PATH in /usr/libexec/sshd-session /usr/sbin/sshd-session; do | |
| if [[ -f "$SSHD_SESSION_PATH" ]]; then | |
| /usr/libexec/ApplicationFirewall/socketfilterfw --add "$SSHD_SESSION_PATH" 2>/dev/null || true | |
| if /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp "$SSHD_SESSION_PATH" 2>/dev/null; then | |
| log "Firewall: sshd-session allowed ($SSHD_SESSION_PATH)" | |
| else | |
| warn "Could not configure sshd-session firewall rule" | |
| fi | |
| break | |
| fi | |
| done | |
| # Enable stealth mode | |
| if /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on 2>/dev/null; then | |
| log "Stealth mode enabled" | |
| else | |
| warn "Could not enable stealth mode" | |
| fi | |
| echo "" | |
| # --- 4. Enable Auto-Restart After Power Loss ------------------------------- | |
| echo "--- Step 4/5: Configuring Power Settings ---" | |
| dialog_info "Step 4/5: Configuring Power Settings..." | |
| if systemsetup -setrestartfreeze on 2>/dev/null; then | |
| log "Auto-restart after freeze enabled" | |
| else | |
| warn "Could not enable auto-restart after freeze" | |
| fi | |
| if systemsetup -setrestartpowerfailure on 2>/dev/null; then | |
| log "Auto-restart after power failure enabled" | |
| else | |
| warn "Could not enable auto-restart after power failure" | |
| fi | |
| # Prevent sleep (headless server should never sleep) | |
| pmset -a displaysleep 0 2>/dev/null && log "Display sleep disabled" || warn "Could not disable display sleep" | |
| pmset -a sleep 0 2>/dev/null && log "System sleep disabled" || warn "Could not disable system sleep" | |
| pmset -a disksleep 0 2>/dev/null && log "Disk sleep disabled" || warn "Could not disable disk sleep" | |
| # Wake on network access (Wake-on-LAN) | |
| pmset -a womp 1 2>/dev/null && log "Wake on network access enabled" || warn "Could not enable Wake on LAN" | |
| echo "" | |
| # --- 5. Tailscale ----------------------------------------------------------- | |
| echo "--- Step 5/5: Tailscale (Secure Remote Access) ---" | |
| dialog_info "Step 5/5: Setting up Tailscale..." | |
| TAILSCALE_INSTALLED=false | |
| if [[ -d "/Applications/Tailscale.app" ]] || command -v tailscale &>/dev/null; then | |
| log "Tailscale is already installed" | |
| TAILSCALE_INSTALLED=true | |
| else | |
| echo "" | |
| echo "Tailscale provides the secure network tunnel to this Mac." | |
| echo "Attempting to install from the Mac App Store..." | |
| echo "" | |
| # Try automated install via mas (Mac App Store CLI) | |
| if command -v mas &>/dev/null; then | |
| if mas install 1475387142 2>/dev/null; then | |
| log "Tailscale installed from the Mac App Store" | |
| TAILSCALE_INSTALLED=true | |
| fi | |
| fi | |
| # If automated install didn't work, open the App Store page and guide the user | |
| if [[ "$TAILSCALE_INSTALLED" == "false" ]]; then | |
| echo "Automatic install not available. Opening the App Store..." | |
| open "macappstore://apps.apple.com/app/tailscale/id1475387142" 2>/dev/null || \ | |
| open "https://apps.apple.com/app/tailscale/id1475387142" 2>/dev/null || true | |
| dialog "Tailscale needs to be installed manually. | |
| The App Store should have opened. Please: | |
| 1. Click \"Get\" then \"Install\" | |
| 2. Wait for it to finish | |
| 3. Click OK here when done" | |
| # Check again | |
| if [[ -d "/Applications/Tailscale.app" ]] || command -v tailscale &>/dev/null; then | |
| log "Tailscale installed manually" | |
| TAILSCALE_INSTALLED=true | |
| else | |
| fail "Tailscale not found after manual install attempt — install it from the App Store before continuing" | |
| fi | |
| fi | |
| fi | |
| # Launch Tailscale and prompt sign-in | |
| if [[ "$TAILSCALE_INSTALLED" == "true" ]]; then | |
| # Launch the app if not running | |
| if ! pgrep -x "Tailscale" > /dev/null 2>&1; then | |
| sudo -u "$CURRENT_USER" open -a Tailscale 2>/dev/null || true | |
| sleep 3 | |
| fi | |
| dialog "Tailscale is installed. Now you need to sign in. | |
| 1. Look for the Tailscale icon in the menu bar (top-right of screen) | |
| 2. Click it and choose \"Sign in...\" or \"Log in...\" | |
| 3. Sign in with the Tailscale credentials from your setup packet | |
| 4. When the sign-in page says \"Success\" or you see a connected status, click OK here" | |
| # Try to get the Tailscale IP | |
| sleep 2 | |
| if command -v tailscale &>/dev/null; then | |
| TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "") | |
| fi | |
| # If CLI not available, try reading from the app's status | |
| if [[ -z "$TAILSCALE_IP" ]]; then | |
| # The App Store version may not have the CLI in PATH; try the bundled binary | |
| TAILSCALE_CLI="/Applications/Tailscale.app/Contents/MacOS/Tailscale" | |
| if [[ -x "$TAILSCALE_CLI" ]]; then | |
| TAILSCALE_IP=$("$TAILSCALE_CLI" ip -4 2>/dev/null || echo "") | |
| fi | |
| fi | |
| if [[ -n "$TAILSCALE_IP" ]]; then | |
| log "Tailscale connected — IP: $TAILSCALE_IP" | |
| else | |
| warn "Could not detect Tailscale IP — make sure you signed in. Check the Tailscale icon in the menu bar for the IP (looks like 100.x.x.x)" | |
| fi | |
| fi | |
| echo "" | |
| # --- Verification ----------------------------------------------------------- | |
| echo "==============================================" | |
| echo " Verifying Configuration..." | |
| echo "==============================================" | |
| echo "" | |
| VERIFY_ERRORS=() | |
| # Check SSH | |
| if systemsetup -getremotelogin 2>/dev/null | grep -q "On"; then | |
| echo " [PASS] Remote Login (SSH) is ON" | |
| else | |
| echo " [FAIL] Remote Login (SSH) is OFF" | |
| VERIFY_ERRORS+=("Remote Login is not enabled") | |
| fi | |
| # Check Screen Sharing | |
| if launchctl list 2>/dev/null | grep -q "com.apple.screensharing"; then | |
| echo " [PASS] Screen Sharing is running" | |
| else | |
| echo " [FAIL] Screen Sharing is not running" | |
| VERIFY_ERRORS+=("Screen Sharing is not running") | |
| fi | |
| # Check firewall | |
| if /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null | grep -q "enabled"; then | |
| echo " [PASS] Firewall is enabled" | |
| else | |
| echo " [FAIL] Firewall is not enabled" | |
| VERIFY_ERRORS+=("Firewall is not enabled") | |
| fi | |
| # Check block-all is off | |
| if /usr/libexec/ApplicationFirewall/socketfilterfw --getblockall 2>/dev/null | grep -q "DISABLED"; then | |
| echo " [PASS] Block-all is disabled" | |
| else | |
| echo " [FAIL] Block-all is still enabled (SSH will be blocked!)" | |
| VERIFY_ERRORS+=("Block-all is enabled — SSH will not work") | |
| fi | |
| # Check sleep | |
| SLEEP_VAL=$(pmset -g 2>/dev/null | grep "^ sleep" | awk '{print $2}' || echo "unknown") | |
| if [[ "$SLEEP_VAL" == "0" ]]; then | |
| echo " [PASS] System sleep is disabled" | |
| else | |
| echo " [FAIL] System sleep is set to $SLEEP_VAL (should be 0)" | |
| VERIFY_ERRORS+=("System sleep is not disabled (value: $SLEEP_VAL)") | |
| fi | |
| # Check auto-restart | |
| if systemsetup -getrestartpowerfailure 2>/dev/null | grep -qi "on"; then | |
| echo " [PASS] Auto-restart after power failure is ON" | |
| else | |
| echo " [FAIL] Auto-restart after power failure is OFF" | |
| VERIFY_ERRORS+=("Auto-restart after power failure is not enabled") | |
| fi | |
| # Check Tailscale | |
| if [[ -d "/Applications/Tailscale.app" ]] || command -v tailscale &>/dev/null; then | |
| echo " [PASS] Tailscale is installed" | |
| else | |
| echo " [FAIL] Tailscale is not installed" | |
| VERIFY_ERRORS+=("Tailscale is not installed") | |
| fi | |
| if [[ -n "$TAILSCALE_IP" ]]; then | |
| echo " [PASS] Tailscale IP: $TAILSCALE_IP" | |
| else | |
| echo " [WARN] Tailscale IP not detected — check menu bar icon" | |
| fi | |
| echo "" | |
| # --- Write Report to Desktop ------------------------------------------------ | |
| { | |
| echo "==============================================" | |
| echo " OpenClaw Remote Access Setup Report" | |
| echo " $(date)" | |
| echo " Mac User: $CURRENT_USER" | |
| echo "==============================================" | |
| echo "" | |
| echo "PASSED:" | |
| for item in "${PASSES[@]}"; do | |
| echo " [OK] $item" | |
| done | |
| echo "" | |
| if [[ ${#WARNINGS[@]} -gt 0 ]]; then | |
| echo "WARNINGS:" | |
| for item in "${WARNINGS[@]}"; do | |
| echo " [!!] $item" | |
| done | |
| echo "" | |
| fi | |
| if [[ ${#ERRORS[@]} -gt 0 ]]; then | |
| echo "ERRORS:" | |
| for item in "${ERRORS[@]}"; do | |
| echo " [ERR] $item" | |
| done | |
| echo "" | |
| fi | |
| echo "VERIFICATION:" | |
| if [[ ${#VERIFY_ERRORS[@]} -eq 0 ]]; then | |
| echo " All checks passed." | |
| else | |
| for item in "${VERIFY_ERRORS[@]}"; do | |
| echo " [FAIL] $item" | |
| done | |
| fi | |
| echo "" | |
| if [[ -n "$TAILSCALE_IP" ]]; then | |
| echo "TAILSCALE IP: $TAILSCALE_IP" | |
| else | |
| echo "TAILSCALE IP: (not detected — check Tailscale menu bar icon)" | |
| fi | |
| echo "" | |
| echo "NEXT STEP: Send the Tailscale IP to your remote administrator." | |
| echo "==============================================" | |
| } > "$REPORT_FILE" 2>/dev/null | |
| if [[ -f "$REPORT_FILE" ]]; then | |
| log "Report saved to Desktop: remote-access-setup-report.txt" | |
| else | |
| warn "Could not write report to Desktop" | |
| fi | |
| echo "" | |
| # --- Summary Dialog --------------------------------------------------------- | |
| SUMMARY_STATUS="Setup complete!" | |
| if [[ ${#ERRORS[@]} -gt 0 ]]; then | |
| SUMMARY_STATUS="Setup finished with ${#ERRORS[@]} error(s). Check the report on your Desktop." | |
| elif [[ ${#WARNINGS[@]} -gt 0 ]]; then | |
| SUMMARY_STATUS="Setup finished with ${#WARNINGS[@]} warning(s). Check the report on your Desktop." | |
| fi | |
| TAILSCALE_MSG="" | |
| if [[ -n "$TAILSCALE_IP" ]]; then | |
| TAILSCALE_MSG=" | |
| Tailscale IP: $TAILSCALE_IP | |
| Send this IP to your remote administrator now." | |
| else | |
| TAILSCALE_MSG=" | |
| Tailscale IP: Check the Tailscale icon in the menu bar (top-right). | |
| The IP looks like 100.x.x.x | |
| Send that IP to your remote administrator." | |
| fi | |
| dialog "$SUMMARY_STATUS | |
| $TAILSCALE_MSG | |
| A report has been saved to your Desktop: | |
| remote-access-setup-report.txt | |
| DO NOT disconnect the monitor/keyboard until the admin confirms they can connect." | |
| echo "==============================================" | |
| echo " $SUMMARY_STATUS" | |
| echo "==============================================" | |
| echo "" | |
| if [[ -n "$TAILSCALE_IP" ]]; then | |
| echo " Tailscale IP: $TAILSCALE_IP" | |
| echo "" | |
| echo " >>> Send this IP to your remote administrator <<<" | |
| else | |
| echo " Tailscale IP: Check the Tailscale menu bar icon" | |
| echo "" | |
| echo " >>> Send the IP to your remote administrator <<<" | |
| fi | |
| echo "" | |
| echo " Report saved to: $REPORT_FILE" | |
| echo "" | |
| echo " DO NOT disconnect monitor/keyboard until the" | |
| echo " admin confirms they can connect remotely." | |
| echo "" | |
| echo "==============================================" | |
| echo "" | |
| echo "You can close this window now." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment