<# .SYNOPSIS Installs Claude Code Remote Control as a persistent Windows service. .DESCRIPTION This script is fully self-contained — run it standalone or pipe via: irm https://raw.githubusercontent.com/stillhoriz3n/claude-rc-service/main/install.ps1 | iex It will: 1. Create an install directory at C:\claude-rc-service 2. Locate or install the native Claude Code binary (claude.exe) 3. Authenticate to Anthropic (opens browser if needed) 4. Generate a config file with machine name, transcript, and service options 5. Enable the tengu_ccr_bridge feature flag and configure workspace trust 6. Generate the service scripts (bat, zombie reaper, transcript cleanup) 7. Register a Windows Scheduled Task that auto-starts on boot .NOTES Requires: Administrator privileges #> $ErrorActionPreference = "Stop" $InstallDir = "C:\claude-rc-service" $TaskName = "ClaudeRemoteControl" # --- Check admin --- $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Host "ERROR: This script must be run as Administrator." -ForegroundColor Red Write-Host "Right-click PowerShell -> 'Run as Administrator', then try again." -ForegroundColor Yellow return } Write-Host "" Write-Host "=== Claude Code Remote Control - Installer ===" -ForegroundColor Cyan Write-Host "" # --- Step 1: Create install directory --- Write-Host "[1/7] Setting up install directory..." -ForegroundColor Yellow if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null } $logDir = Join-Path $InstallDir "logs" if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } Write-Host " Install dir: $InstallDir" -ForegroundColor Green Write-Host " Log dir: $logDir" -ForegroundColor Green # --- Step 2: Find or acquire the native binary --- Write-Host "" Write-Host "[2/7] Locating native Claude Code binary..." -ForegroundColor Yellow $claudeExe = Join-Path $InstallDir "claude.exe" if (Test-Path $claudeExe) { $version = & $claudeExe --version 2>&1 Write-Host " Found existing: $claudeExe ($version)" -ForegroundColor Green } else { $found = $false # Try VSCode extensions first $vscodeExts = Join-Path $env:USERPROFILE ".vscode\extensions" if (Test-Path $vscodeExts) { $nativeBinaries = Get-ChildItem -Path $vscodeExts -Filter "claude.exe" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.DirectoryName -like "*native-binary*" } | Sort-Object LastWriteTime -Descending if ($nativeBinaries.Count -gt 0) { $source = $nativeBinaries[0].FullName Write-Host " Found in VSCode extensions: $source" -ForegroundColor Green Copy-Item -Path $source -Destination $claudeExe -Force $found = $true } } # Try winget packages if (-not $found) { $wingetDir = "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" if (Test-Path $wingetDir) { $wingetBin = Get-ChildItem -Path $wingetDir -Filter "claude.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 if ($wingetBin) { Copy-Item -Path $wingetBin.FullName -Destination $claudeExe -Force Write-Host " Found in winget packages: $($wingetBin.FullName)" -ForegroundColor Green $found = $true } } } # Install via winget as last resort if (-not $found) { Write-Host " Not found locally. Installing via winget..." -ForegroundColor Yellow try { winget install --id Anthropic.ClaudeCode --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null $wingetBin = Get-ChildItem -Path "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Filter "claude.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 if ($wingetBin) { Copy-Item -Path $wingetBin.FullName -Destination $claudeExe -Force Write-Host " Installed via winget" -ForegroundColor Green $found = $true } } catch { # winget not available } } if (-not $found) { Write-Host " ERROR: Could not find native claude.exe binary." -ForegroundColor Red Write-Host " Install one of:" -ForegroundColor Red Write-Host " - Claude Code VSCode extension" -ForegroundColor Red Write-Host " - winget install Anthropic.ClaudeCode" -ForegroundColor Red Write-Host " Then re-run this script." -ForegroundColor Red return } } $testVersion = & $claudeExe --version 2>&1 Write-Host " Binary version: $testVersion" -ForegroundColor Green # Verify binary integrity $sig = Get-AuthenticodeSignature $claudeExe $hash = (Get-FileHash $claudeExe -Algorithm SHA256).Hash if ($sig.Status -eq 'Valid') { Write-Host " Signature: Valid ($($sig.SignerCertificate.Subject))" -ForegroundColor Green Write-Host " SHA256: $hash" -ForegroundColor Gray } else { Write-Host " WARNING: Binary signature could not be verified (Status: $($sig.Status))" -ForegroundColor Yellow Write-Host " SHA256: $hash" -ForegroundColor Yellow Write-Host " This may be normal for some builds, but verify the source if unsure." -ForegroundColor Yellow $proceed = Read-Host " Continue anyway? (y/N)" if ($proceed -notmatch '^[Yy]') { Write-Host " Aborted by user." -ForegroundColor Red return } } # --- Step 3: Check authentication --- Write-Host "" Write-Host "[3/7] Checking Anthropic authentication..." -ForegroundColor Yellow $credFile = Join-Path $env:USERPROFILE ".claude\.credentials.json" if (Test-Path $credFile) { $creds = Get-Content $credFile -Raw | ConvertFrom-Json if ($creds.claudeAiOauth.accessToken) { Write-Host " Already authenticated (OAuth token found)" -ForegroundColor Green } else { Write-Host " Credentials file exists but no OAuth token found." -ForegroundColor Yellow Write-Host " Launching Claude Code login..." -ForegroundColor Yellow & $claudeExe auth login 2>&1 } } else { Write-Host " Not authenticated. Launching Claude Code login..." -ForegroundColor Yellow Write-Host " A browser window will open — sign in with your Anthropic account." -ForegroundColor White Write-Host "" & $claudeExe auth login 2>&1 Write-Host "" if (Test-Path $credFile) { Write-Host " Authentication successful!" -ForegroundColor Green } else { Write-Host " ERROR: Authentication failed or was cancelled." -ForegroundColor Red Write-Host " Run 'claude auth login' manually, then re-run this installer." -ForegroundColor Yellow return } } # --- Step 4: Configuration --- Write-Host "" Write-Host "[4/7] Configuring service options..." -ForegroundColor Yellow $configPath = Join-Path $InstallDir "config.json" $hostname = [System.Net.Dns]::GetHostName() if (-not (Test-Path $configPath)) { Write-Host " Machine name in claude.ai: $hostname" -ForegroundColor Cyan Write-Host "" $transcriptInput = Read-Host " Enable session transcripts? (y/N)" $transcriptsEnabled = $transcriptInput -match '^[Yy]' $serviceConfig = [ordered]@{ transcripts = [ordered]@{ enabled = $transcriptsEnabled retentionDays = 7 } sessionNaming = [ordered]@{ enabled = $true format = "{timestamp}_{hostname}_{sessionId}" } restartDelaySecs = 10 verbose = $false } $serviceConfig | ConvertTo-Json -Depth 5 | Set-Content $configPath -Encoding UTF8 Write-Host " Config saved: $configPath" -ForegroundColor Green if ($transcriptsEnabled) { $transcriptDir = Join-Path $InstallDir "transcripts" if (-not (Test-Path $transcriptDir)) { New-Item -ItemType Directory -Path $transcriptDir -Force | Out-Null } Write-Host " Transcripts enabled -> $transcriptDir" -ForegroundColor Green } Write-Host " Session auto-naming: enabled" -ForegroundColor Green } else { Write-Host " Existing config found: $configPath" -ForegroundColor Green $existingConfig = Get-Content $configPath -Raw | ConvertFrom-Json Write-Host " Machine name: $hostname" -ForegroundColor Green if ($existingConfig.transcripts.enabled) { Write-Host " Transcripts: enabled (retention: $($existingConfig.transcripts.retentionDays) days)" -ForegroundColor Green } else { Write-Host " Transcripts: disabled" -ForegroundColor Gray } } # --- Step 5: Configure feature flags and trust --- Write-Host "" Write-Host "[5/7] Configuring Remote Control feature flags..." -ForegroundColor Yellow $claudeConfigDir = Join-Path $env:USERPROFILE ".claude" $configFile = Join-Path $claudeConfigDir ".config.json" # Show the user exactly what will be changed Write-Host "" Write-Host " The following changes will be made to ~\.claude\.config.json:" -ForegroundColor White Write-Host " 1. Set feature flag 'tengu_ccr_bridge' = true (enables remote control)" -ForegroundColor White Write-Host " 2. Set 'remoteDialogSeen' = true (skips info dialog on next CLI launch)" -ForegroundColor White Write-Host " 3. Trust workspace 'C:/claude-rc-service' (allows CLI to operate in install dir)" -ForegroundColor White Write-Host "" $consent = Read-Host " Apply these changes? (Y/n)" if ($consent -match '^[Nn]') { Write-Host " Skipped config changes. Remote control may not work until you configure these manually." -ForegroundColor Yellow Write-Host " See README -> 'Feature Flag Bypass' and 'Workspace Trust' for manual steps." -ForegroundColor Yellow } else { if (-not (Test-Path $claudeConfigDir)) { New-Item -ItemType Directory -Path $claudeConfigDir -Force | Out-Null } # Back up existing config before modification if (Test-Path $configFile) { $backupPath = Join-Path $InstallDir "config-backup_claude-config.json" if (-not (Test-Path $backupPath)) { Copy-Item $configFile $backupPath Write-Host " Backed up existing config -> $backupPath" -ForegroundColor Green } $config = Get-Content $configFile -Raw | ConvertFrom-Json Write-Host " Loaded existing config" -ForegroundColor Green } else { $config = [PSCustomObject]@{} Write-Host " Creating new config" -ForegroundColor Green } # Feature flag if (-not $config.PSObject.Properties['cachedGrowthBookFeatures']) { $config | Add-Member -NotePropertyName 'cachedGrowthBookFeatures' -NotePropertyValue ([PSCustomObject]@{}) } if (-not $config.cachedGrowthBookFeatures.PSObject.Properties['tengu_ccr_bridge']) { $config.cachedGrowthBookFeatures | Add-Member -NotePropertyName 'tengu_ccr_bridge' -NotePropertyValue $true } else { $config.cachedGrowthBookFeatures.tengu_ccr_bridge = $true } # Skip remote dialog if (-not $config.PSObject.Properties['remoteDialogSeen']) { $config | Add-Member -NotePropertyName 'remoteDialogSeen' -NotePropertyValue $true } else { $config.remoteDialogSeen = $true } # Workspace trust $trustKey = $InstallDir.Replace('\', '/') if (-not $config.PSObject.Properties['projects']) { $config | Add-Member -NotePropertyName 'projects' -NotePropertyValue ([PSCustomObject]@{}) } if (-not $config.projects.PSObject.Properties[$trustKey]) { $trustObj = [PSCustomObject]@{ allowedTools = @() hasTrustDialogAccepted = $true hasCompletedProjectOnboarding = $true } $config.projects | Add-Member -NotePropertyName $trustKey -NotePropertyValue $trustObj } $config | ConvertTo-Json -Depth 10 | Set-Content $configFile -Encoding UTF8 Write-Host " Feature flag 'tengu_ccr_bridge' = true" -ForegroundColor Green Write-Host " Remote dialog suppressed" -ForegroundColor Green Write-Host " Workspace trust set for: $trustKey" -ForegroundColor Green } # --- Step 6: Generate service scripts --- Write-Host "" Write-Host "[6/7] Generating service scripts..." -ForegroundColor Yellow # --- 6a: reap-zombies.ps1 --- $reapScript = @' # reap-zombies.ps1 - Kill orphaned claude.exe processes from previous bridge sessions # Called by start-rc.bat before each bridge launch $InstallDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ourExePath = Join-Path $InstallDir "claude.exe" try { $procs = Get-CimInstance Win32_Process -Filter "Name='claude.exe'" -ErrorAction Stop foreach ($proc in $procs) { if (-not $proc.ExecutablePath) { continue } if ($proc.ExecutablePath -ne $ourExePath) { continue } $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] Reaping orphaned PID=$($proc.ProcessId) CMD=$($proc.CommandLine)" Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } } catch { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] Reaper error: $_" } '@ Set-Content (Join-Path $InstallDir "reap-zombies.ps1") $reapScript -Encoding UTF8 Write-Host " Generated: reap-zombies.ps1" -ForegroundColor Green # --- 6b: cleanup-transcripts.ps1 --- $cleanupScript = @' # cleanup-transcripts.ps1 - Remove old transcript files based on retention policy # Called by start-rc.bat before each bridge launch $InstallDir = Split-Path -Parent $MyInvocation.MyCommand.Path $transcriptDir = Join-Path $InstallDir "transcripts" $configFile = Join-Path $InstallDir "config.json" if (-not (Test-Path $transcriptDir)) { return } if (-not (Test-Path $configFile)) { return } try { $config = Get-Content $configFile -Raw | ConvertFrom-Json $retentionDays = 7 if ($config.transcripts.retentionDays -gt 0) { $retentionDays = $config.transcripts.retentionDays } $cutoff = (Get-Date).AddDays(-$retentionDays) $removed = 0 Get-ChildItem -Path $transcriptDir -File | Where-Object { $_.LastWriteTime -lt $cutoff } | ForEach-Object { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue $removed++ } if ($removed -gt 0) { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] Cleaned $removed transcript(s) older than $retentionDays days" } } catch { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] Transcript cleanup error: $_" } '@ Set-Content (Join-Path $InstallDir "cleanup-transcripts.ps1") $cleanupScript -Encoding UTF8 Write-Host " Generated: cleanup-transcripts.ps1" -ForegroundColor Green # --- 6c: name-sessions.ps1 --- $nameScript = @' # name-sessions.ps1 - Auto-rename transcript files with human-readable session names # Runs after bridge exits, renames bridge-transcript-*.jsonl and bridge-debug-*.log # to {timestamp}_{hostname}_{sessionId}.{ext} and logs to sessions.csv index $InstallDir = Split-Path -Parent $MyInvocation.MyCommand.Path $transcriptDir = Join-Path $InstallDir "transcripts" $configFile = Join-Path $InstallDir "config.json" $indexFile = Join-Path $transcriptDir "sessions.csv" if (-not (Test-Path $transcriptDir)) { return } try { # Read naming format from config $format = "{timestamp}_{hostname}_{sessionId}" if (Test-Path $configFile) { $config = Get-Content $configFile -Raw | ConvertFrom-Json if ($config.sessionNaming.format) { $format = $config.sessionNaming.format } } $hostname = [System.Net.Dns]::GetHostName() $renamed = 0 # Find un-renamed transcript files (still have bridge-transcript- or bridge-debug- prefix) Get-ChildItem -Path $transcriptDir -File | Where-Object { $_.Name -match '^bridge-(transcript|debug)-(.+)\.(jsonl|log)$' } | ForEach-Object { $type = $Matches[1] # "transcript" or "debug" $sessionId = $Matches[2] # e.g. "session_01WpsXdUQ..." $ext = $Matches[3] # "jsonl" or "log" $ts = $_.LastWriteTime.ToString("yyyy-MM-dd_HHmmss") # Build new name from format template $newName = $format -replace '\{timestamp\}', $ts ` -replace '\{hostname\}', $hostname ` -replace '\{sessionId\}', $sessionId $newName = "${newName}_${type}.${ext}" $newPath = Join-Path $transcriptDir $newName if (-not (Test-Path $newPath)) { Rename-Item $_.FullName $newPath -ErrorAction SilentlyContinue $renamed++ # Append to session index CSV if ($type -eq "transcript") { $indexLine = "$ts,$hostname,$sessionId,$newName" if (-not (Test-Path $indexFile)) { "timestamp,hostname,session_id,file" | Set-Content $indexFile -Encoding UTF8 } Add-Content $indexFile $indexLine -Encoding UTF8 } } } if ($renamed -gt 0) { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] Auto-named $renamed session file(s)" } } catch { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "[$timestamp] Session naming error: $_" } '@ Set-Content (Join-Path $InstallDir "name-sessions.ps1") $nameScript -Encoding UTF8 Write-Host " Generated: name-sessions.ps1" -ForegroundColor Green # --- 6d: run-hidden.vbs --- $vbsScript = @' ' run-hidden.vbs - Launch start-rc.bat with no visible window ' Used by the Scheduled Task to prevent an accidental close from killing the bridge Set WshShell = CreateObject("WScript.Shell") WshShell.Run "cmd.exe /c """"" & Replace(WScript.ScriptFullName, "run-hidden.vbs", "start-rc.bat") & """""", 0, True '@ $vbsPath = Join-Path $InstallDir "run-hidden.vbs" Set-Content $vbsPath $vbsScript -Encoding ASCII Write-Host " Generated: run-hidden.vbs" -ForegroundColor Green # --- 6e: start-rc.bat --- $batContent = @' @echo off setlocal enabledelayedexpansion :: Claude Remote Control - Persistent Service Script :: Auto-generated by install.ps1 :: Features: zombie reaping, auto session naming, config-driven flags, optional transcripts :: Stop gracefully: create a file named STOP in the install dir (e.g. echo.> C:\claude-rc-service\STOP) set CLAUDE_CONFIG_DIR=%USERPROFILE%\.claude set HOME=%USERPROFILE% set INSTALL_DIR=%~dp0 set CRASH_COUNT=0 set MAX_LOG_BYTES=5242880 :: Strip Node.js from PATH to prevent process.execPath resolving to node.exe :: This fixes "node.exe: bad option: --sdk-url" when the bridge spawns sessions set "PATH=!PATH:C:\Program Files\nodejs\;=!" set "PATH=!PATH:C:\Program Files\nodejs=!" set "PATH=!PATH:C:\Program Files (x86)\nodejs\;=!" set "PATH=!PATH:C:\Program Files (x86)\nodejs=!" if not exist "%INSTALL_DIR%logs" mkdir "%INSTALL_DIR%logs" cd /d "%INSTALL_DIR%" :loop :: Check for stop file — graceful shutdown if exist "%INSTALL_DIR%STOP" ( echo [%date% %time%] Stop file detected. Exiting service loop. >> "%INSTALL_DIR%logs\claude-rc.log" del "%INSTALL_DIR%STOP" 2>nul exit /b 0 ) :: Log rotation — if log exceeds ~5MB, rotate it for %%F in ("%INSTALL_DIR%logs\claude-rc.log") do ( if %%~zF GEQ !MAX_LOG_BYTES! ( if exist "%INSTALL_DIR%logs\claude-rc.log.old" del "%INSTALL_DIR%logs\claude-rc.log.old" rename "%INSTALL_DIR%logs\claude-rc.log" "claude-rc.log.old" echo [%date% %time%] Log rotated ^(previous log saved as claude-rc.log.old^) > "%INSTALL_DIR%logs\claude-rc.log" ) ) echo [%date% %time%] === Restart cycle === >> "%INSTALL_DIR%logs\claude-rc.log" :: Phase 1: Zombie reaping - kill orphaned claude.exe from our install dir powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%INSTALL_DIR%reap-zombies.ps1" >> "%INSTALL_DIR%logs\claude-rc.log" 2>&1 :: Phase 2: Transcript maintenance - cleanup old files, rename new ones if exist "%INSTALL_DIR%config.json" ( powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%INSTALL_DIR%cleanup-transcripts.ps1" >> "%INSTALL_DIR%logs\claude-rc.log" 2>&1 powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%INSTALL_DIR%name-sessions.ps1" >> "%INSTALL_DIR%logs\claude-rc.log" 2>&1 ) :: Phase 3: Read config and build flags set "EXTRA_FLAGS=" set "RESTART_DELAY=10" if exist "%INSTALL_DIR%config.json" ( for /f "usebackq tokens=*" %%A in (`powershell.exe -NoProfile -Command "(Get-Content '%INSTALL_DIR%config.json' -Raw | ConvertFrom-Json).transcripts.enabled"`) do ( if /i "%%A"=="True" ( if not exist "%INSTALL_DIR%transcripts" mkdir "%INSTALL_DIR%transcripts" set "EXTRA_FLAGS=--debug-file "%INSTALL_DIR%transcripts\bridge-debug.log"" ) ) for /f "usebackq tokens=*" %%A in (`powershell.exe -NoProfile -Command "(Get-Content '%INSTALL_DIR%config.json' -Raw | ConvertFrom-Json).verbose"`) do ( if /i "%%A"=="True" set "EXTRA_FLAGS=!EXTRA_FLAGS! --verbose" ) for /f "usebackq tokens=*" %%A in (`powershell.exe -NoProfile -Command "$c=(Get-Content '%INSTALL_DIR%config.json' -Raw | ConvertFrom-Json).restartDelaySecs; if($c){$c}else{'10'}"`) do ( set "RESTART_DELAY=%%A" ) ) :: Record start time for crash detection set START_TIME=%time% :: Phase 4: Launch bridge echo [%date% %time%] Starting claude remote-control !EXTRA_FLAGS! >> "%INSTALL_DIR%logs\claude-rc.log" "%INSTALL_DIR%claude.exe" remote-control !EXTRA_FLAGS! >> "%INSTALL_DIR%logs\claude-rc.log" 2>&1 :: Phase 5: Post-session - auto-name transcript files if exist "%INSTALL_DIR%config.json" ( powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%INSTALL_DIR%name-sessions.ps1" >> "%INSTALL_DIR%logs\claude-rc.log" 2>&1 ) :: Phase 6: Crash guard — detect rapid consecutive failures :: Use PowerShell to check if the process ran for less than 10 seconds for /f "usebackq tokens=*" %%R in (`powershell.exe -NoProfile -Command "$s='!START_TIME!'.Trim(); $now=Get-Date; try{ $then=[datetime]::Parse((Get-Date).ToString('yyyy-MM-dd')+' '+$s); if(($now-$then).TotalSeconds -lt 10){'FAST'}else{'OK'} }catch{'OK'}"`) do set CRASH_RESULT=%%R if /i "!CRASH_RESULT!"=="FAST" ( set /a CRASH_COUNT+=1 echo [%date% %time%] Rapid exit detected ^(count: !CRASH_COUNT!^) >> "%INSTALL_DIR%logs\claude-rc.log" ) else ( set CRASH_COUNT=0 ) if !CRASH_COUNT! GEQ 5 ( echo [%date% %time%] 5 consecutive rapid failures. Cooling down for 5 minutes... >> "%INSTALL_DIR%logs\claude-rc.log" timeout /t 300 /nobreak > nul set CRASH_COUNT=0 ) :: Phase 7: Restart delay echo [%date% %time%] Process exited. Restarting in !RESTART_DELAY!s... >> "%INSTALL_DIR%logs\claude-rc.log" timeout /t !RESTART_DELAY! /nobreak > nul goto loop '@ $batPath = Join-Path $InstallDir "start-rc.bat" Set-Content $batPath $batContent -Encoding ASCII Write-Host " Generated: start-rc.bat" -ForegroundColor Green # --- Step 7: Register Scheduled Task --- Write-Host "" Write-Host "[7/7] Registering Windows Scheduled Task..." -ForegroundColor Yellow $action = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument """$vbsPath""" -WorkingDirectory $InstallDir $trigger = New-ScheduledTaskTrigger -AtStartup -RandomDelay (New-TimeSpan -Seconds 30) $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -StartWhenAvailable ` -RestartCount 50 ` -RestartInterval (New-TimeSpan -Minutes 1) ` -ExecutionTimeLimit (New-TimeSpan -Days 365) $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -RunLevel Limited Register-ScheduledTask ` -TaskName $TaskName ` -Action $action ` -Trigger $trigger ` -Settings $settings ` -Principal $principal ` -Description "Claude Code Remote Control - persistent bridge for remote access via claude.ai/code" ` -Force | Out-Null Write-Host " Task '$TaskName' registered" -ForegroundColor Green # Start immediately try { Start-ScheduledTask -TaskName $TaskName Start-Sleep -Seconds 5 $taskState = (Get-ScheduledTask -TaskName $TaskName).State if ($taskState -eq 'Running') { Write-Host " Task state: $taskState" -ForegroundColor Green } else { Write-Host " Task state: $taskState" -ForegroundColor Yellow Write-Host " WARNING: Task registered but may not be running yet." -ForegroundColor Yellow Write-Host " Check logs: Get-Content '$logDir\claude-rc.log' -Tail 20" -ForegroundColor Yellow } } catch { Write-Host " WARNING: Task registered but failed to start: $_" -ForegroundColor Yellow Write-Host " Try starting manually: Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Yellow } # --- Done --- Write-Host "" Write-Host "=== Installation Complete ===" -ForegroundColor Cyan Write-Host "" Write-Host " Machine: $hostname" -ForegroundColor White Write-Host " Visible: https://claude.ai/code + Claude iOS/Android app" -ForegroundColor White Write-Host "" Write-Host "Manage:" -ForegroundColor White Write-Host " Status: (Get-ScheduledTask -TaskName '$TaskName').State" -ForegroundColor Gray Write-Host " Logs: Get-Content '$logDir\claude-rc.log' -Tail 20" -ForegroundColor Gray Write-Host " Config: notepad '$configPath'" -ForegroundColor Gray Write-Host " Transcripts: dir '$InstallDir\transcripts'" -ForegroundColor Gray Write-Host " Stop: Stop-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray Write-Host " Start: Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray Write-Host " Uninstall: Unregister-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray Write-Host ""