| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- <#
- .SYNOPSIS
- Disable (or re-enable) Windows startup entries via the StartupApproved
- registry mechanism — no admin required, fully reversible.
- .DESCRIPTION
- Equivalent of Task Manager's "Disable" button: writes a 12-byte binary
- flag to HKCU\...\Explorer\StartupApproved\{Run,Run32,StartupFolder}
- so the entry is skipped at next logon. Works on HKLM entries from a
- non-admin context (overlay applies per-user only).
- For an entry to be disable-able by this script it must exist in one of:
- - HKCU/HKLM\...\CurrentVersion\Run (64-bit)
- - HKLM\...\WOW6432Node\Microsoft\...\Run (32-bit)
- - Startup folders (user + all-users)
- Services and scheduled tasks are NOT touched by this script — those
- need Set-Service / Disable-ScheduledTask respectively.
- .PARAMETER Name
- The Run-key value name to disable. Multiple names accepted (positional
- or via pipeline).
- .PARAMETER Enable
- Re-enable instead of disable (flips status byte 0x03 -> 0x02).
- .PARAMETER List
- List current state of all StartupApproved entries and exit. Ignores -Name.
- .PARAMETER Json
- Emit machine-readable JSON of the action taken.
- .EXAMPLE
- scripts/safe-disable-startup.ps1 -Name 'Adobe Creative Cloud'
- Disable a single entry by exact value name.
- .EXAMPLE
- scripts/safe-disable-startup.ps1 -Name 'Granola','MuseHub','CometUpdaterTask*'
- Disable multiple entries; wildcards expand against actual Run-key entries.
- .EXAMPLE
- scripts/safe-disable-startup.ps1 -List
- Show current enabled/disabled state of every known startup entry.
- .EXAMPLE
- scripts/safe-disable-startup.ps1 -Name 'Adobe Creative Cloud' -Enable
- Re-enable a previously-disabled entry.
- .NOTES
- Exit codes:
- 0 success
- 2 usage (no names given and not -List)
- 3 not found (no matching Run-key entry for the given name)
- 4 validation error
- #>
- [CmdletBinding(SupportsShouldProcess)]
- param(
- [Parameter(ValueFromPipeline, Position=0)][string[]]$Name,
- [switch]$Enable,
- [switch]$List,
- [switch]$Json
- )
- $ErrorActionPreference = 'Stop'
- . "$PSScriptRoot\_lib\common.ps1"
- . (Join-Path $PSScriptRoot '..\..\_lib\term.ps1')
- Initialize-Term
- # Map: registry path -> StartupApproved variant for the overlay
- $pathVariantMap = @(
- @{ Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'; Variant = 'Run' }
- @{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'; Variant = 'Run' }
- @{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'; Variant = 'Run32' }
- )
- function Get-RunEntries {
- $entries = @()
- foreach ($m in $pathVariantMap) {
- if (Test-Path $m.Path) {
- (Get-ItemProperty $m.Path -ErrorAction SilentlyContinue).PSObject.Properties |
- Where-Object { $_.Name -notmatch '^PS' } |
- ForEach-Object {
- $entries += [PSCustomObject]@{
- Name = $_.Name
- Command = $_.Value
- Path = $m.Path
- Variant = $m.Variant
- }
- }
- }
- }
- # Startup folder shortcuts use a separate StartupApproved variant
- foreach ($d in @("$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup",
- "$env:ALLUSERSPROFILE\Microsoft\Windows\Start Menu\Programs\StartUp")) {
- if (Test-Path $d) {
- Get-ChildItem $d -Filter *.lnk -ErrorAction SilentlyContinue | ForEach-Object {
- $entries += [PSCustomObject]@{
- Name = $_.Name # full filename, e.g. "Comet.lnk"
- Command = $_.FullName
- Path = $d
- Variant = 'StartupFolder'
- }
- }
- }
- }
- return $entries
- }
- function Get-CurrentState {
- param(
- [Parameter(Mandatory)][string]$EntryName,
- [Parameter(Mandatory)][ValidateSet('Run','Run32','StartupFolder')][string]$Variant
- )
- $key = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\$Variant"
- if (-not (Test-Path $key)) { return 'unmanaged' }
- $val = (Get-ItemProperty $key -Name $EntryName -ErrorAction SilentlyContinue).$EntryName
- if (-not $val) { return 'unmanaged' } # No overlay = uses default (enabled)
- if ($val[0] -eq 0x03) { return 'disabled' }
- elseif ($val[0] -eq 0x02) { return 'enabled' }
- else { return "unknown(0x{0:X2})" -f $val[0] }
- }
- # ─────────────────────────────────────────────────────────────────────
- # Mode: List
- # ─────────────────────────────────────────────────────────────────────
- if ($List) {
- $allEntries = Get-RunEntries
- $rows = foreach ($e in $allEntries) {
- $state = Get-CurrentState -EntryName $e.Name -Variant $e.Variant
- [PSCustomObject]@{
- Name = $e.Name
- State = $state
- Variant = $e.Variant
- Source = (Split-Path $e.Path -Leaf) + '\' + (Split-Path $e.Path -Parent | Split-Path -Leaf)
- Command = $e.Command -replace '"',''
- }
- }
- if ($Json) {
- foreach ($r in $rows) {
- [Console]::Out.WriteLine(($r | ConvertTo-Json -Compress))
- }
- exit $script:EXIT_OK
- }
- # Group by state for the panel
- $enabled = $rows | Where-Object { $_.State -eq 'enabled' -or $_.State -eq 'unmanaged' }
- $disabled = $rows | Where-Object { $_.State -eq 'disabled' }
- $unknown = $rows | Where-Object { $_.State -ne 'enabled' -and $_.State -ne 'unmanaged' -and $_.State -ne 'disabled' }
- Write-TermLine (New-TermPanelOpen -Brand 'windows-ops' -Name 'windows-ops' -Subtitle 'safe-disable-startup' -Indicator "$($rows.Count) entries")
- Write-TermLine (New-TermPanelVert)
- $summary = "$($enabled.Count) active · $($disabled.Count) disabled"
- if ($unknown.Count -gt 0) { $summary += " · $($unknown.Count) unknown" }
- Write-TermLine (New-TermSummary -Text $summary)
- Write-TermLine (New-TermPanelVert)
- if ($enabled) {
- Write-TermLine (New-TermSection -State 'PASS' -Label 'active' -Count $enabled.Count)
- $last = $enabled[-1]
- foreach ($e in $enabled) {
- $variant = switch ($e.Variant) { 'Run' { 'HKCU/HKLM' } 'Run32' { 'WOW64' } 'StartupFolder' { 'startup folder' } default { $e.Variant } }
- Write-TermLine (New-TermLeaf -Name $e.Name -Meta $variant -IsLast:($e -eq $last))
- }
- Write-TermLine (New-TermPanelVert)
- }
- if ($disabled) {
- Write-TermLine (New-TermSection -State 'WARN' -Label 'disabled' -Count $disabled.Count)
- $last = $disabled[-1]
- foreach ($e in $disabled) {
- $variant = switch ($e.Variant) { 'Run' { 'HKCU/HKLM' } 'Run32' { 'WOW64' } 'StartupFolder' { 'startup folder' } default { $e.Variant } }
- Write-TermLine (New-TermLeaf -Name $e.Name -Meta $variant -IsLast:($e -eq $last))
- }
- Write-TermLine (New-TermPanelVert)
- }
- if ($unknown) {
- Write-TermLine (New-TermSection -State 'FAILING' -Label 'unknown state' -Count $unknown.Count)
- $last = $unknown[-1]
- foreach ($e in $unknown) {
- $variant = switch ($e.Variant) { 'Run' { 'HKCU/HKLM' } 'Run32' { 'WOW64' } 'StartupFolder' { 'startup folder' } default { $e.Variant } }
- Write-TermLine (New-TermLeaf -Name $e.Name -Meta $variant -Age $e.State -IsLast:($e -eq $last))
- Write-TermLine (New-TermAlert -Severity warning -Text 'partial/corrupt StartupApproved entry — verify with Task Manager')
- }
- Write-TermLine (New-TermPanelVert)
- }
- $hk = @(
- (New-TermHotkey -Key 'E' -Verb 'enable')
- (New-TermHotkey -Key 'D' -Verb 'disable')
- (New-TermHotkey -Key '?' -Verb 'help')
- ) | Join-TermHotkeys
- $hl = @(
- (New-TermHealth -State 'healthy' -Text "$($enabled.Count) active")
- ) | Join-TermHealths
- Write-TermLine (New-TermPanelClose -Hotkeys $hk -Healths $hl)
- exit $script:EXIT_OK
- }
- # ─────────────────────────────────────────────────────────────────────
- # Mode: Disable/Enable
- # ─────────────────────────────────────────────────────────────────────
- if (-not $Name) {
- Write-Log -Level ERROR -Message "Must provide -Name or -List. See -? for help."
- exit $script:EXIT_USAGE
- }
- $statusByte = if ($Enable) { [byte]0x02 } else { [byte]0x03 }
- $action = if ($Enable) { 'enable' } else { 'disable' }
- $valueBytes = ConvertTo-Bytes12 -StatusByte $statusByte
- $allEntries = Get-RunEntries
- $matched = @()
- foreach ($pattern in $Name) {
- $hits = $allEntries | Where-Object { $_.Name -like $pattern }
- if (-not $hits) {
- Write-Log -Level WARN -Message "No Run-key entries match pattern: $pattern"
- continue
- }
- foreach ($e in $hits) {
- if ($PSCmdlet.ShouldProcess("$($e.Name) (Variant=$($e.Variant))", "$action via StartupApproved\$($e.Variant)")) {
- try {
- $key = Get-StartupApprovedKey -Variant $e.Variant
- Set-ItemProperty -Path $key -Name $e.Name -Value $valueBytes -Type Binary -Force
- $matched += $e
- $verified = Get-CurrentState -EntryName $e.Name -Variant $e.Variant
- Write-Log -Level PASS -Message "${action}d: $($e.Name) [$($e.Variant)] -> verified state: $verified"
- if ($Json) {
- [Console]::Out.WriteLine((@{
- action = $action
- name = $e.Name
- variant = $e.Variant
- verified = $verified
- } | ConvertTo-Json -Compress))
- }
- } catch {
- Write-Log -Level FAIL -Message "Failed to $action $($e.Name): $_"
- }
- }
- }
- }
- if (-not $matched) {
- Write-Log -Level ERROR -Message "No matching entries acted on."
- exit $script:EXIT_NOT_FOUND
- }
- if (-not $Json -and -not $Quiet) {
- [Console]::Error.WriteLine("")
- [Console]::Error.WriteLine("$($matched.Count) entr$(if ($matched.Count -eq 1) {'y'} else {'ies'}) ${action}d. Effect applies at next user logon.")
- [Console]::Error.WriteLine("Re-run with -List to verify.")
- }
- exit $script:EXIT_OK
|