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
-WhatIffirst.
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:
-
Get-StartupEntry: a read-only list. Changes nothing. -
Disable-RunEntry: removes oneRunvalue, but only after saving it to a JSON file, and it supports-WhatIfand-Confirm. -
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
}
}
}
}
Run it in a normal (non-admin) PowerShell window. Reading HKLM doesn't need admin rights:
Get-StartupEntry | Format-Table -AutoSize
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
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 }
}
}
Three things make this safe to run:
-
SupportsShouldProcessgives the function-WhatIfand-Confirmfor free. Nothing inside theif ($PSCmdlet.ShouldProcess(...))block runs during-WhatIf. -
The backup is written before the delete.
New-ItemandSet-Contentuse-ErrorAction Stop, so if the backup folder or file can't be written, the function stops andRemove-ItemPropertynever runs. Without-ErrorAction Stopthis 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 (
StringvsExpandString), 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'
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 }
}
}
Restore-RunEntry -BackupFile "$env:LOCALAPPDATA\StartupBackups\SomeOldUpdater_20261029-101500.json" -WhatIf
Restore-RunEntry -BackupFile "$env:LOCALAPPDATA\StartupBackups\SomeOldUpdater_20261029-101500.json"
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
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
-WhatIffirst, 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)
- Configure startup applications in Windows (Settings, Task Manager impact levels, Startup folders, registry locations, back up the registry first): https://support.microsoft.com/en-us/windows/experience/startup-boot/configure-startup-applications-in-windows
- Run and RunOnce registry keys: https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys
- Win32_StartupCommand class: https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-startupcommand
- Everything you wanted to know about ShouldProcess: https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess
This article was created with the help of AI. Not affiliated with or endorsed by Microsoft.
Top comments (0)