remove-hidden-devices.ps1

<#PSScriptInfo

.VERSION 1.1.1

.GUID b7ca4884-b23d-4029-826b-34f4b5fc496b

.AUTHOR vadyaravadim

.COMPANYNAME

.COPYRIGHT

.TAGS Windows Windows10 Windows11 DeviceManager GhostDevices HiddenDevices Cleanup PnP Drivers Tweak

.LICENSEURI https://github.com/vadyaravadim/remove-hidden-devices/blob/main/LICENSE

.PROJECTURI https://github.com/vadyaravadim/remove-hidden-devices

.ICONURI

.EXTERNALMODULEDEPENDENCIES

.REQUIREDSCRIPTS

.EXTERNALSCRIPTDEPENDENCIES

.RELEASENOTES

.PRIVATEDATA

#>


<#
.SYNOPSIS
    Removes hidden / ghost devices (non-present hardware) from Device Manager on Windows 10/11.
.DESCRIPTION
    Enumerates devices whose driver is still registered but that are not
    currently present - the ghost entries left behind by unplugged USB sticks,
    headsets, dongles and old GPUs - and removes them so Device Manager reflects
    only hardware that is actually connected. Self-elevates via UAC. Zero
    external dependencies.
.NOTES
    Requirements: Windows 10/11. The script requests administrator rights on
    its own (UAC prompt).
.LINK
    https://github.com/vadyaravadim/remove-hidden-devices
#>


# Launched via `irm <url> | iex` - no file on disk. Save the script to the
# user profile and rerun it from there (the rerun handles elevation).
if (-not $PSCommandPath) {
    # The piped text is not recoverable from inside iex ($MyInvocation there
    # holds the caller's command line, not the script body) - download the
    # script.
    try {
        $body = Invoke-RestMethod 'https://raw.githubusercontent.com/vadyaravadim/remove-hidden-devices/main/remove-hidden-devices.ps1' -TimeoutSec 30
    } catch {
        Write-Host "ERROR: could not download the script ($($_.Exception.Message)). Check your internet connection, or save the script to a file and run it from there." -ForegroundColor Red
        return
    }
    $saved = Join-Path $env:USERPROFILE 'remove-hidden-devices.ps1'
    if ((Test-Path $saved) -and ([IO.File]::ReadAllText($saved) -cne $body)) {
        Copy-Item $saved "$saved.bak" -Force
        Write-Host "Existing $saved differs - previous copy kept as $saved.bak" -ForegroundColor Yellow
    }
    # UTF8Encoding($false) = no BOM: a BOM would break a later `irm | iex` of
    # the saved copy and violates the ASCII/no-BOM invariant the repo enforces.
    [IO.File]::WriteAllText($saved, $body, [Text.UTF8Encoding]::new($false))
    Write-Host "Script saved to: $saved" -ForegroundColor Cyan
    powershell -NoProfile -ExecutionPolicy Bypass -File $saved
    # The rerun's exit code stays in $LASTEXITCODE for scripted callers.
    return
}

Write-Host "==================================="
Write-Host "REMOVE UNKNOWN DEVICES"
Write-Host "==================================="
Write-Host ""

# Self-elevate via UAC when not running as Administrator
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    Write-Host "Not running as Administrator. Requesting elevation..."
    try {
        Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @(
            '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"")
    } catch {
        # Not always a refusal (UAC service disabled, ...) - show the real cause.
        Write-Host "ERROR: elevation failed ($($_.Exception.Message)). Run this script as Administrator."
        pause
    }
    exit
}

try {
    # Get unknown devices
    Write-Host "Scanning for unknown devices..."
    $UnknownDevices = Get-PnpDevice | Where-Object { $_.Status -eq 'Unknown' }
    
    if ($UnknownDevices.Count -eq 0) {
        Write-Host ""
        Write-Host "No unknown devices found"
        Write-Host ""
        pause
        exit
    }
    
    Write-Host ""
    Write-Host "Found $($UnknownDevices.Count) unknown device(s):"
    Write-Host ""
    
    $UnknownDevices | ForEach-Object { 
        Write-Host " -> $($_.FriendlyName)"
    }
    
    Write-Host ""
    Write-Host "==================================="
    $Confirm = Read-Host "Remove these devices? (Y/N)"
    
    if ($Confirm -ne 'Y' -and $Confirm -ne 'y') {
        Write-Host ""
        Write-Host "Operation cancelled"
        Write-Host ""
        pause
        exit
    }
    
    Write-Host ""
    Write-Host "Removing devices..."
    
    foreach ($Device in $UnknownDevices) {
        pnputil /remove-device "$($Device.InstanceId)"
    }
    
    Write-Host ""
    Write-Host "==================================="
    Write-Host "REMOVE UNKNOWN DEVICES COMPLETED"
    Write-Host "==================================="
    Write-Host ""
    Write-Host "For full registry changes to take effect,"
    Write-Host "a system restart is recommended."
    Write-Host ""

    $reboot = Read-Host "Restart computer now? (y/n)"
    if ($reboot -eq "y" -or $reboot -eq "Y") {
        Write-Host ""
        Write-Host "Restarting in 10 seconds..."
        Write-Host "Press Ctrl+C to cancel"
        
        for ($i = 10; $i -gt 0; $i--) {
            Write-Host "Restarting in $i seconds..." -NoNewline
            Start-Sleep -Seconds 1
            Write-Host "`r" -NoNewline
        }
        
        Write-Host ""
        Write-Host "Restarting..." -ForegroundColor Green
        Restart-Computer -Force
    } else {
        Write-Host ""
        Write-Host "Restart cancelled." -ForegroundColor Yellow
        Write-Host "Don't forget to restart your computer later"
        Write-Host "for full registry changes to take effect!"
    }
    
} catch {
    Write-Host ""
    Write-Host "ERROR: $($_.Exception.Message)"
    Write-Host ""
}

pause