VOOZH about

URL: https://dev.to/mediaexpres/building-a-zero-bloat-winget-background-auto-updater-with-powershell-33b4

⇱ Building a Zero-Bloat WinGet Background Auto-Updater with PowerShell - DEV Community


Before diving into the code, let me share exactly why I wrote this script and why you might find it helpful. For a long time, I tested commercial utilities like IObit Software Updater and CCleaner Software Updater. While they technically handle updates, they come with a high cost to system performance. Their underlying executables introduce unnecessary background telemetry, aggressive pop-ups for premium upgrades, and persistent processes that consume system resources.

Moving from a consumer attitude towards the computer system to a developer's perspective, I felt I needed the workstations to be lean and optimized. I realized that installing a bloated, closed-source program to manage software updates is counterproductive. By writing a custom PowerShell script that leverages native WinGet commands, we eliminate the bloat. Here is my transparent, open-source automation tool that runs silently in the background, executing exactly what we need and nothing more.

Keeping Windows applications up to date is a standard requirement for any developer's workstation. While Microsoft provides the excellent Windows Package Manager (WinGet), it currently lacks a native, silent background auto-updater.

If you search for solutions, you will likely find tools like Winget-AutoUpdate (WAU). While incredibly powerful for enterprise IT departments, it is heavily bloated for a single developer's laptop. I wanted a lightweight, "set-it-and-forget-it" solution.

Working alongside Google Gemini and Anthropic's Claude as AI pair-programming partners, I iteratively built and security-hardened a robust, zero-bloat PowerShell automation script. Here is a breakdown of the development journey, the technical hurdles we solved, and the final code.

The Technical Hurdles

We initially wrote a script that triggered at system startup using the hidden NT AUTHORITY\SYSTEM account. However, we quickly ran into several Windows architecture quirks that required refactoring.

1. The User Context Bug
Because WinGet is installed on a per-user basis (living in AppData), the SYSTEM account literally could not find the winget executable, throwing a CommandNotFoundException.

  • The Fix: We refactored the Scheduled Task principal to dynamically grab the interactive user's profile and run with highest administrative privileges.

2. Preventing Log Bloat
Since this script runs daily and logs its output silently, the text file would eventually grow massive.

  • The Fix: We implemented an automatic log-trimming function. Before running the update, PowerShell checks if the log exceeds 2 MB. If it does, it uses the highly efficient -Tail 500 parameter to keep only the most recent history.

The Enterprise Security Upgrade

Because the Scheduled Task runs invisibly (-WindowStyle Hidden) with highest privileges (-RunLevel Highest), it introduces a risk: if malicious software overwrote the background payload, the system would silently execute the virus every time you log in. Collaborating on a deep-dive threat model with Claude, we implemented enterprise-grade security hardening to mitigate this:

  • Race Condition Prevention & Forced Reset: The setup script forces an icacls /reset and recursively clears any existing automation directory on launch to ensure a clean deployment. It handles the directory creation and completely locks down the C:\Automation directory before it writes the payload, ensuring no malware can swap the file during creation.
  • Cryptographic Hash Pinning: We don't just rely on folder permissions. The setup script calculates the SHA-256 hash of the payload and pins it to the Windows Registry (HKLM). The Scheduled Task verifies this exact hash before execution; if a single byte has been altered, it aborts.
  • Path Hijacking Defense: We replaced bare commands with absolute, secure paths to both winget.exe and PowerShell.exe to prevent execution spoofing.

The Final Code

Here is the complete, bulletproof script. Paste this into an Administrator PowerShell window, and it will automatically clear old instances, generate the payload, secure the folder, pin the hash, and register the Scheduled Task.

<#
.SYNOPSIS
 Hardened setup for a scheduled WinGet auto-updater task.
#>$ErrorActionPreference='Stop'$Folder="C:\Automation"$ScriptPath="$Folder\BackgroundUpdater.ps1"$LogPath="$Folder\updater_log.txt"$TaskName="Automated_WinGet_Updater"$RegKeyPath="HKLM:\SOFTWARE\WinGetAutomation"$RegHashName="ExpectedScriptHash"functionWrite-Section{param([string]$Message,[string]$Color='Gray')Write-Host$Message-ForegroundColor$Color}try{# -------------------------------------------------------------------# 0. Confirm elevation and capture the real interactive user identity# -------------------------------------------------------------------$identity=[System.Security.Principal.WindowsIdentity]::GetCurrent()$principal=New-ObjectSystem.Security.Principal.WindowsPrincipal($identity)if(-not$principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)){throw"This script must be run from an elevated (Administrator) PowerShell session."}$CurrentUser=$identity.NameWrite-Section"Running elevated as $CurrentUser."'Gray'# -------------------------------------------------------------------# 1. Force clear any existing automation folder to ensure a fresh slate# -------------------------------------------------------------------if(Test-Path$Folder){$existing=Get-Item$Folder-Forceif($existing.Attributes-band[IO.FileAttributes]::ReparsePoint){throw"$Folder is a reparse point / junction / symlink. Refusing to reuse it."}Write-Section"Existing deployment detected. Resetting ACLs and force-deleting $Folder..."'Yellow'&icacls$Folder/reset/T/C/Q|Out-NullRemove-Item-Path$Folder-Recurse-Force}New-Item-ItemTypeDirectory-Path$Folder|Out-Null# Lock it down immediately$icaclsArgs=@($Folder,'/inheritance:r','/grant',"*S-1-5-32-544:(OI)(CI)F",'/grant',"*S-1-5-18:(OI)(CI)F",'/grant',"${CurrentUser}:(OI)(CI)RX",'/T','/C','/Q')$icaclsOutput=&icacls@icaclsArgs2>&1if($LASTEXITCODE-ne0){throw"icacls failed to lock down $Folder (exit code $LASTEXITCODE): $icaclsOutput"}# Verify ACL using proper SID Translation$acl=Get-Acl$Folderif(-not$acl.AreAccessRulesProtected){throw"$Folder still inherits parent permissions after icacls /inheritance:r - aborting."}$hasAdminFull=$acl.Access|Where-Object{$sid=$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value$sid-eq'S-1-5-32-544'-and$_.FileSystemRights-match'FullControl'}if(-not$hasAdminFull){throw"Expected Administrators FullControl ACE not found on $Folder after lockdown - aborting."}Write-Section"Folder $Folder created and locked down (verified) before any payload was written."'Green'# -------------------------------------------------------------------# 2. Write the updater payload# -------------------------------------------------------------------$UpdaterCode=@"
`$ErrorActionPreference = 'Stop'
`$LogPath = "$LogPath"

if ((Test-Path `$LogPath) -and ((Get-Item `$LogPath).Length -gt 2097152)) {
 (Get-Content `$LogPath -Tail 500) | Set-Content `$LogPath -Encoding UTF8
}

try {
 Start-Transcript -Path `$LogPath -Append | Out-Null
 Write-Host "Starting background WinGet update sequence: `$(Get-Date -Format o)"

 # Securely resolve WinGet path to prevent PATH hijacking
 `$WinGetPath = "`$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe"

 if (-not (Test-Path `$WinGetPath)) { throw "winget.exe could not be found at secure path." }

 & `$WinGetPath upgrade --all --include-unknown --silent --accept-package-agreements --accept-source-agreements
 `$wingetExit = `$LASTEXITCODE
 if (`$wingetExit -eq 0) {
 Write-Host "Sequence completed successfully (exit code 0)."
 } else {
 Write-Host "WARNING: winget exited with code `$wingetExit."
 }
}
catch {
 Write-Host "ERROR: `$_"
}
finally {
 Stop-Transcript | Out-Null
}
"@Set-Content-Path$ScriptPath-Value$UpdaterCode-EncodingUTF8-ForceWrite-Section"Payload written to $ScriptPath."'Gray'# -------------------------------------------------------------------# 3. Pin an integrity hash in HKLM# -------------------------------------------------------------------$expectedHash=(Get-FileHash-Path$ScriptPath-AlgorithmSHA256).Hashif(-not(Test-Path$RegKeyPath)){New-Item-Path$RegKeyPath-Force|Out-Null}Set-ItemProperty-Path$RegKeyPath-Name$RegHashName-Value$expectedHash-TypeStringWrite-Section"Integrity hash pinned at $RegKeyPath\$RegHashName."'Gray'# -------------------------------------------------------------------# 4. Build a scheduled task Action that verifies the payload's hash# -------------------------------------------------------------------$VerifyAndRun=@'
$ErrorActionPreference = 'Stop'
$scriptPath = 'REPLACE_SCRIPT_PATH'
$regKeyPath = 'REPLACE_REG_KEY'
$regHashName = 'REPLACE_REG_NAME'
try {
 $expected = (Get-ItemProperty -Path $regKeyPath -Name $regHashName -ErrorAction Stop).$regHashName
 $actual = (Get-FileHash -Path $scriptPath -Algorithm SHA256 -ErrorAction Stop).Hash
 if ($expected -ne $actual) {
 throw "Hash Mismatch! Aborting execution."
 }
 & $scriptPath
}
catch {
 throw "Verification Failed."
}
'@$VerifyAndRun=$VerifyAndRun.Replace('REPLACE_SCRIPT_PATH',$ScriptPath).Replace('REPLACE_REG_KEY',$RegKeyPath).Replace('REPLACE_REG_NAME',$RegHashName)$EncodedCommand=[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($VerifyAndRun))$PowerShellExe="$env:windir\System32\WindowsPowerShell\v1.0\powershell.exe"# -------------------------------------------------------------------# 5. Register the scheduled task# -------------------------------------------------------------------$Trigger=New-ScheduledTaskTrigger-AtLogOn$Trigger.Delay="PT15M"$Action=New-ScheduledTaskAction-Execute$PowerShellExe`
-Argument"-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy RemoteSigned -EncodedCommand $EncodedCommand"$Principal=New-ScheduledTaskPrincipal-UserId$CurrentUser-LogonTypeInteractive-RunLevelHighest$Settings=New-ScheduledTaskSettingsSet-CompatibilityWin8$existingTask=Get-ScheduledTask-TaskName$TaskName-ErrorActionSilentlyContinueif($existingTask){Write-Section"An existing task named '$TaskName' was found and will be replaced."'Yellow'}Register-ScheduledTask-TaskName$TaskName-Trigger$Trigger-Action$Action-Principal$Principal-Settings$Settings-Force|Out-Null# -------------------------------------------------------------------# 6. Final verification pass# -------------------------------------------------------------------$verifyTask=Get-ScheduledTask-TaskName$TaskName-ErrorActionSilentlyContinue$verifyAcl=Get-Acl$Folder$verifyHash=(Get-FileHash-Path$ScriptPath-AlgorithmSHA256).Hash$regHash=(Get-ItemProperty-Path$RegKeyPath-Name$RegHashName).$RegHashName$allGood=$trueif(-not$verifyTask){Write-Section"FAILED: scheduled task not found after registration."'Red'$allGood=$false}if($verifyHash-ne$regHash){Write-Section"FAILED: pinned hash does not match the file currently on disk."'Red'$allGood=$false}if($allGood){Write-Host""Write-Host"Success: '$ScriptPath' created with auto-trimming logs and a hash-verifying launcher,"-ForegroundColorGreenWrite-Host" triggered 15 minutes after logon as task '$TaskName'."-ForegroundColorGreenWrite-Host"Hardening applied: ACL locked before write, integrity hash pinned in HKLM,"-ForegroundColorCyanWrite-Host" winget resolved by absolute path."-ForegroundColorCyan}else{throw"One or more post-install verification checks failed. See above."}}catch{Write-Host""Write-Host"Setup failed: $_"-ForegroundColorRed}

Get the Code & Documentation

You can check out the full repository, including the README.md documentation, on GitHub here:
👉 MediaExpres/windows-automation


What's your approach?

How do you currently handle keeping your development environment up to date? Do you rely on third-party tools, or have you built your own custom scripts? Let me know in the comments below!

(🤖 Acknowledgment: The code and documentation in this project were developed iteratively with Google Gemini and Anthropic's Claude as AI pair-programming partners).