> ## Documentation Index
> Fetch the complete documentation index at: https://docs.velatir.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Uninstall Cleanup

> Remove every trace of Velatir for Desktop from a Windows device when a standard uninstall leaves something behind

export const UninstallScriptDownload = () => {
  const [copied, setCopied] = useState(false);
  const fileName = "Uninstall-VelatirDesktopApp.ps1";
  const scriptLines = ["<#", ".SYNOPSIS", "    Completely removes the Velatir desktop app and every system artifact it", "    creates on Windows. Run this alongside, or instead of, `msiexec /x` when a", "    normal uninstall leaves a machine in a broken state.", "", ".DESCRIPTION", "    NOTE: this file is published for customers (it is embedded in the public", "    documentation), so keep comments factual and free of internal product or", "    roadmap framing. The full internal rationale lives in", "    docs/windows-installer.md, which is not published.", "", "    Two traffic-capture backends have shipped, and which artifacts a machine", "    carries depends on which one ran:", "", "      * Wintun + tun2socks -- arm64, and x64 on versions 0.7.28 and earlier.", "        Creates a TUN adapter (\"Velatir\", 10.7.0.1/24) plus a low-metric default", "        route to the tun2socks gateway 10.7.0.2. If the host is force-killed (or", "        an uninstall is interrupted) that route can survive while tun2socks is", "        gone, so every packet is sent into a dead interface and the machine loses", "        connectivity -- the failure this script repairs first.", "", "      * WinDivert (WFP flow-redirect) -- x64 on versions after 0.7.28. Installs", "        NO adapter and NO route, so the route repair above is a no-op there.", "        Instead WinDivert.dll registers a kernel driver service (\"WinDivert\") on", "        first WinDivertOpen(), and that registration SURVIVES process exit --", "        upstream only removes it on reboot. Deleting the application files leaves", "        the service pointing at a driver file that is gone.", "", "    Both artifact sets are swept, so one script covers every version and", "    architecture. A machine that updated across the 0.7.28 boundary can carry", "    both.", "", "    The script is idempotent and best-effort: every step is wrapped so a single", "    failure never aborts the run. It can be run repeatedly and on machines that", "    are only partially installed. Order is deliberate:", "", "      1. Stop the service and kill processes        (stop new routes being added)", "      2. Delete the poison routes                   (RESTORE CONNECTIVITY FIRST)", "      3. msiexec /x the registered product          (clean the Installer database)", "      4. Delete service + scheduled task + the", "         WinDivert capture driver                   (all daemon/driver mechanisms)", "      5. Remove the \"Velatir\" Wintun adapter        (surgical -- see safety note)", "      6. Re-sweep routes + remove firewall rules", "      7. Remove the Velatir CA from the cert stores", "      8. Remove files and directories", "      9. Remove registry keys, PATH and per-user env", "     10. Flush DNS, then verify and report leftovers", "", "    SAFETY", "    ------", "    * Adapter removal matches the connection name \"Velatir\" only. WireGuard,", "      Tailscale and other Wintun users keep their adapters, and the shared", "      `wintun` driver package is intentionally NOT removed.", "    * WinDivert is a SHARED third-party driver (Spybot, NordVPN and others ship", "      it). The service is only deleted when its ImagePath points inside a", "      Velatir directory; a foreign owner is reported and left alone.", "    * Certificate removal matches certificates whose subject or friendly name", "      contains \"Velatir\" in the Root stores. Your own (bring-your-own) CA is not", "      touched.", "    * NODE_EXTRA_CA_CERTS is only cleared when it points at a Velatir path, so your", "      own Node CA bundle is preserved.", "    * No global IP-stack reset is performed; only Velatir's own routes are", "      deleted, so the real default route is preserved.", "", ".PARAMETER DryRun", "    Log every action without changing anything. Use this first to preview.", "", ".PARAMETER SkipMsiUninstall", "    Do not invoke msiexec (use when you have already run `msiexec /x` and only", "    want the leftover sweep).", "", ".PARAMETER Reboot", "    Reboot the machine when finished. A reboot is recommended to fully clear a", "    removed network adapter, but is not required for connectivity to return.", "", ".PARAMETER NoSelfElevate", "    Do not attempt to relaunch elevated when run without admin rights (just", "    fail). Useful for RMM/Intune contexts that already run as SYSTEM.", "", ".PARAMETER LegacyOnly", "    Sweep ONLY the orphaned legacy artifacts and leave a current install intact.", "    Use this when a machine was upgraded to a current build BEFORE this script", "    was ever run, so the legacy leftovers are stranded underneath a working", "    install. Removes: the orphaned WinDivert driver service, the superseded", "    legacy CA (pinned by thumbprint), a capture route whose adapter is already", "    gone, and a NODE_EXTRA_CA_CERTS value whose target file no longer exists.", "", "    Without this switch the script is a FULL uninstall: it runs `msiexec /x`", "    against any registered Velatir product, deletes the service and the install", "    directories, and removes every Velatir CA. Run against a machine that has", "    already been upgraded, that uninstalls the CURRENT build too.", "", ".EXAMPLE", "    powershell -ExecutionPolicy Bypass -File .\\Uninstall-VelatirDesktopApp.ps1", "", ".EXAMPLE", "    # Machine was already upgraded to a current build -- strip only the stranded", "    # legacy leftovers and leave the working install alone:", "    powershell -ExecutionPolicy Bypass -File .\\Uninstall-VelatirDesktopApp.ps1 -LegacyOnly", "", ".EXAMPLE", "    # Preview only, change nothing:", "    powershell -ExecutionPolicy Bypass -File .\\Uninstall-VelatirDesktopApp.ps1 -DryRun", "", ".NOTES", "    Requires administrator / SYSTEM. Targets Windows PowerShell 5.1 (built in to", "    Windows 10/11) and PowerShell 7+. Run from an elevated prompt, an RMM tool,", "    or Intune (Run as SYSTEM).", "#>", "[CmdletBinding()]", "param(", "    [switch]$DryRun,", "    [switch]$SkipMsiUninstall,", "    [switch]$Reboot,", "    [switch]$NoSelfElevate,", "    [switch]$LegacyOnly", ")", "", "# Best-effort throughout: keep going on errors, we handle them per-step.", "$ErrorActionPreference = 'Continue'", "$ProgressPreference    = 'SilentlyContinue'", "", "# \u2500\u2500 Constants: the exact artifacts the app creates (see velatir-desktop-app) \u2500\u2500", "$AdapterName      = 'Velatir'                                   # AdapterConfigurator.AdapterName", "$TunGateway       = '10.7.0.2'                                  # AdapterConfigurator.TunGatewayIpv4", "# NOTE: no $AdapterSubnet constant. The on-link 10.7.0.0/24 route is deleted via", "# Get-NetRoute by interface, never via route.exe by destination -- see the safety", "# note in section 2(c) for why an unqualified destination delete is unsafe.", "$ServiceName      = 'VelatirAgent'                              # WindowsServiceInstaller.ServiceName", "$ScheduledTask    = 'Velatir'                                   # Program.cs --register-startup", "$ProcessNames     = @('Velatir', 'VelatirAgent', 'velatir', 'tun2socks')", "$FirewallRuleName = 'Velatir QUIC Block (UDP 443)'             # Program.cs --unregister-wintun", "$UpgradeCode      = '{7A3B8F1E-4C2D-4E5F-9A6B-1D2E3F4A5B6C}'   # Variables.wxi", "$ProductName      = 'Velatir'", "$CertMatch        = 'velatir'                                   # subject/friendly-name contains (CN=Velatir Desktop CA, older CN=VELATIR, 0.8.3 friendly-name \"Velatir CA\")", "$WinDivertService = 'WinDivert'                                  # vendor/windivert 2.2.2 -- service the DLL registers on WinDivertOpen()", "$WinDivertRegPath = \"HKLM:\\SYSTEM\\CurrentControlSet\\Services\\WinDivert\"   # registry is authoritative; Get-Service is unreliable for kernel drivers", "$AumidSubKey      = 'Software\\Classes\\AppUserModelId\\Velatir.DesktopApp'  # WindowsNotificationService.EnsureAumidRegistered (per-user, runtime-written)", "# HKCU\\Software\\Classes is a MERGED view -- per-user writes land in the separate", "# HKEY_USERS\\<SID>_Classes hive (UsrClass.dat), not in <SID>\\Software\\Classes. Sweep", "# both layouts so other profiles are actually covered.", "$AumidClassesKey  = 'AppUserModelId\\Velatir.DesktopApp'", "$NodeCaEnvVar     = 'NODE_EXTRA_CA_CERTS'                        # FluxzyHostedService sets this persistently (User scope)", "$OwnershipMatch   = 'velatir'                                    # \"is this shared artifact ours?\" -- matched against driver ImagePath / env values", "", "# Superseded CA from builds that predate per-device certificate generation. It expires", "# 2036-03-13, and it carries the SAME subject (CN=Velatir Desktop CA, O=Velatir, C=DK)", "# as the current per-device CA -- so subject matching cannot tell the two apart. The", "# thumbprint can: this one is identical on every install of that era, while a per-device", "# CA is unique to its machine. Pinned by thumbprint so -LegacyOnly removes the", "# superseded cert and never disturbs a live per-device CA.", "#", "# NOTE: this file is published for customer download, so keep the rationale for WHY the", "# superseded cert must go in the internal runbook (docs/windows-installer.md), not here.", "$LegacyCaThumbprint = 'FB20E36BB095598CF2FD08DCED5E5816C6465D7C'", "$InstallDir       = Join-Path $env:ProgramFiles 'Velatir'      # C:\\Program Files\\Velatir", "$ProgramDataDir   = Join-Path $env:ProgramData  'Velatir'      # C:\\ProgramData\\Velatir", "$StartMenuLink    = Join-Path $env:ProgramData  'Microsoft\\Windows\\Start Menu\\Programs\\Velatir.lnk'", "", "# Capture-route prefixes the app lays down (active + the older /1 halves).", "$CaptureRoutes = @(", "    @{ Dest = '0.0.0.0';   Mask = '0.0.0.0'   },   # active 0.0.0.0/0", "    @{ Dest = '0.0.0.0';   Mask = '128.0.0.0' },   # legacy 0.0.0.0/1", "    @{ Dest = '128.0.0.0'; Mask = '128.0.0.0' }    # legacy 128.0.0.0/1", ")", "", "# \u2500\u2500 Logging helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "$script:Leftovers = New-Object System.Collections.Generic.List[string]", "", "function Write-Section($text) { Write-Host \"\"; Write-Host \"==== $text ====\" -ForegroundColor Cyan }", "function Write-Step($text)    { Write-Host \"[*] $text\" -ForegroundColor White }", "function Write-Ok($text)      { Write-Host \"    OK   $text\" -ForegroundColor Green }", "function Write-Skip($text)    { Write-Host \"    --   $text\" -ForegroundColor DarkGray }", "function Write-Warn2($text)   { Write-Host \"    WARN $text\" -ForegroundColor Yellow }", "function Write-Dry($text)     { Write-Host \"    DRY  would $text\" -ForegroundColor Magenta }", "", "# Run a mutating action unless -DryRun. $Action is a scriptblock; failures are", "# logged, never thrown. Returns $true on success, $false on failure/skip.", "function Invoke-Mutation {", "    param([string]$Description, [scriptblock]$Action)", "    if ($DryRun) { Write-Dry $Description; return $false }", "    try {", "        & $Action", "        Write-Ok $Description", "        return $true", "    } catch {", "        Write-Warn2 \"$Description -- $($_.Exception.Message)\"", "        return $false", "    }", "}", "", "# Run a native command (sc.exe, netsh, route, msiexec...) without throwing.", "# Returns the exit code (or -1 if it could not be launched).", "function Invoke-Native {", "    param([string]$File, [string[]]$Arguments)", "    if ($DryRun) { Write-Dry \"$File $($Arguments -join ' ')\"; return 0 }", "    try {", "        $out = & $File @Arguments 2>&1", "        $code = $LASTEXITCODE", "        if ($out) { $out | ForEach-Object { Write-Host \"      $_\" -ForegroundColor DarkGray } }", "        return $code", "    } catch {", "        Write-Warn2 \"$File failed to launch -- $($_.Exception.Message)\"", "        return -1", "    }", "}", "", "# Delete a directory tree robustly, handling junctions/symlinks (the install", "# tree contains a `current` junction). rmdir removes the link, not its target.", "function Remove-DirHard {", "    param([string]$Path)", "    if (-not (Test-Path -LiteralPath $Path)) { Write-Skip \"not present: $Path\"; return }", "    if ($DryRun) { Write-Dry \"delete tree $Path\"; return }", "", "    # Remove reparse points first so recursion never traverses a junction.", "    Get-ChildItem -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue |", "        Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } |", "        ForEach-Object { & cmd.exe /c rmdir /q \"$($_.FullName)\" 2>$null }", "", "    Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue", "    if (Test-Path -LiteralPath $Path) {", "        & cmd.exe /c rmdir /s /q \"$Path\" 2>$null", "    }", "    if (Test-Path -LiteralPath $Path) { Write-Warn2 \"could not fully remove $Path\"; $script:Leftovers.Add(\"dir: $Path\") }", "    else { Write-Ok \"removed $Path\" }", "}", "", "# \u2500\u2500 Verification + summary (shared by the full and -LegacyOnly paths) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "", "function Check-Gone {", "    param([string]$Label, [scriptblock]$Test)", "    # $Test returns $true when the artifact is GONE (clean).", "    try {", "        if (& $Test) { Write-Ok \"$Label : clean\" }", "        else { Write-Warn2 \"$Label : LEFTOVER\"; $script:Leftovers.Add($Label) }", "    } catch { Write-Ok \"$Label : clean\" }", "}", "", "# Prints the summary, closes the transcript, optionally reboots, and EXITS the script.", "function Complete-Run {", "    Write-Host \"\"", "    if ($DryRun) {", "        Write-Host \"DRY RUN complete -- no changes were made. Re-run without -DryRun to apply.\" -ForegroundColor Magenta", "    } elseif ($script:Leftovers.Count -eq 0) {", "        Write-Host \"SUCCESS -- $(if ($LegacyOnly) { 'all legacy leftovers removed; the current install was left intact.' } else { 'all Velatir desktop app artifacts removed.' })\" -ForegroundColor Green", "        Write-Host \"A reboot is recommended to finish clearing the removed $(if ($LegacyOnly) { 'kernel driver' } else { 'network adapter' }).\" -ForegroundColor Yellow", "    } else {", "        Write-Host \"DONE with $($script:Leftovers.Count) leftover(s) -- most clear after a reboot:\" -ForegroundColor Yellow", "        $script:Leftovers | ForEach-Object { Write-Host \"    - $_\" -ForegroundColor Yellow }", "        Write-Host \"Reboot and re-run this script if any persist.\" -ForegroundColor Yellow", "    }", "    Write-Host \"Log saved to: $logPath\" -ForegroundColor DarkCyan", "", "    try { Stop-Transcript | Out-Null } catch { }", "", "    if ($Reboot -and -not $DryRun) {", "        Write-Host \"Rebooting in 10 seconds (Ctrl+C to cancel)...\" -ForegroundColor Yellow", "        Start-Sleep -Seconds 10", "        Restart-Computer -Force", "    }", "", "    # Exit code: 0 clean (or dry run), 2 if leftovers remain (so RMM/Intune can flag).", "    if (-not $DryRun -and $script:Leftovers.Count -gt 0) { exit 2 } else { exit 0 }", "}", "", "# \u2500\u2500 Legacy-artifact helpers (shared by the full and -LegacyOnly paths) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "", "function Get-WinDivertImagePath {", "    try { return (Get-ItemProperty -Path $WinDivertRegPath -Name 'ImagePath' -ErrorAction Stop).ImagePath }", "    catch { return '' }", "}", "", "# WinDivert capture driver (x64 payloads after 0.7.28).", "# WinDivert.dll registers this kernel service on the first WinDivertOpen(); upstream", "# only unregisters it on reboot, so deleting the payload leaves a driver service", "# pointing at a .sys that no longer exists. Current builds carry no WinDivert code at", "# all, so nothing in the product will ever clean this up on its own.", "#", "# WinDivert is a SHARED third-party driver, so only delete it when it is demonstrably", "# ours: ImagePath must reference a Velatir directory.", "function Remove-VelatirWinDivertDriver {", "    if (-not (Test-Path $WinDivertRegPath)) {", "        Write-Skip \"no '$WinDivertService' driver service (Wintun-era or arm64 install)\"", "        return", "    }", "    $imagePath = Get-WinDivertImagePath", "    Write-Step \"Driver service '$WinDivertService' present (ImagePath: $(if ($imagePath) { $imagePath } else { '<none>' }))\"", "", "    if ($imagePath -notmatch $OwnershipMatch) {", "        # Not ours -- another product installed WinDivert. Never touch it.", "        Write-Warn2 \"driver '$WinDivertService' ImagePath is not a Velatir path -- another product owns it, leaving it installed\"", "        return", "    }", "    Invoke-Native -File 'sc.exe' -Arguments @('stop', $WinDivertService) | Out-Null", "    Invoke-Native -File 'sc.exe' -Arguments @('delete', $WinDivertService) | Out-Null", "    if (-not $DryRun) {", "        if (Test-Path $WinDivertRegPath) {", "            Write-Warn2 \"driver '$WinDivertService' still registered (delete pending; clears on reboot)\"", "        } else {", "            Write-Ok \"driver service '$WinDivertService' deleted\"", "        }", "    }", "}", "", "# The superseded legacy CA, pinned by thumbprint. Subject matching cannot be used here:", "# it shares the subject CN=Velatir Desktop CA with the current per-device cert, so a", "# subject sweep would also strip a live install's per-device CA.", "function Remove-SupersededLegacyCa {", "    $found = $false", "    foreach ($loc in @('LocalMachine', 'CurrentUser')) {", "        $storePath = \"Cert:\\$loc\\Root\"", "        $cert = Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue |", "            Where-Object { $_.Thumbprint -eq $LegacyCaThumbprint }", "        if (-not $cert) { continue }", "        $found = $true", "        foreach ($c in $cert) {", "            Invoke-Mutation \"remove superseded legacy CA '$($c.Subject)' ($($c.Thumbprint)) from $loc\\Root\" {", "                Remove-Item -Path \"$storePath\\$($c.Thumbprint)\" -Force -ErrorAction Stop", "            } | Out-Null", "        }", "    }", "    if (-not $found) { Write-Skip \"superseded legacy CA ($LegacyCaThumbprint) not trusted\" }", "}", "", "# \u2500\u2500 0. Elevation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "function Test-Admin {", "    $id = [Security.Principal.WindowsIdentity]::GetCurrent()", "    (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)", "}", "", "if (-not (Test-Admin)) {", "    if ($NoSelfElevate -or -not [Environment]::UserInteractive) {", "        Write-Host \"This script must run as Administrator (or SYSTEM).\" -ForegroundColor Red", "        exit 1", "    }", "    Write-Host \"Not elevated -- relaunching with administrator rights...\" -ForegroundColor Yellow", "    $psExe = (Get-Process -Id $PID).Path  # powershell.exe or pwsh.exe", "    $argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', \"`\"$PSCommandPath`\"\")", "    # Every switch MUST be forwarded. Dropping -LegacyOnly here would silently", "    # escalate a legacy-only sweep into a full uninstall of the current install.", "    if ($DryRun)           { $argList += '-DryRun' }", "    if ($SkipMsiUninstall) { $argList += '-SkipMsiUninstall' }", "    if ($Reboot)           { $argList += '-Reboot' }", "    if ($LegacyOnly)       { $argList += '-LegacyOnly' }", "    try { Start-Process -FilePath $psExe -Verb RunAs -ArgumentList $argList } catch { Write-Host \"Elevation declined.\" -ForegroundColor Red }", "    exit", "}", "", "# \u2500\u2500 Transcript \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "$stamp   = Get-Date -Format 'yyyyMMdd-HHmmss'", "$logPath = Join-Path $env:TEMP \"Velatir-uninstall-$stamp.log\"", "try { Start-Transcript -Path $logPath -Force | Out-Null } catch { }", "", "Write-Host \"\"", "Write-Host \"  Velatir Desktop App -- full uninstall / cleanup\" -ForegroundColor Cyan", "Write-Host \"  Machine : $env:COMPUTERNAME\" -ForegroundColor DarkCyan", "Write-Host \"  User    : $([Security.Principal.WindowsIdentity]::GetCurrent().Name)\" -ForegroundColor DarkCyan", "Write-Host \"  Mode    : $(if ($DryRun) { 'DRY RUN (no changes)' } else { 'LIVE' })$(if ($LegacyOnly) { ' -- LEGACY ONLY (current install preserved)' } else { ' -- FULL UNINSTALL' })\" -ForegroundColor DarkCyan", "Write-Host \"  Log     : $logPath\" -ForegroundColor DarkCyan", "", "# \u2500\u2500 Legacy-only sweep \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "# For a machine that was upgraded to a current build BEFORE this script ever ran, so", "# the legacy leftovers are stranded underneath a working install. Every step here is", "# either impossible for a current build to own, or is proven orphaned before it is", "# touched -- so nothing in this path can damage the live install.", "if ($LegacyOnly) {", "    Write-Section 'Legacy-only sweep (current install left intact)'", "    Write-Host \"  Nothing here touches the live install: no msiexec, no service or\" -ForegroundColor DarkCyan", "    Write-Host \"  directory removal, no per-device CA removal.\" -ForegroundColor DarkCyan", "", "    Write-Section 'L1. Orphaned WinDivert capture driver'", "    Remove-VelatirWinDivertDriver", "", "    Write-Section 'L2. Superseded legacy CA'", "    Remove-SupersededLegacyCa", "", "    Write-Section 'L3. Orphaned capture route'", "    # Only safe when the adapter is already gone -- that proves no live tunnel owns it.", "    if (Get-NetAdapter -Name $AdapterName -IncludeHidden -ErrorAction SilentlyContinue) {", "        Write-Warn2 \"adapter '$AdapterName' still present -- a live install may own its route, skipping (use a full run to remove it)\"", "    } else {", "        $orphanRoutes = Get-NetRoute -ErrorAction SilentlyContinue | Where-Object { $_.NextHop -eq $TunGateway }", "        if ($orphanRoutes) {", "            foreach ($r in $orphanRoutes) {", "                Invoke-Mutation \"delete orphaned route $($r.DestinationPrefix) -> $TunGateway (adapter already gone)\" {", "                    $r | Remove-NetRoute -Confirm:$false -ErrorAction Stop", "                } | Out-Null", "            }", "        } else {", "            Write-Skip \"no routes via $TunGateway\"", "        }", "    }", "", "    Write-Section 'L4. Stale NODE_EXTRA_CA_CERTS'", "    # Only clear when the target file is GONE. A live install's value points at a real", "    # PEM and must survive.", "    foreach ($scope in @('User', 'Machine')) {", "        try {", "            $val = [Environment]::GetEnvironmentVariable($NodeCaEnvVar, $scope)", "            if (-not $val) {", "                Write-Skip \"$NodeCaEnvVar not set ($scope)\"", "            } elseif ($val -notmatch $OwnershipMatch) {", "                Write-Warn2 \"$NodeCaEnvVar ($scope) is '$val' -- not a Velatir path, leaving it\"", "            } elseif (Test-Path -LiteralPath $val) {", "                Write-Skip \"$NodeCaEnvVar ($scope) -> '$val' still exists (live install), leaving it\"", "            } else {", "                Invoke-Mutation \"clear stale $NodeCaEnvVar ($scope) -- target '$val' is gone\" {", "                    [Environment]::SetEnvironmentVariable($NodeCaEnvVar, $null, $scope)", "                } | Out-Null", "            }", "        } catch { Write-Warn2 \"$NodeCaEnvVar ($scope) check failed -- $($_.Exception.Message)\" }", "    }", "", "    Write-Section 'Verification'", "    Check-Gone \"WinDivert driver service\" { -not (Test-Path $WinDivertRegPath) -or ((Get-WinDivertImagePath) -notmatch $OwnershipMatch) }", "    Check-Gone \"superseded legacy CA\"         { -not (Get-ChildItem 'Cert:\\LocalMachine\\Root' -ErrorAction SilentlyContinue | Where-Object { $_.Thumbprint -eq $LegacyCaThumbprint }) }", "    Complete-Run", "}", "", "# \u2500\u2500 1. Stop the service and kill processes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '1. Stop service and processes'", "", "$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue", "if ($svc) {", "    Write-Step \"Service '$ServiceName' is present (status: $($svc.Status)) -- disabling and stopping\"", "    # Disable first so SCM recovery actions can't restart it while we work.", "    Invoke-Native -File 'sc.exe' -Arguments @('config', $ServiceName, 'start=', 'disabled') | Out-Null", "    Invoke-Mutation \"stop service $ServiceName\" { Stop-Service -Name $ServiceName -Force -ErrorAction Stop } | Out-Null", "    Invoke-Native -File 'sc.exe' -Arguments @('stop', $ServiceName) | Out-Null", "} else {", "    Write-Skip \"service '$ServiceName' not installed\"", "}", "", "foreach ($p in ($ProcessNames | Sort-Object -Unique)) {", "    $procs = Get-Process -Name $p -ErrorAction SilentlyContinue", "    if ($procs) {", "        Invoke-Mutation \"kill process '$p' (PID $($procs.Id -join ', '))\" {", "            Stop-Process -Name $p -Force -ErrorAction Stop", "        } | Out-Null", "    } else {", "        Write-Skip \"process '$p' not running\"", "    }", "}", "", "# \u2500\u2500 2. Delete the poison routes (restore connectivity FIRST) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '2. Remove Velatir routes (restore connectivity)'", "", "# Enumerate ONCE, and record whether the enumeration actually worked. \"Cmdlet", "# returned nothing\" and \"cmdlet is unavailable or threw\" are indistinguishable", "# under -ErrorAction SilentlyContinue, and the difference decides whether the", "# route.exe fallback in (c) is needed at all.", "$routeEnumOk = $false", "$allRoutes   = @()", "try {", "    $allRoutes   = @(Get-NetRoute -ErrorAction Stop)", "    $routeEnumOk = $true", "} catch {", "    Write-Warn2 \"Get-NetRoute unavailable ($($_.Exception.Message)) -- falling back to route.exe\"", "}", "", "# (a) Anything whose next hop is the dead tun2socks gateway -- the highest-value", "#     fix. Catches the default route(s) regardless of interface name or state.", "$byHop = $allRoutes | Where-Object { $_.NextHop -eq $TunGateway }", "if ($byHop) {", "    foreach ($r in $byHop) {", "        Invoke-Mutation \"delete route $($r.DestinationPrefix) -> $TunGateway (ifIndex $($r.ifIndex))\" {", "            $r | Remove-NetRoute -Confirm:$false -ErrorAction Stop", "        } | Out-Null", "    }", "} elseif ($routeEnumOk) {", "    Write-Skip \"no routes via $TunGateway\"", "}", "", "# (b) Anything bound to the Velatir interface (on-link 10.7.0.0/24, etc.).", "$byIf = $allRoutes | Where-Object { $_.InterfaceAlias -eq $AdapterName }", "if ($byIf) {", "    foreach ($r in $byIf) {", "        Invoke-Mutation \"delete route $($r.DestinationPrefix) on '$AdapterName'\" {", "            $r | Remove-NetRoute -Confirm:$false -ErrorAction Stop", "        } | Out-Null", "    }", "} elseif ($routeEnumOk) {", "    Write-Skip \"no routes on interface '$AdapterName'\"", "}", "", "# (c) route.exe fallback, ONLY when the routing table could not be enumerated", "#     above. When Get-NetRoute works, (a) and (b) are authoritative -- next-hop", "#     and interface are exactly the two ways a capture route can be ours -- so", "#     firing blind deletes as well would add risk and no coverage.", "#", "#     SAFETY: every delete here is qualified by the gateway $TunGateway, so it can", "#     only match a route whose next hop is Velatir's own TUN gateway. Never issue", "#     an unqualified `route delete <dest> mask <mask>`: with the gateway omitted,", "#     route.exe deletes EVERY route matching that destination and mask on any", "#     interface via any gateway. An earlier version did that for 10.7.0.0/24,", "#     which would have deleted a customer's own route had they happened to use", "#     that subnet. The on-link subnet route needs no fallback anyway: it belongs", "#     to the adapter, so it cannot outlive it, and (b) covers it while it exists.", "if (-not $routeEnumOk) {", "    Write-Step \"Sweeping capture routes via route.exe (each qualified by next hop $TunGateway)\"", "    foreach ($cr in $CaptureRoutes) {", "        Invoke-Native -File 'route.exe' -Arguments @('delete', $cr.Dest, 'mask', $cr.Mask, $TunGateway) | Out-Null", "    }", "} else {", "    Write-Skip 'route.exe fallback not needed (routing table enumerated directly)'", "}", "", "# \u2500\u2500 3. msiexec uninstall of the registered product (cleans Installer database) \u2500\u2500", "Write-Section '3. Uninstall registered MSI product'", "", "if ($SkipMsiUninstall) {", "    Write-Skip 'msiexec uninstall skipped (-SkipMsiUninstall)'", "} else {", "    $uninstallRoots = @(", "        'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',", "        'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'", "    )", "    $found = @()", "    foreach ($root in $uninstallRoots) {", "        Get-ChildItem $root -ErrorAction SilentlyContinue | ForEach-Object {", "            $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue", "            if ($props.DisplayName -like \"*$ProductName*\" -or $props.Publisher -eq 'Velatir ApS') {", "                $found += [pscustomobject]@{ Key = $_.PSChildName; Name = $props.DisplayName; Uninstall = $props.UninstallString }", "            }", "        }", "    }", "    if ($found.Count -eq 0) {", "        Write-Skip 'no Velatir product registered in Programs & Features'", "    }", "    foreach ($f in ($found | Sort-Object Key -Unique)) {", "        Write-Step \"Uninstalling '$($f.Name)' ($($f.Key))\"", "        if ($f.Key -match '^\\{[0-9A-Fa-f\\-]+\\}$') {", "            # ProductCode GUID -> drive msiexec directly (runs the MSI's own", "            # cert/service/route custom actions, then removes the ARP entry).", "            Invoke-Native -File 'msiexec.exe' -Arguments @('/x', $f.Key, '/qn', '/norestart') | Out-Null", "        } elseif ($f.Uninstall) {", "            Write-Step \"Running uninstall string: $($f.Uninstall)\"", "            Invoke-Native -File 'cmd.exe' -Arguments @('/c', $f.Uninstall, '/qn', '/norestart') | Out-Null", "        }", "    }", "}", "", "# \u2500\u2500 4. Delete the service and scheduled task (both daemon mechanisms) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '4. Remove service + scheduled task'", "", "if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) {", "    Invoke-Native -File 'sc.exe' -Arguments @('delete', $ServiceName) | Out-Null", "    if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) {", "        Write-Warn2 \"service '$ServiceName' still present (delete pending; clears on reboot)\"", "    } else { Write-Ok \"service '$ServiceName' deleted\" }", "} else {", "    Write-Skip \"service '$ServiceName' not present\"", "}", "", "$task = Get-ScheduledTask -TaskName $ScheduledTask -ErrorAction SilentlyContinue", "if ($task) {", "    Invoke-Mutation \"delete scheduled task '$ScheduledTask'\" {", "        Unregister-ScheduledTask -TaskName $ScheduledTask -Confirm:$false -ErrorAction Stop", "    } | Out-Null", "} else {", "    # schtasks fallback (covers task-name edge cases the cmdlet misses).", "    Invoke-Native -File 'schtasks.exe' -Arguments @('/Delete', '/TN', $ScheduledTask, '/F') | Out-Null", "}", "", "Remove-VelatirWinDivertDriver", "", "# \u2500\u2500 5. Remove the \"Velatir\" Wintun adapter (surgical) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '5. Remove Wintun adapter'", "", "$adapters = Get-NetAdapter -Name $AdapterName -IncludeHidden -ErrorAction SilentlyContinue", "if (-not $adapters) {", "    Write-Skip \"no adapter named '$AdapterName' (already gone, or torn down on process exit)\"", "} else {", "    foreach ($a in $adapters) {", "        # Defensive: only remove if it is the Wintun tunnel device, never some", "        # unrelated NIC a user happened to rename \"Velatir\".", "        if ($a.InterfaceDescription -notlike '*Wintun*' -and $a.InterfaceDescription -notlike '*Tunnel*') {", "            Write-Warn2 \"adapter '$AdapterName' is '$($a.InterfaceDescription)', not a Wintun tunnel -- skipping for safety\"", "            continue", "        }", "        $pnpId = $a.PnPDeviceID", "        Write-Step \"Removing adapter '$AdapterName' ($($a.InterfaceDescription), $pnpId)\"", "        $removed = $false", "        if ($pnpId) {", "            $removed = Invoke-Mutation \"remove PnP device $pnpId\" {", "                Get-PnpDevice -InstanceId $pnpId -ErrorAction Stop | Remove-PnpDevice -Confirm:$false -ErrorAction Stop | Out-Null", "            }", "            if (-not $removed) {", "                # pnputil fallback for hosts without the Remove-PnpDevice cmdlet.", "                if ((Invoke-Native -File 'pnputil.exe' -Arguments @('/remove-device', $pnpId)) -eq 0) { $removed = $true }", "            }", "        }", "        if (-not $removed) {", "            Invoke-Mutation \"disable adapter '$AdapterName' (could not remove device; reboot to clear)\" {", "                Disable-NetAdapter -Name $AdapterName -Confirm:$false -ErrorAction Stop", "            } | Out-Null", "        }", "    }", "}", "# NOTE: the shared `wintun` driver package is intentionally left installed --", "# WireGuard / Tailscale and others rely on it. Only the adapter is removed.", "", "# \u2500\u2500 6. Re-sweep routes + remove firewall rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '6. Firewall rules + final route sweep'", "", "# QUIC block rule from older builds (newer builds drop QUIC at the relay).", "$fwRules = Get-NetFirewallRule -ErrorAction SilentlyContinue |", "    Where-Object { $_.DisplayName -like 'Velatir*' -or $_.Name -like 'Velatir*' }", "if ($fwRules) {", "    foreach ($rule in $fwRules) {", "        Invoke-Mutation \"remove firewall rule '$($rule.DisplayName)'\" {", "            $rule | Remove-NetFirewallRule -ErrorAction Stop", "        } | Out-Null", "    }", "} else {", "    Write-Skip 'no Velatir firewall rules'", "}", "Invoke-Native -File 'netsh.exe' -Arguments @('advfirewall', 'firewall', 'delete', 'rule', \"name=$FirewallRuleName\") | Out-Null", "", "# Routes one more time (the adapter is gone now, but be thorough).", "$residual = Get-NetRoute -ErrorAction SilentlyContinue | Where-Object { $_.NextHop -eq $TunGateway }", "if ($residual) {", "    foreach ($r in $residual) {", "        Invoke-Mutation \"delete residual route $($r.DestinationPrefix) -> $TunGateway\" {", "            $r | Remove-NetRoute -Confirm:$false -ErrorAction Stop", "        } | Out-Null", "    }", "} else {", "    Write-Skip \"no residual routes via $TunGateway\"", "}", "", "# \u2500\u2500 7. Remove the Velatir CA from the certificate stores \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '7. Remove Velatir CA certificate'", "", "foreach ($loc in @('LocalMachine', 'CurrentUser')) {", "    $storePath = \"Cert:\\$loc\\Root\"", "    $certs = Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue |", "        Where-Object { $_.Subject -match $CertMatch -or $_.FriendlyName -match $CertMatch }", "    if ($certs) {", "        foreach ($c in $certs) {", "            Invoke-Mutation \"remove cert '$($c.Subject)' (thumbprint $($c.Thumbprint)) from $loc\\Root\" {", "                Remove-Item -Path \"$storePath\\$($c.Thumbprint)\" -Force -ErrorAction Stop", "            } | Out-Null", "        }", "    } else {", "        Write-Skip \"no Velatir cert in $loc\\Root\"", "    }", "}", "", "# \u2500\u2500 8. Remove files and directories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '8. Remove files and directories'", "", "Remove-DirHard -Path $InstallDir", "Remove-DirHard -Path $ProgramDataDir", "", "# Per-user logs / extracted cert (%LOCALAPPDATA%\\Velatir\\DesktopApp on every profile).", "Get-ChildItem -Path (Join-Path $env:SystemDrive 'Users') -Directory -ErrorAction SilentlyContinue | ForEach-Object {", "    $userVelatir = Join-Path $_.FullName 'AppData\\Local\\Velatir'", "    if (Test-Path -LiteralPath $userVelatir) { Remove-DirHard -Path $userVelatir }", "}", "", "# Start Menu shortcut (All Users).", "if (Test-Path -LiteralPath $StartMenuLink) {", "    Invoke-Mutation \"remove Start Menu shortcut $StartMenuLink\" { Remove-Item -LiteralPath $StartMenuLink -Force -ErrorAction Stop } | Out-Null", "} else {", "    Write-Skip 'no Start Menu shortcut'", "}", "", "# \u2500\u2500 9. Remove registry keys, the PATH entry and per-user env \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '9. Remove registry + PATH + per-user env'", "", "# HKLM\\Software\\Velatir, HKCU\\Software\\Velatir, and the toast AUMID registration", "# (current user). The AUMID key is written at RUNTIME by WindowsNotificationService,", "# not just by the MSI, so msiexec /x does not take the per-user copies with it.", "foreach ($key in @('HKLM:\\SOFTWARE\\Velatir', 'HKCU:\\SOFTWARE\\Velatir', \"HKCU:\\$AumidSubKey\")) {", "    if (Test-Path $key) {", "        Invoke-Mutation \"remove registry key $key\" { Remove-Item -Path $key -Recurse -Force -ErrorAction Stop } | Out-Null", "    } else {", "        Write-Skip \"no key $key\"", "    }", "}", "", "# Every loaded user hive (covers other profiles' per-user keys). Get-ChildItem on", "# HKEY_USERS enumerates both <SID> and <SID>_Classes hives; each candidate subkey is", "# Test-Path guarded, so probing all three layouts against every hive is harmless.", "foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {", "    foreach ($sub in @('SOFTWARE\\Velatir', $AumidSubKey, $AumidClassesKey)) {", "        $userKey = \"Registry::$($hive.Name)\\$sub\"", "        if (Test-Path $userKey) {", "            Invoke-Mutation \"remove $userKey\" { Remove-Item -Path $userKey -Recurse -Force -ErrorAction Stop } | Out-Null", "        }", "    }", "}", "", "# Strip \"C:\\Program Files\\Velatir\" from the machine PATH.", "try {", "    $machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')", "    if ($machinePath) {", "        $target = $InstallDir.TrimEnd('\\')", "        $kept = $machinePath -split ';' | Where-Object { $_ -and ($_.TrimEnd('\\') -ne $target) }", "        $newPath = ($kept -join ';')", "        if ($newPath -ne $machinePath) {", "            Invoke-Mutation \"remove '$target' from machine PATH\" {", "                [Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine')", "            } | Out-Null", "        } else {", "            Write-Skip 'machine PATH has no Velatir entry'", "        }", "    }", "} catch { Write-Warn2 \"PATH cleanup failed -- $($_.Exception.Message)\" }", "", "# NODE_EXTRA_CA_CERTS -- the proxy sets this persistently (User scope) to the", "# per-device CA PEM under %LOCALAPPDATA%\\Velatir. It is cleared on a graceful stop,", "# but a force-killed host leaves it pointing at a PEM we are about to delete, and", "# every Node-based AI client then fails to start on a missing CA file.", "# Only clear it when the value is ours -- never discard your own bundle.", "foreach ($scope in @('User', 'Machine')) {", "    try {", "        $val = [Environment]::GetEnvironmentVariable($NodeCaEnvVar, $scope)", "        if (-not $val) {", "            Write-Skip \"$NodeCaEnvVar not set ($scope)\"", "        } elseif ($val -match $OwnershipMatch) {", "            Invoke-Mutation \"clear $NodeCaEnvVar ($scope) -- was '$val'\" {", "                [Environment]::SetEnvironmentVariable($NodeCaEnvVar, $null, $scope)", "            } | Out-Null", "        } else {", "            Write-Warn2 \"$NodeCaEnvVar ($scope) is '$val' -- not a Velatir path, leaving it\"", "        }", "    } catch { Write-Warn2 \"$NodeCaEnvVar ($scope) cleanup failed -- $($_.Exception.Message)\" }", "}", "", "# Other profiles' copies (HKEY_USERS\\<SID>\\Environment) -- [Environment] only ever", "# reaches the current user, so edit the loaded hives directly.", "foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {", "    $envKey = \"Registry::$($hive.Name)\\Environment\"", "    if (-not (Test-Path $envKey)) { continue }", "    try {", "        $val = (Get-ItemProperty -Path $envKey -Name $NodeCaEnvVar -ErrorAction Stop).$NodeCaEnvVar", "        if (-not $val) {", "            continue  # present but empty -- nothing to attribute or clear", "        } elseif ($val -match $OwnershipMatch) {", "            Invoke-Mutation \"remove $NodeCaEnvVar from $envKey -- was '$val'\" {", "                Remove-ItemProperty -Path $envKey -Name $NodeCaEnvVar -Force -ErrorAction Stop", "            } | Out-Null", "        } else {", "            Write-Warn2 \"$NodeCaEnvVar in $envKey is '$val' -- not a Velatir path, leaving it\"", "        }", "    } catch { }  # value absent on this hive -- nothing to do", "}", "", "# \u2500\u2500 10. Flush DNS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section '10. Flush DNS resolver cache'", "Invoke-Native -File 'ipconfig.exe' -Arguments @('/flushdns') | Out-Null", "", "# \u2500\u2500 Verification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "Write-Section 'Verification'", "", "Check-Gone \"routes via $TunGateway\"      { -not (Get-NetRoute -ErrorAction SilentlyContinue | Where-Object { $_.NextHop -eq $TunGateway }) }", "Check-Gone \"adapter '$AdapterName'\"      { -not (Get-NetAdapter -Name $AdapterName -IncludeHidden -ErrorAction SilentlyContinue) }", "Check-Gone \"service '$ServiceName'\"      { -not (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) }", "Check-Gone \"scheduled task '$ScheduledTask'\" { -not (Get-ScheduledTask -TaskName $ScheduledTask -ErrorAction SilentlyContinue) }", "Check-Gone \"CA cert (LocalMachine\\Root)\" { -not (Get-ChildItem 'Cert:\\LocalMachine\\Root' -ErrorAction SilentlyContinue | Where-Object { $_.Subject -match $CertMatch -or $_.FriendlyName -match $CertMatch }) }", "Check-Gone \"install dir\"                 { -not (Test-Path -LiteralPath $InstallDir) }", "Check-Gone \"programdata dir\"             { -not (Test-Path -LiteralPath $ProgramDataDir) }", "Check-Gone \"HKLM\\SOFTWARE\\Velatir\"       { -not (Test-Path 'HKLM:\\SOFTWARE\\Velatir') }", "# Clean when absent OR owned by another product (we deliberately leave those).", "Check-Gone \"WinDivert driver service\"    { -not (Test-Path $WinDivertRegPath) -or ((Get-WinDivertImagePath) -notmatch $OwnershipMatch) }", "Check-Gone \"toast AUMID key\"             { -not (Test-Path \"HKCU:\\$AumidSubKey\") }", "Check-Gone \"$NodeCaEnvVar (User)\"        { $v = [Environment]::GetEnvironmentVariable($NodeCaEnvVar, 'User'); (-not $v) -or ($v -notmatch $OwnershipMatch) }", "", "Complete-Run"];
  const buildScript = () => "\uFEFF" + scriptLines.join("\r\n") + "\r\n";
  const handleDownload = () => {
    const blob = new Blob([buildScript()], {
      type: "text/plain;charset=utf-8"
    });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  };
  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(scriptLines.join("\n"));
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (e) {}
  };
  const buttonBase = {
    padding: "9px 18px",
    borderRadius: "8px",
    fontWeight: 600,
    fontSize: "14px",
    cursor: "pointer"
  };
  return <div style={{
    border: "1px solid rgba(128,128,128,0.3)",
    borderRadius: "12px",
    padding: "20px",
    margin: "16px 0"
  }}>
      <div style={{
    display: "flex",
    gap: "10px",
    flexWrap: "wrap"
  }}>
        <button onClick={handleDownload} style={{
    ...buttonBase,
    border: "none",
    background: "#F74F4F",
    color: "#fff"
  }}>
          Download Uninstall-VelatirDesktopApp.ps1
        </button>
        <button onClick={handleCopy} style={{
    ...buttonBase,
    border: "1px solid #F74F4F",
    background: "transparent",
    color: "#F74F4F"
  }}>
          {copied ? "Copied" : "Copy script"}
        </button>
      </div>
    </div>;
};

A standard uninstall removes Velatir. This page is for when it does not — the device will not come off through your MDM, the uninstall was interrupted, or items remain once it has finished. The cleanup script below removes everything Velatir put on a Windows device, and is safe to run alongside or instead of `msiexec /x`.

Start with the standard route in [Download and install](/desktop-app/download-and-install#updates-and-uninstall). Come here if it does not finish the job.

<Warning>
  A full run removes Velatir completely — including a version you may want to keep. If the device is already running a current version and you only want to clear out what an older one left behind, use [`-LegacyOnly`](#already-running-a-newer-version).
</Warning>

## When you need it

| Symptom                                                      | What is happening                                                                                  |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| The app is still listed after uninstalling                   | The uninstall was interrupted before it finished                                                   |
| Your MDM reports the uninstall as failed, or nothing changes | The MDM never ran the removal, so nothing was removed                                              |
| The device lost internet after a forced removal              | A network route survived the process that owned it                                                 |
| An older version leaves items behind                         | Older versions used a different capture method, with artefacts a standard uninstall does not clear |

## Get the script

<UninstallScriptDownload />

## Run it on one device

Preview first — this changes nothing:

```powershell theme={null}
powershell -ExecutionPolicy Bypass -File .\Uninstall-VelatirDesktopApp.ps1 -DryRun
```

Then run it for real:

```powershell theme={null}
powershell -ExecutionPolicy Bypass -File .\Uninstall-VelatirDesktopApp.ps1
```

It asks for administrator rights if you start it without them. Every step is independent, so one failure never stops the rest, and you can run it again as many times as you need.

Restart the device afterwards to finish clearing a removed network adapter. Internet access returns without a restart — the script repairs routes first, before the slower work, so a device that lost connectivity is usable again immediately.

## Deploy it with Intune

<Steps>
  <Step title="Change the app assignment first">
    Set the Velatir app assignment to **Uninstall**, or remove it. While it is still assigned as **Required**, Intune reinstalls the app behind the cleanup and the device looks unchanged.
  </Step>

  <Step title="Add it as a remediation script">
    Go to **Devices → Scripts and remediations**, add the script, and set it to run as **SYSTEM** in **64-bit** PowerShell. A remediation script needs no detection rule, so a detection rule that never matches cannot block the removal.

    ```powershell theme={null}
    powershell -ExecutionPolicy Bypass -NoProfile -File .\Uninstall-VelatirDesktopApp.ps1 -NoSelfElevate
    ```
  </Step>

  <Step title="Act on the exit code">
    `0` means the device is clean. `2` means something remains — restart those devices and run it again. The device also writes a full transcript to `%TEMP%\Velatir-uninstall-<timestamp>.log`.
  </Step>
</Steps>

The same approach works for any MDM or RMM tool that can run PowerShell as SYSTEM.

<Note>
  Removing Velatir is not an upgrade path. If you want a device on a current version, uninstall and then install again — see [Download and install](/desktop-app/download-and-install).
</Note>

## Already running a newer version

If a device was updated to a current version **before** you ran the cleanup, an older version's leftovers are stranded underneath a working install. Add `-LegacyOnly`:

```powershell theme={null}
powershell -ExecutionPolicy Bypass -File .\Uninstall-VelatirDesktopApp.ps1 -LegacyOnly
```

This removes only what a current version cannot own, and checks each item is genuinely orphaned before touching it. It never runs `msiexec`, and never removes the service, the program directories, the registry keys, or the certificate your current install is using.

## What it removes

* The `VelatirAgent` service, the logon task older versions used, and any running Velatir processes
* Network routes and the `Velatir` network adapter, plus the packet-capture driver older versions installed
* Velatir firewall rules
* Velatir certificates from the trusted root stores
* `C:\Program Files\Velatir`, `C:\ProgramData\Velatir`, every user's `%LOCALAPPDATA%\Velatir`, and the Start menu shortcut
* Velatir registry keys, its entry in the machine `PATH`, and the `NODE_EXTRA_CA_CERTS` environment variable it set

## What it leaves alone

The script only ever removes things it can confirm are Velatir's:

| Left in place                                     | Why                                                                                                                                                              |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Other VPN adapters                                | It matches the adapter named `Velatir` only, so WireGuard, Tailscale and similar keep theirs. The shared `wintun` driver other products rely on stays installed. |
| A packet-capture driver another product installed | Several products ship the same driver. It is removed only when it points at a Velatir directory, and reported and left alone otherwise.                          |
| Your own certificate authority                    | If you [bring your own CA](/desktop-app/bring-your-own-ca), it is not touched.                                                                                   |
| Your own `NODE_EXTRA_CA_CERTS`                    | Cleared only when the value points at a Velatir path.                                                                                                            |
| Your network configuration                        | No IP-stack reset. Only Velatir's own routes are deleted, so your real default route survives.                                                                   |

## Confirm the device is clean

The script prints a verification report and exits `2` if anything remains. To check by hand:

```powershell theme={null}
# No Velatir certificate in the trusted root store - expect no results
Get-ChildItem Cert:\LocalMachine\Root, Cert:\CurrentUser\Root |
  Where-Object Subject -like '*Velatir*'

# No service, no files - expect no results
Get-Service VelatirAgent -ErrorAction SilentlyContinue
Test-Path 'C:\Program Files\Velatir', 'C:\ProgramData\Velatir'
```

<Note>
  On macOS with Firefox, the Velatir browser extension may still be listed after uninstalling. It is inactive, and Firefox does not remove policy-installed extensions by itself. See [Download and install](/desktop-app/download-and-install#updates-and-uninstall) for how to remove it.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Download and install" icon="download" href="/desktop-app/download-and-install">
    Install a current version on a cleaned device.
  </Card>

  <Card title="Enterprise deployment" icon="building" href="/desktop-app/enterprise-deployment">
    Roll out across your fleet with Intune, Jamf, or any MDM.
  </Card>

  <Card title="Troubleshooting" icon="life-buoy" href="/desktop-app/troubleshooting">
    Resolve install and first-run issues.
  </Card>

  <Card title="FAQ" icon="circle-question" href="/desktop-app/faq">
    Common questions about the desktop app.
  </Card>
</CardGroup>
