DEV Community

Scott Williams
Scott Williams

Posted on

Audit Windows startup entries with PowerShell without breaking anything: read-only first, -WhatIf, a JSON backup and an undo

AI disclosure: this post was written with AI assistance. The code was syntax-checked and linted, and its logic was tested with simulated registry objects, but it has not yet been run on Windows: not against a real registry, and not on Windows PowerShell 5.1. Read it before you run it, and try it with -WhatIf first.

Settings > Apps > Startup and Task Manager's Startup apps tab are the right first stop when a Windows PC is slow after sign-in. Use them first. But sometimes something still starts at sign-in and you can't see where it comes from, or Settings shows an entry called "We couldn't find this app" left behind by something you uninstalled.

Microsoft documents where those entries live: the Run registry keys and the two Startup folders. This post builds three small PowerShell functions around them:

  1. Get-StartupEntry: a read-only list. Changes nothing.
  2. Disable-RunEntry: removes one Run value, but only after saving it to a JSON file, and it supports -WhatIf and -Confirm.
  3. Restore-RunEntry: puts the value back from that JSON file.

The pattern (report first, preview with -WhatIf, back up, then change, and keep an undo) is worth copying for any script that touches the registry.

Where startup entries live

From Microsoft's startup apps page and the Run/RunOnce documentation:

Location Scope
HKCU\Software\Microsoft\Windows\CurrentVersion\Run Current user
HKLM\Software\Microsoft\Windows\CurrentVersion\Run All users
HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run All users, 32-bit apps on 64-bit Windows
...\RunOnce (HKCU and HKLM) Runs once at the next sign-in, then the value is deleted
shell:startup = %AppData%\Microsoft\Windows\Start Menu\Programs\Startup Current user's Startup folder
shell:common startup = %ProgramData%\Microsoft\Windows\Start Menu\Programs\Startup All users' Startup folder

This is not the complete list of ways a program can start (scheduled tasks and services are others), but it covers what Settings' startup list points you to.

Step 1: a read-only audit

$runKeys = @(
    'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
    'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce'
    'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run'
    'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce'
    'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'
)

function Get-StartupEntry {
    [CmdletBinding()]
    param()

    foreach ($key in $runKeys) {
        if (-not (Test-Path -LiteralPath $key)) { continue }
        $item = Get-Item -LiteralPath $key
        foreach ($name in $item.GetValueNames()) {
            if ($name -eq '') { continue }   # skip the (Default) value
            [pscustomobject]@{
                Source   = 'Registry'
                Location = $key
                Name     = $name
                # Read the raw text, so %ProgramFiles% etc. are not expanded
                Command  = $item.GetValue($name, $null, 'DoNotExpandEnvironmentNames')
            }
        }
    }

    $folders = [Environment]::GetFolderPath('Startup'),
               [Environment]::GetFolderPath('CommonStartup')
    foreach ($folder in $folders) {
        if (-not $folder -or -not (Test-Path -LiteralPath $folder)) { continue }
        Get-ChildItem -LiteralPath $folder -File |
            Where-Object Name -ne 'desktop.ini' |
            ForEach-Object {
                [pscustomobject]@{
                    Source   = 'StartupFolder'
                    Location = $folder
                    Name     = $_.Name
                    Command  = $_.FullName
                }
            }
    }
}
Enter fullscreen mode Exit fullscreen mode

Run it in a normal (non-admin) PowerShell window. Reading HKLM doesn't need admin rights:

Get-StartupEntry | Format-Table -AutoSize
Enter fullscreen mode Exit fullscreen mode

A few details that matter:

  • GetValue(..., 'DoNotExpandEnvironmentNames') returns the command exactly as stored, so %LOCALAPPDATA%\... stays as written. You want the raw text in a backup, otherwise restoring it changes the value's meaning.
  • [Environment]::GetFolderPath('Startup') and 'CommonStartup' resolve the two Startup folders without hard-coding paths.
  • This list doesn't show the On/Off state you see in Settings or Task Manager. Cross-check there before you remove anything. If an entry can be turned off with the toggle, use the toggle. It's simpler and you can undo it with one click.

Windows also exposes a combined view through WMI, which is handy as a second opinion:

Get-CimInstance -ClassName Win32_StartupCommand |
    Select-Object Name, Command, Location, User
Enter fullscreen mode Exit fullscreen mode

Step 2: remove one Run value, safely

Only do this for an entry you understand: typically a leftover that points to a program you have already uninstalled. Don't remove security software, and look up anything you don't recognise first. Microsoft warns that editing the registry can have unintended consequences and says to back it up first. This function makes the backup for you, for the one value it changes.

function Disable-RunEntry {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory)] [string] $Key,
        [Parameter(Mandatory)] [string] $Name,
        [string] $BackupDir = (Join-Path $env:LOCALAPPDATA 'StartupBackups')
    )

    $item = Get-Item -LiteralPath $Key -ErrorAction Stop
    if ($item.GetValueNames() -notcontains $Name) {
        throw "No value named '$Name' under $Key"
    }

    # Capture everything needed to put it back, before touching anything.
    $backup = [pscustomobject]@{
        Key     = $Key
        Name    = $Name
        Value   = $item.GetValue($Name, $null, 'DoNotExpandEnvironmentNames')
        Kind    = $item.GetValueKind($Name).ToString()
        SavedAt = (Get-Date).ToString('o')
    }

    if ($PSCmdlet.ShouldProcess("$Key\$Name", 'Remove startup value (backup is saved first)')) {
        New-Item -ItemType Directory -Path $BackupDir -Force -ErrorAction Stop -Confirm:$false | Out-Null
        $safeName = $Name -replace '[^\w.-]', '_'
        $file = Join-Path $BackupDir ('{0}_{1}.json' -f $safeName, (Get-Date -Format 'yyyyMMdd-HHmmss'))
        $backup | ConvertTo-Json | Set-Content -LiteralPath $file -Encoding UTF8 -ErrorAction Stop -Confirm:$false

        Remove-ItemProperty -LiteralPath $Key -Name $Name -ErrorAction Stop -Confirm:$false
        # Output the backup path so you can pass it straight to Restore-RunEntry
        [pscustomobject]@{ Removed = "$Key\$Name"; BackupFile = $file }
    }
}
Enter fullscreen mode Exit fullscreen mode

Three things make this safe to run:

  • SupportsShouldProcess gives the function -WhatIf and -Confirm for free. Nothing inside the if ($PSCmdlet.ShouldProcess(...)) block runs during -WhatIf.
  • The backup is written before the delete. New-Item and Set-Content use -ErrorAction Stop, so if the backup folder or file can't be written, the function stops and Remove-ItemProperty never runs. Without -ErrorAction Stop this wouldn't hold: most write failures (access denied, disk full) are non-terminating errors, and the next line would still run.
  • The value kind is saved too (String vs ExpandString), so the restore writes back exactly the same type.

The inner -Confirm:$false calls stop you being asked three times when you run with -Confirm. You confirm once, at the ShouldProcess prompt.

Preview first, then run it for real:

Disable-RunEntry -Key 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'SomeOldUpdater' -WhatIf
Disable-RunEntry -Key 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'SomeOldUpdater'
Enter fullscreen mode Exit fullscreen mode

The second command prints the path of the backup file. Entries under HKLM: need an administrator PowerShell window.

Step 3: undo

function Restore-RunEntry {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param([Parameter(Mandatory)] [string] $BackupFile)

    $b = Get-Content -LiteralPath $BackupFile -Raw -ErrorAction Stop | ConvertFrom-Json
    if ($PSCmdlet.ShouldProcess("$($b.Key)\$($b.Name)", 'Restore startup value')) {
        New-ItemProperty -LiteralPath $b.Key -Name $b.Name -Value $b.Value `
            -PropertyType $b.Kind -Force -ErrorAction Stop -Confirm:$false | Out-Null
        [pscustomobject]@{ Restored = "$($b.Key)\$($b.Name)"; Value = $b.Value }
    }
}
Enter fullscreen mode Exit fullscreen mode
Restore-RunEntry -BackupFile "$env:LOCALAPPDATA\StartupBackups\SomeOldUpdater_20261029-101500.json" -WhatIf
Restore-RunEntry -BackupFile "$env:LOCALAPPDATA\StartupBackups\SomeOldUpdater_20261029-101500.json"
Enter fullscreen mode Exit fullscreen mode

What about the Startup folders?

Don't delete shortcuts there. Move them to a backup folder instead, and move them back to undo:

$backup = Join-Path $env:LOCALAPPDATA 'StartupBackups\StartupFolder'
New-Item -ItemType Directory -Path $backup -Force | Out-Null
Move-Item -LiteralPath (Join-Path ([Environment]::GetFolderPath('Startup')) 'SomeApp.lnk') -Destination $backup -WhatIf
Enter fullscreen mode Exit fullscreen mode

Remove -WhatIf when the preview looks right. The all-users folder needs admin rights.

Limits, honestly

  • The code was tested with PowerShell 7 on Linux, using fake registry objects. It parsed cleanly, PSScriptAnalyzer reported nothing, and Pester tests of the backup/remove/restore logic passed (including a simulated failed backup write, which stops the removal). It has not been run against a real Windows registry or on Windows PowerShell 5.1. Try it with -WhatIf first, and make a restore point if you are nervous.
  • It only covers the Run/RunOnce keys and Startup folders, not scheduled tasks, services or per-app settings.
  • It removes one value at a time on purpose. Bulk "cleaners" are how people end up with a missing touchpad or audio driver panel after a restart.

Sources (Microsoft)

This article was created with the help of AI. Not affiliated with or endorsed by Microsoft.

Top comments (0)