With the Windows Package Manager (winget), it is easy to install software from the command line. This makes your installs scriptable and thus repeatable. This article is the WinGet counterpart to my previous Notes on Chocolatey: it shows how to reproduce a machine's installation instructions and update your packages regularly.
Install WinGet
WinGet is supported on Windows 11, Windows 10 version 1809 (build 17763) and later, and Windows Server 2025. On Windows desktop, it is delivered as part of App Installer through the Microsoft Store. On a normal, up-to-date Windows 10 or Windows 11 installation, winget is usually already available. Check it from PowerShell:
winget --version
winget source updateIf winget is missing or broken, update App Installer from the Microsoft Store. The PowerShell repair command can also install the latest WinGet client:
Repair-WinGetPackageManager -Latest -ForceListing installation instructions
Let's generate installation instructions from the applications on a machine. WinGet exports the packages it recognizes to JSON, which we can turn into individual install commands:
& {
$exportPath = Join-Path $env:TEMP "winget-packages-$([guid]::NewGuid()).json"
try {
$exportOutput = & winget export --output $exportPath --disable-interactivity 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "WinGet export failed (exit code ${exitCode}):`n$($exportOutput -join [Environment]::NewLine)"
}
$export = Get-Content -LiteralPath $exportPath -Raw |
ConvertFrom-Json -ErrorAction Stop
foreach ($source in ($export.Sources | Sort-Object { $_.SourceDetails.Name })) {
$sourceName = $source.SourceDetails.Name.Replace("'", "''")
foreach ($package in ($source.Packages | Sort-Object PackageIdentifier)) {
$packageId = $package.PackageIdentifier.Replace("'", "''")
'winget install --id ''{0}'' --exact --source ''{1}''' -f $packageId, $sourceName
}
}
}
finally {
Remove-Item -LiteralPath $exportPath -Force -ErrorAction SilentlyContinue
}
}
This prints commands you can copy into an installation script; it does not install anything. Each command includes the package's source, so Microsoft Store packages keep --source 'msstore'.
The generated packages.json file is useful when setting up a new machine too. Instead of running the individual commands, import it with:
winget import --import-file packages.jsonUnlike Chocolatey, WinGet uses package IDs from its own sources. Search for an application and inspect its exact ID before putting it in a script:
winget search git --source winget
winget show --id Git.Git --exact --source wingetUpgrade Task
This creates a weekly Windows Task Scheduler task. When it runs every Monday at 9:30 AM, it requests elevation once and runs all package upgrades in an elevated PowerShell session:
& {
$taskName = 'UpgradeWingetPackages'
$scriptPath = Join-Path $env:LOCALAPPDATA 'Upgrade-WingetPackages.ps1'
$upgradeScript = @'
winget source update
if ($LASTEXITCODE -ne 0) {
throw "WinGet source update failed (exit code $LASTEXITCODE)."
}
winget upgrade --all `
--silent `
--disable-interactivity `
--accept-source-agreements `
--accept-package-agreements
if ($LASTEXITCODE -ne 0) {
throw "WinGet upgrade failed (exit code $LASTEXITCODE)."
}
'@
Set-Content -LiteralPath $scriptPath -Value $upgradeScript -Encoding UTF8
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`""
$trigger = New-ScheduledTaskTrigger `
-Weekly `
-DaysOfWeek Monday `
-At '9:30AM'
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable
$principal = New-ScheduledTaskPrincipal `
-UserId ([Security.Principal.WindowsIdentity]::GetCurrent().Name) `
-LogonType Interactive `
-RunLevel Limited
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($existingTask) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
}
Register-ScheduledTask `
-TaskName $taskName `
-Description 'Upgrade all WinGet packages' `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Principal $principal
}
The task runs only while you are logged in so Windows can show the UAC prompt. It elevates the full WinGet process, so use it only with package sources and applications you trust. The task replaces an existing task with the same name. Some installers may still require input or a reboot.
Make Upgrade Task Compatible with MakeMeAdmin
At Wehkamp we use Make Me Admin. You need to claim admin rights before running administrative tasks. The regular UAC prompt does not let you claim those rights first.
This script updates the task action to launch Make Me Admin, wait for you to request temporary admin rights, and then start the elevated WinGet upgrade:
& {
$taskName = 'UpgradeWingetPackages'
$makeMeAdminDir = Join-Path $env:ProgramFiles 'Make Me Admin'
$makeMeAdminPath = Join-Path $makeMeAdminDir 'MakeMeAdminUI.exe'
$scriptPath = Join-Path $env:LOCALAPPDATA 'Upgrade-WingetPackages.ps1'
if (-not (Test-Path -LiteralPath $makeMeAdminPath)) {
throw "Make Me Admin was not found at '$makeMeAdminPath'."
}
if (-not (Test-Path -LiteralPath $scriptPath)) {
throw "The WinGet upgrade script was not found at '$scriptPath'. Run upgrade-task.ps1 first."
}
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction Stop
$quotedMakeMeAdminDir = $makeMeAdminDir.Replace("'", "''")
$quotedScriptPath = $scriptPath.Replace("'", "''")
$launcher = @"
Set-Location -LiteralPath '$quotedMakeMeAdminDir'
& (Join-Path '$quotedMakeMeAdminDir' 'MakeMeAdminUI.exe')
Read-Host 'Request temporary administrator access, then press Enter to continue'
`$arguments = '-NoProfile -ExecutionPolicy Bypass -File `"$quotedScriptPath`"'
`$process = Start-Process -FilePath 'powershell.exe' -ArgumentList `$arguments -Verb RunAs -Wait -PassThru
exit `$process.ExitCode
"@
$encodedLauncher = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($launcher))
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument "-NoProfile -EncodedCommand $encodedLauncher"
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
Register-ScheduledTask `
-TaskName $taskName `
-Description 'Upgrade all WinGet packages with Make Me Admin' `
-Action $action `
-Trigger $task.Triggers `
-Settings $task.Settings `
-Principal $task.Principal
}
The next time the task runs, claim temporary admin rights in Make Me Admin before continuing.
Skip an upgrade
WinGet supports pins for packages that should not be upgraded automatically. For example:
winget pin add --id Git.Git
winget pin list
winget pin remove --id Git.GitA package with a non-blocking pin can still be included explicitly when upgrading everything:
winget upgrade --all --include-pinnedMy arbitrary list of installed packages
I use my articles as notes to my future self. When I get a new machine, I usually install the following packages. These are the WinGet IDs corresponding to the tools from my Chocolatey article:
& {
# development tools
winget install --id Amazon.AWSCLI --exact
winget install --id Amazon.Kiro --exact
## Node Version Manager for Windows
winget install --id CoreyButler.NVMforWindows --exact
winget install --id Git.Git --exact
winget install --id GitHub.cli --exact
winget install --id GnuPG.Gpg4win --exact
winget install --id Google.ContainerTools.Skaffold --exact
winget install --id jqlang.jq --exact
winget install --id Microsoft.VisualStudioCode --exact
winget install --id MikeFarah.yq --exact
winget install --id ScooterSoftware.BeyondCompare.5 --exact
winget install --id SST.opencode --exact
winget install --id SUSE.RancherDesktop --exact
winget install --id wagoodman.dive --exact
winget install --id xhcoding.sshpass-win32 --exact
# image and video tools
winget install --id dotPDN.PaintDotNet --exact
winget install --id Gyan.FFmpeg --exact
winget install --id Inkscape.Inkscape --exact
## draw.io
winget install --id JGraph.Draw --exact
winget install --id NickeManarin.ScreenToGif --exact
winget install --id VideoLAN.VLC --exact
winget install --id yt-dlp.FFmpeg --exact
winget install --id yt-dlp.yt-dlp --exact
# tools
winget install --id Brave.Brave --exact
winget install --id CodeSector.TeraCopy --exact
winget install --id JAMSoftware.TreeSize.Free --exact
winget install --id Logitech.Options --exact
winget install --id Microsoft.Coreutils --exact
winget install --id Microsoft.PowerShell --exact
winget install --id Microsoft.PowerToys --exact
winget install --id RavensburgerAG.tiptoiManager --exact
## OpenSSL command-line toolkit
winget install --id ShiningLight.OpenSSL.Light --exact
winget install --id SparkLabs.Viscosity --exact
winget install --id StablyAI.Orca --exact
# Microsoft Store apps
## Dynamic Theme: daily Bing and Windows Spotlight wallpapers
winget install --id 9NBLGGH1ZBKW --exact --source msstore
## WhatsApp
winget install --id 9NKSQGP7F2NH --exact --source msstore
## ChatGPT
winget install --id 9PLM9XGG6VKS --exact --source msstore
## Spotify
winget install --id 9NCBCSZSJRSB --exact --source msstore
}
Beyond Compare has separate WinGet packages for versions 4 and 5. I used version 5 above. Package availability can change; use winget search to check before installing.
You can install the packages without repeating every command by saving the list as a JSON export and importing it later. For a script, however, explicit IDs make the intended software clear and --exact avoids an ambiguous search result.
Post installation
After installing the tools, you might want to install a version of Node.js and some global dependencies.
These are my go-to global dependencies:
& {
# download and use Node.js 24 LTS
nvm install 24
nvm use 24
}
& {
# check if your package file is still up to date
npm install --global npm-check-updates
}
Enjoy!
The code is on GitHub, so check it out: 18.winget.