<# .SYNOPSIS OrbStack-equivalent Development Sandbox Setup for Windows using Hyper-V .DESCRIPTION Creates fully isolated Hyper-V VMs for running Claude Code with maximum security. Uses Ubuntu Cloud Images with cloud-init for automated provisioning. Unlike WSL2, Hyper-V provides: - Complete kernel isolation (separate kernel per VM) - Full network isolation (own network stack) - No shared filesystem by default - Process isolation from Windows host .PARAMETER VMName Name for the virtual machine (default: dev-sandbox) .PARAMETER MemoryGB Memory allocation in GB (default: 8) .PARAMETER DiskGB Virtual disk size in GB (default: 60) .PARAMETER CPUs Number of virtual CPUs (default: 4) .EXAMPLE .\setup_env_windows.ps1 -VMName my-project .EXAMPLE .\setup_env_windows.ps1 -VMName elixir-dev -MemoryGB 16 -DiskGB 100 .NOTES Requirements: - Windows 10/11 Pro, Enterprise, or Education (Hyper-V not available on Home) - Hyper-V enabled - Administrator privileges - Internet connection for downloading Ubuntu image #> [CmdletBinding()] param( [Parameter(Position=0)] [ValidatePattern('^[a-zA-Z][a-zA-Z0-9_-]*$')] [ValidateLength(1, 64)] [string]$VMName = "dev-sandbox", [ValidateRange(2, 64)] [int]$MemoryGB = 8, [ValidateRange(40, 500)] [int]$DiskGB = 60, [ValidateRange(1, 16)] [int]$CPUs = 4, [switch]$Force ) # ============================================================================ # Configuration # ============================================================================ $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # Speed up downloads $UBUNTU_VERSION = "24.04" $UBUNTU_IMAGE_URL = "https://cloud-images.ubuntu.com/releases/$UBUNTU_VERSION/release/ubuntu-$UBUNTU_VERSION-server-cloudimg-amd64.vhdx" $VM_STORAGE_PATH = "$env:ProgramData\HyperV-DevSandbox" $IMAGES_PATH = "$VM_STORAGE_PATH\Images" $VMS_PATH = "$VM_STORAGE_PATH\VMs" $CONFIG_FILE = "$PSScriptRoot\config.env" # ============================================================================ # Helper Functions # ============================================================================ function Write-Status { param([string]$Message, [string]$Type = "Info") $colors = @{ "Info" = "Cyan" "Success" = "Green" "Warning" = "Yellow" "Error" = "Red" } $prefix = @{ "Info" = "==>" "Success" = "[OK]" "Warning" = "[!]" "Error" = "[X]" } Write-Host "$($prefix[$Type]) $Message" -ForegroundColor $colors[$Type] } function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Test-HyperVEnabled { $hyperv = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -ErrorAction SilentlyContinue return $hyperv -and $hyperv.State -eq "Enabled" } function Get-VMSwitch-Default { # Try to find an external switch first, then default switch $switch = Get-VMSwitch -SwitchType External -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $switch) { $switch = Get-VMSwitch -Name "Default Switch" -ErrorAction SilentlyContinue } return $switch } function New-CloudInitISO { param( [string]$OutputPath, [string]$UserData, [string]$MetaData ) # Create temp directory for cloud-init files $tempDir = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } try { # Write cloud-init files $UserData | Out-File -FilePath "$tempDir\user-data" -Encoding UTF8 -NoNewline $MetaData | Out-File -FilePath "$tempDir\meta-data" -Encoding UTF8 -NoNewline # Use oscdimg to create ISO (included in Windows ADK, or use alternative) # For simplicity, we'll use a PowerShell-based approach with .NET # Check for oscdimg $oscdimg = Get-Command oscdimg -ErrorAction SilentlyContinue if ($oscdimg) { & oscdimg -j2 -lcidata "$tempDir" "$OutputPath" 2>&1 | Out-Null } else { # Fallback: Create a simple ISO using PowerShell # This requires the CDROM file system support Write-Warning "oscdimg not found. Installing Windows ADK tools is recommended." Write-Warning "Attempting alternative ISO creation..." # Use a .NET approach or download a tool # For now, we'll use a simple approach with New-IsoFile if available throw "ISO creation requires Windows ADK. Install it from: https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install" } } finally { Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } function Get-SecureCredential { param([string]$Prompt) $secure = Read-Host -Prompt $Prompt -AsSecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } function ConvertTo-Base64 { param([string]$Text) return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Text)) } # ============================================================================ # Pre-flight Checks # ============================================================================ Write-Host "" Write-Host "============================================================================" -ForegroundColor Green Write-Host " Hyper-V Development Sandbox - Maximum Security Mode" -ForegroundColor Green Write-Host "============================================================================" -ForegroundColor Green Write-Host "" # Check admin privileges if (-not (Test-Administrator)) { Write-Status "This script requires Administrator privileges." "Error" Write-Host "" Write-Host "Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow exit 1 } # Check Hyper-V if (-not (Test-HyperVEnabled)) { Write-Status "Hyper-V is not enabled." "Error" Write-Host "" Write-Host "To enable Hyper-V, run:" -ForegroundColor Yellow Write-Host " Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All" -ForegroundColor Cyan Write-Host "" Write-Host "Then restart your computer and run this script again." -ForegroundColor Yellow exit 1 } # Check for existing VM $existingVM = Get-VM -Name $VMName -ErrorAction SilentlyContinue if ($existingVM) { if (-not $Force) { Write-Status "VM '$VMName' already exists." "Error" Write-Host "" Write-Host "To delete it: Remove-VM -Name $VMName -Force" -ForegroundColor Yellow Write-Host "To connect: vmconnect localhost $VMName" -ForegroundColor Yellow Write-Host "Or use -Force to replace it" -ForegroundColor Yellow exit 1 } else { Write-Status "Removing existing VM '$VMName'..." "Warning" Stop-VM -Name $VMName -Force -ErrorAction SilentlyContinue Remove-VM -Name $VMName -Force Remove-Item -Path "$VMS_PATH\$VMName" -Recurse -Force -ErrorAction SilentlyContinue } } # Get VM switch $vmSwitch = Get-VMSwitch-Default if (-not $vmSwitch) { Write-Status "No suitable network switch found." "Error" Write-Host "" Write-Host "Create a virtual switch in Hyper-V Manager or run:" -ForegroundColor Yellow Write-Host ' New-VMSwitch -Name "External Switch" -NetAdapterName (Get-NetAdapter | Where-Object Status -eq Up | Select-Object -First 1).Name' -ForegroundColor Cyan exit 1 } Write-Status "Using network switch: $($vmSwitch.Name)" "Info" # ============================================================================ # Load or Create Config # ============================================================================ $GitName = "" $GitEmail = "" if (Test-Path $CONFIG_FILE) { Write-Status "Loading config from: $CONFIG_FILE" "Info" Get-Content $CONFIG_FILE | ForEach-Object { if ($_ -match '^GIT_NAME="(.+)"$') { $GitName = $matches[1] } if ($_ -match '^GIT_EMAIL="(.+)"$') { $GitEmail = $matches[1] } } Write-Host " Name: $GitName" -ForegroundColor Gray Write-Host " Email: $GitEmail" -ForegroundColor Gray } else { Write-Status "First run - creating config file" "Info" Write-Host "" do { $GitName = Read-Host "Git commit author name" } while ([string]::IsNullOrWhiteSpace($GitName)) do { $GitEmail = Read-Host "Git commit author email" } while ($GitEmail -notmatch '^[^@]+@[^@]+\.[^@]+$') # Save config @" GIT_NAME="$GitName" GIT_EMAIL="$GitEmail" "@ | Out-File -FilePath $CONFIG_FILE -Encoding UTF8 # Restrict permissions (Windows equivalent of chmod 600) $acl = Get-Acl $CONFIG_FILE $acl.SetAccessRuleProtection($true, $false) $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, "FullControl", "Allow" ) $acl.SetAccessRule($rule) Set-Acl -Path $CONFIG_FILE -AclObject $acl Write-Host "" Write-Status "Config saved to: $CONFIG_FILE" "Success" } # Prompt for VM password (not stored) Write-Host "" do { $VMPassword = Get-SecureCredential "VM user password (min 8 chars)" } while ($VMPassword.Length -lt 8) # ============================================================================ # Download Ubuntu Cloud Image # ============================================================================ New-Item -ItemType Directory -Path $IMAGES_PATH -Force | Out-Null New-Item -ItemType Directory -Path "$VMS_PATH\$VMName" -Force | Out-Null $imagePath = "$IMAGES_PATH\ubuntu-$UBUNTU_VERSION-cloudimg-amd64.vhdx" if (-not (Test-Path $imagePath)) { Write-Status "Downloading Ubuntu $UBUNTU_VERSION cloud image..." "Info" Write-Host " This may take a few minutes..." -ForegroundColor Gray try { Invoke-WebRequest -Uri $UBUNTU_IMAGE_URL -OutFile $imagePath -UseBasicParsing Write-Status "Download complete" "Success" } catch { Write-Status "Failed to download Ubuntu image: $_" "Error" exit 1 } } else { Write-Status "Using cached Ubuntu image: $imagePath" "Info" } # ============================================================================ # Create VM Disk (copy and resize) # ============================================================================ $vmDiskPath = "$VMS_PATH\$VMName\disk.vhdx" Write-Status "Creating VM disk..." "Info" Copy-Item -Path $imagePath -Destination $vmDiskPath -Force Resize-VHD -Path $vmDiskPath -SizeBytes ($DiskGB * 1GB) # ============================================================================ # Create Cloud-Init Configuration # ============================================================================ Write-Status "Generating cloud-init configuration..." "Info" # Generate SSH key for passwordless access from host $sshKeyPath = "$env:USERPROFILE\.ssh\id_ed25519_$VMName" if (-not (Test-Path $sshKeyPath)) { ssh-keygen -t ed25519 -f $sshKeyPath -N '""' -C "$VMName-sandbox" 2>&1 | Out-Null } $sshPublicKey = Get-Content "$sshKeyPath.pub" # Encode setup script for transfer $setupScriptPath = "$PSScriptRoot\setup_env.sh" if (-not (Test-Path $setupScriptPath)) { Write-Status "setup_env.sh not found in script directory" "Error" exit 1 } $setupScriptB64 = ConvertTo-Base64 (Get-Content $setupScriptPath -Raw) $cloudInitUserData = @" #cloud-config hostname: $VMName manage_etc_hosts: true users: - name: dev sudo: ALL=(ALL) NOPASSWD:ALL shell: /bin/bash lock_passwd: false passwd: $(echo "$VMPassword" | openssl passwd -6 -stdin 2>/dev/null || echo '$VMPassword') ssh_authorized_keys: - $sshPublicKey package_update: true package_upgrade: true packages: - openssh-server - curl - git write_files: - path: /tmp/setup_env.sh permissions: '0755' encoding: b64 content: $setupScriptB64 - path: /tmp/provision.sh permissions: '0755' content: | #!/bin/bash set -e export GIT_NAME="$GitName" export GIT_EMAIL="$GitEmail" cd /home/dev sudo -u dev bash /tmp/setup_env.sh --non-interactive --yes 2>&1 | tee /var/log/provision.log runcmd: - systemctl enable ssh - systemctl start ssh - bash /tmp/provision.sh final_message: "Cloud-init provisioning complete after \$UPTIME seconds" "@ $cloudInitMetaData = @" instance-id: $VMName-$(Get-Date -Format 'yyyyMMddHHmmss') local-hostname: $VMName "@ # Create cloud-init ISO $cloudInitISO = "$VMS_PATH\$VMName\cloud-init.iso" Write-Status "Creating cloud-init ISO..." "Info" try { New-CloudInitISO -OutputPath $cloudInitISO -UserData $cloudInitUserData -MetaData $cloudInitMetaData } catch { Write-Status "Failed to create cloud-init ISO: $_" "Error" Write-Host "" Write-Host "Alternative: Install Windows ADK from:" -ForegroundColor Yellow Write-Host " https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install" -ForegroundColor Cyan Write-Host "" Write-Host "Or use WSL to create the ISO:" -ForegroundColor Yellow Write-Host " wsl genisoimage -output cloud-init.iso -volid cidata -joliet -rock user-data meta-data" -ForegroundColor Cyan exit 1 } # ============================================================================ # Create Hyper-V VM # ============================================================================ Write-Status "Creating Hyper-V VM: $VMName" "Info" Write-Host " CPUs: $CPUs" -ForegroundColor Gray Write-Host " Memory: $MemoryGB GB" -ForegroundColor Gray Write-Host " Disk: $DiskGB GB" -ForegroundColor Gray # Create VM (Generation 2 for UEFI) $vm = New-VM -Name $VMName ` -Generation 2 ` -MemoryStartupBytes ($MemoryGB * 1GB) ` -VHDPath $vmDiskPath ` -SwitchName $vmSwitch.Name ` -Path "$VMS_PATH\$VMName" # Configure VM Set-VMProcessor -VM $vm -Count $CPUs Set-VMMemory -VM $vm -DynamicMemoryEnabled $true -MinimumBytes 2GB -MaximumBytes ($MemoryGB * 1GB) # Disable Secure Boot for Ubuntu Set-VMFirmware -VM $vm -EnableSecureBoot Off # Add cloud-init ISO as DVD drive Add-VMDvdDrive -VM $vm -Path $cloudInitISO # Configure boot order (disk first, then DVD) $bootOrder = @( (Get-VMHardDiskDrive -VM $vm), (Get-VMDvdDrive -VM $vm) ) Set-VMFirmware -VM $vm -BootOrder $bootOrder # Disable integration services for maximum isolation # (Comment these out if you want clipboard/file sharing) Disable-VMIntegrationService -VM $vm -Name "Guest Service Interface" -ErrorAction SilentlyContinue Disable-VMIntegrationService -VM $vm -Name "Heartbeat" -ErrorAction SilentlyContinue # Enable nested virtualization (useful for Docker in VM) Set-VMProcessor -VM $vm -ExposeVirtualizationExtensions $true -ErrorAction SilentlyContinue # ============================================================================ # Start VM and Wait for Provisioning # ============================================================================ Write-Status "Starting VM..." "Info" Start-VM -Name $VMName Write-Host "" Write-Host "VM is starting. Cloud-init provisioning will take several minutes." -ForegroundColor Yellow Write-Host "" Write-Host "To monitor progress:" -ForegroundColor Cyan Write-Host " vmconnect localhost $VMName" -ForegroundColor White Write-Host "" Write-Host "To connect via SSH (after provisioning):" -ForegroundColor Cyan Write-Host " ssh -i $sshKeyPath dev@" -ForegroundColor White Write-Host "" Write-Host "To find the VM's IP address:" -ForegroundColor Cyan Write-Host " (Get-VM -Name $VMName | Get-VMNetworkAdapter).IPAddresses" -ForegroundColor White Write-Host "" # Wait for VM to get an IP Write-Status "Waiting for VM to obtain IP address..." "Info" $maxWait = 300 # 5 minutes $waited = 0 $vmIP = $null while ($waited -lt $maxWait) { $vmIP = (Get-VM -Name $VMName | Get-VMNetworkAdapter).IPAddresses | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } | Select-Object -First 1 if ($vmIP) { break } Start-Sleep -Seconds 5 $waited += 5 Write-Host "." -NoNewline } Write-Host "" if ($vmIP) { Write-Status "VM IP: $vmIP" "Success" # Add to hosts file for easy access $hostsEntry = "$vmIP`t$VMName.local" $hostsFile = "$env:SystemRoot\System32\drivers\etc\hosts" $existingEntry = Select-String -Path $hostsFile -Pattern "$VMName\.local" -ErrorAction SilentlyContinue if (-not $existingEntry) { Add-Content -Path $hostsFile -Value $hostsEntry Write-Status "Added $VMName.local to hosts file" "Info" } Write-Host "" Write-Host "============================================================================" -ForegroundColor Green Write-Host " VM '$VMName' is starting!" -ForegroundColor Green Write-Host "============================================================================" -ForegroundColor Green Write-Host "" Write-Host "CONNECT:" -ForegroundColor Yellow Write-Host " Console: vmconnect localhost $VMName" -ForegroundColor White Write-Host " SSH: ssh -i $sshKeyPath dev@$vmIP" -ForegroundColor White Write-Host " SSH (alt): ssh -i $sshKeyPath dev@$VMName.local" -ForegroundColor White Write-Host "" Write-Host "SERVICES (after provisioning):" -ForegroundColor Yellow Write-Host " PostgreSQL: psql -h $vmIP -U dev -d dev" -ForegroundColor White Write-Host " VNC: Start in VM with 'vnc-start', then connect to $vmIP`:5901" -ForegroundColor White Write-Host "" Write-Host "CLAUDE CODE:" -ForegroundColor Yellow Write-Host " ssh -i $sshKeyPath dev@$vmIP -- claude" -ForegroundColor White Write-Host "" Write-Host "MANAGEMENT:" -ForegroundColor Yellow Write-Host " Stop VM: Stop-VM -Name $VMName" -ForegroundColor White Write-Host " Start VM: Start-VM -Name $VMName" -ForegroundColor White Write-Host " Delete VM: Remove-VM -Name $VMName -Force" -ForegroundColor White Write-Host "" } else { Write-Status "Could not obtain VM IP within timeout." "Warning" Write-Host "" Write-Host "The VM may still be starting. Check:" -ForegroundColor Yellow Write-Host " vmconnect localhost $VMName" -ForegroundColor White Write-Host "" } Write-Host "============================================================================" -ForegroundColor Green Write-Host " Security: Full Hyper-V isolation (separate kernel, network, filesystem)" -ForegroundColor Green Write-Host "============================================================================" -ForegroundColor Green Write-Host ""