Typing winget install git into a terminal and watching an installer appear is the whole of most people’s experience of the Windows Package Manager. Put the same line into a deployment script and three things change: the query can match more than one package and resolve to none of them, the run stops on prompts nobody is there to answer, and the number it leaves behind in $LASTEXITCODE is a negative integer, not the hex value the reference pages print.
This page is about the second use. It covers how winget decides which package a query means, which flags a run needs before it can be called non-interactive, what winget upgrade --all does and does not include, and how to read a return code so a reporting script does not call a healthy machine broken. The reference pages list every option; what they do not do is show the options interacting.
The reader’s side of it is a folder of small PowerShell scripts that model WinGet’s documented behaviour without installing anything. Nothing on this page installs a package, adds a source or changes a machine’s state, which means it can be followed on a workstation during working hours.
Applies to: Windows 10 version 1809 (build 17763) or later, Windows 11, and Windows Server 2025. Earlier Windows Server releases do not ship the App Installer that carries winget.
What you are about to build, and why
By the end of this page you will have one folder, C:\winget-lab, holding eight small files: six PowerShell scripts, one sample JSON file, and one script that deletes the folder again. None of them call winget except the last wrapper, and that one has a -DryRun switch so you can see the command line it would run before it runs anything.
The point of modelling the behaviour rather than just running the tool is that the interesting parts of winget are decisions it makes before it does any work: which package a query resolved to, whether a prompt was going to appear, and what the return code meant. Those are the parts a script has to get right, and they are the parts you cannot see by watching a progress bar.
| File | What it does |
|---|---|
Test-WingetQuery.ps1 | Applies the documented matching rule to a set of real package identifiers and counts what each spelling of a query matches |
Convert-WingetExitCode.ps1 | Moves between the hex form in the documentation and the signed integer a shell records |
Test-WingetExitCode.ps1 | Sorts the return codes into the four outcomes a deployment script has to treat differently |
Measure-WingetErrorList.ps1 | Reads the published return-code list and checks its own arithmetic |
packages.json | A sample package list in the documented export format |
Read-WingetExport.ps1 | Reports what an export file actually pins and writes a trimmed copy |
Invoke-WingetInstall.ps1 | The wrapper to keep: a non-interactive install with its return code classified |
Remove-WingetLab.ps1 | Lists the folder and removes it |
winget itself is not reproduced anywhere on this page, because it depends on which packages a machine has and on what the community repository held on the day the command ran. Where a winget command line is shown, it is shown without output and the text says what to look for.
Before you start
Five steps, in order. The fifth is a note rather than a command.
1. Confirm WinGet is present and see how it is configured. The first line prints the client version; the second prints the fuller picture, including the App Installer package version, the directories WinGet uses, any Group Policy that has been configured, and the admin settings. Run both in PowerShell.
winget --version
winget --info
Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe. On Windows Server 2025 the App Installer arrives through updates rather than through the Store.
2. Create the lab folder. Everything on this page lives in one folder and the cleanup step at the end removes exactly that folder.
New-Item -ItemType Directory -Path 'C:\winget-lab' -Force | Out-Null
Set-Location -Path 'C:\winget-lab'
3. Allow the scripts to run in this session only. This changes nothing permanently: the setting applies to the current PowerShell process and is gone when you close the window.
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
4. Decide which PowerShell you are in. The scripts here run in both Windows PowerShell 5.1 (powershell.exe) and PowerShell 7 (pwsh.exe), but the version is worth knowing before you start reading error messages. Run this and note what it prints; the value differs per machine, so no output is shown for it here.
$PSVersionTable.PSVersion
(Get-Command pwsh -ErrorAction SilentlyContinue).Source
If the second line prints nothing, PowerShell 7 is not installed or is not on PATH. That is fine for this page. If you want it, upgrading PowerShell 7 on Windows 10 and 11 walks through the four install routes, one of which is winget itself.
5. A note on paths. Every script below refers to its own folder through $PSScriptRoot or to a file by bare name, so nothing has a Windows path baked into it. If you put the folder somewhere other than C:\winget-lab, only the New-Item line above changes.
Quick answer
A winget install that is safe to put in a script names the package by identifier, pins the match, names the source, suppresses the installer UI, accepts both of the two separate agreement prompts, and refuses to ask anything at all:
winget install --id Git.Git --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
Then read the return code, and treat three of its values as success rather than failure, because they mean the machine already has what was asked for:
$code = $LASTEXITCODE
# UPDATE_NOT_APPLICABLE, INSTALL_ALREADY_INSTALLED, INSTALL_DOWNGRADE
if ($code -eq 0 -or $code -in -1978335189, -1978334963, -1978334962) {
"nothing to do or installed cleanly"
} else {
"winget returned {0} (0x{0:X8})" -f $code
}
0x8A15002B, 0x8A15010D and 0x8A15010E. The sections below measure the conversion rather than asking you to trust it, and the wrapper at the end of the page carries the same two lists.
The rest of this page is why each of those flags is there, and what the numbers are.
What winget is, and the commands this page uses
WinGet is the client for the Windows Package Manager service. It ships inside the App Installer package rather than as a standalone executable, which is the root of several of its scripting limits, including the one about the SYSTEM account further down. Microsoft lists eleven installer formats it can drive: EXE with silent flags, ZIP, INNO, NULLSOFT, MSI, WIX, APPX, MSIX, BURN, PORTABLE and FONT.
The commands this page uses:
| Command | What it does here |
|---|---|
winget install | Installs a package. Aliased to add. |
winget upgrade | Upgrades a package, or lists what has an upgrade available when given no arguments. Aliased to update. |
winget list | Lists installed packages, with an upgrade column when one is available. Aliased to ls. |
winget export | Writes a JSON list of installed packages. |
winget import | Installs the packages named in such a JSON file. |
winget show | Prints details about a package, including the installer selected for the current system. |
winget search | Finds packages in the configured sources. The source of the identifiers to paste into a script. |
winget error | Prints the description of a known return code. |
winget --info | Prints versions, directories, Group Policy state and admin settings. |
upgrade and list reference pages document an --include-pinned option that overrides a pin. A reference page for the winget pin command itself is not part of the Learn documentation set: the URL that would match the other command pages returns 404, and so does the matching source file in the documentation repository. This page therefore says what the two pages above say about pins, and does not describe how to create one.
The query is matched against three fields at once
This is the sentence that governs everything a script does with winget install, and it is on the install reference page: “By default, winget performs a case-insensitive substring match against the package name, ID, and moniker.” Wild-card syntax is not supported.
So the positional query is not a package name. It is a substring tested against three different fields of every package in every configured source. Four options change that behaviour:
| Option | Documented effect |
|---|---|
--id | Limits the install to the ID of the application. |
--name | Limits the search to the name of the application. |
--moniker | Limits the search to the moniker listed for the application. |
-e, --exact | “Uses the exact string in the query, including checking for case-sensitivity. It will not use the default behavior of a substring.” |
The two groups are independent, and that is the part worth dwelling on. --id narrows which field is searched. -e changes how the comparison is done. Using one without the other leaves half the ambiguity in place.
The same query under seven spellings
Rather than describe the effect, model it. The script below holds seven real packages with their real identifier, name and moniker, copied out of the en-US manifests in the WinGet community repository, and applies the documented rule to each of seven ways of asking for Git.
winget search git on your own machine gives a number that depends on what the repository held that day, and it cannot show you what would have happened with a different flag. Seven packages is enough to make the rule visible, and the rule is what transfers.
Save this as Test-WingetQuery.ps1 in C:\winget-lab and run it with .\Test-WingetQuery.ps1.
# Test-WingetQuery.ps1
# Applies WinGet's documented matching rule to a fixed set of real package
# identifiers, so the effect of --id, --name, --moniker and -e can be compared
# without touching the live source index.
#
# The rule, from the install command reference: the positional query is a
# case-insensitive substring match against the package name, ID and moniker.
# -e, --exact uses the exact string, including checking for case-sensitivity.
#
# Id / Name / Moniker below are copied from the en-US manifests in the
# microsoft/winget-pkgs repository at the versions named in the Version column.
$fixture = @'
Id,PackageName,Moniker,Version
Git.Git,Git,git,2.51.0
Git.MinGit,MinGit,mingit,2.51.0
GitHub.GitHubDesktop,GitHub Desktop,github-desktop,3.4.13
GitHub.cli,GitHub CLI,gh,2.65.0
TortoiseGit.TortoiseGit,TortoiseGit,tortoisegit,2.17.0.2
Microsoft.VisualStudioCode,Microsoft Visual Studio Code,vscode,1.96.2
Microsoft.PowerToys,PowerToys,powertoys,0.87.1
'@ | ConvertFrom-Csv
function Test-Match {
param(
[Parameter(Mandatory)] $Package,
[Parameter(Mandatory)] [string] $Query,
# 'any' is the positional query: name, ID and moniker together.
[ValidateSet('any','id','name','moniker')] [string] $Field = 'any',
[switch] $Exact
)
$values = switch ($Field) {
'id' { @($Package.Id) }
'name' { @($Package.PackageName) }
'moniker' { @($Package.Moniker) }
default { @($Package.Id, $Package.PackageName, $Package.Moniker) }
}
foreach ($v in $values) {
if ($Exact) {
# -e is exact AND case-sensitive, so compare with the ordinal method
# rather than with PowerShell's -eq, which ignores case.
if ([string]::Equals($v, $Query, 'Ordinal')) { return $true }
}
elseif ($v.ToLowerInvariant().Contains($Query.ToLowerInvariant())) { return $true }
}
return $false
}
$spellings = @(
@{ Label = 'winget install git'; Query = 'git'; Field = 'any'; Exact = $false }
@{ Label = 'winget install --id git'; Query = 'git'; Field = 'id'; Exact = $false }
@{ Label = 'winget install --name git'; Query = 'git'; Field = 'name'; Exact = $false }
@{ Label = 'winget install --moniker git'; Query = 'git'; Field = 'moniker'; Exact = $false }
@{ Label = 'winget install --id Git.Git'; Query = 'Git.Git'; Field = 'id'; Exact = $false }
@{ Label = 'winget install --id Git.Git -e'; Query = 'Git.Git'; Field = 'id'; Exact = $true }
@{ Label = 'winget install --id git.git -e'; Query = 'git.git'; Field = 'id'; Exact = $true }
)
'{0,-32}{1,8}' -f 'Command as typed', 'Matches'
'{0,-32}{1,8}' -f ('-' * 16), '-------'
foreach ($s in $spellings) {
$hit = @($fixture | Where-Object {
Test-Match -Package $_ -Query $s.Query -Field $s.Field -Exact:$s.Exact
})
'{0,-32}{1,8}' -f $s.Label, $hit.Count
}
''
'The bare query git, field by field:'
'{0,-30}{1,6}{2,6}{3,9}' -f 'Package identifier', 'Id', 'Name', 'Moniker'
'{0,-30}{1,6}{2,6}{3,9}' -f ('-' * 18), '--', '----', '-------'
foreach ($p in $fixture) {
$i = if (Test-Match -Package $p -Query 'git' -Field 'id') { 'yes' } else { 'no' }
$n = if (Test-Match -Package $p -Query 'git' -Field 'name') { 'yes' } else { 'no' }
$m = if (Test-Match -Package $p -Query 'git' -Field 'moniker') { 'yes' } else { 'no' }
'{0,-30}{1,6}{2,6}{3,9}' -f $p.Id, $i, $n, $m
}
''
'Packages in the fixture: {0}' -f @($fixture).Count
Run it. You should see two tables: seven command spellings with a match count each, then a per-package breakdown of which of the three fields the bare query hit.
Command as typed Matches
---------------- -------
winget install git 5
winget install --id git 5
winget install --name git 5
winget install --moniker git 4
winget install --id Git.Git 1
winget install --id Git.Git -e 1
winget install --id git.git -e 0
The bare query git, field by field:
Package identifier Id Name Moniker
------------------ -- ---- -------
Git.Git yes yes yes
Git.MinGit yes yes yes
GitHub.GitHubDesktop yes yes yes
GitHub.cli yes yes no
TortoiseGit.TortoiseGit yes yes yes
Microsoft.VisualStudioCode no no no
Microsoft.PowerToys no no no
Packages in the fixture: 7
Three readings of that output matter. The first four spellings are all ambiguous: restricting the search to one field does not help when the string being searched for is a substring of the same field in most of them. --moniker is the only one that drops a package, and it drops GitHub CLI, whose moniker is gh. The install reference page is explicit about what happens next: “If there is any ambiguity, you will be prompted to further filter the install command to an exact application.” A prompt is exactly what a scheduled run cannot answer.
The second reading is the last line of the first table. --id git.git -e matches nothing at all. -e makes the comparison case-sensitive, so the lower-case spelling that worked perfectly well as a substring now matches zero packages, and the run fails for a reason that looks nothing like a typo.
-e to be safe. The flag that was supposed to make the command deterministic is the flag that makes it fail. Take identifiers from winget search or winget list output, where the case is the manifest’s own.
The third reading is the shape of the fix, and the install reference page gives it as the recommended form: “The best way to limit the selection to one file is to use the id of the application combined with the exact query option.” Its own example is winget install --id Git.Git -e, and it adds that when several sources are configured a source must be named too, as winget install --id Git.Git -e --source winget. One exception is documented: identifiers in the msstore source are already unique, so they do not need -e.
PackageName rather than Name. In PowerShell every object already answers to Name in some contexts, and a CSV column of that name makes a property reference ambiguous to read. The manifests themselves use PackageName, so the script does too.
Two agreements, two flags
A silent install is not the same thing as a non-interactive one. --silent suppresses the installer’s user interface. It has nothing to say about the prompts WinGet itself raises before the installer starts, and there are two of those, covering two different things.
| Flag | What it accepts |
|---|---|
--accept-package-agreements | “Accepts any license agreements or EULAs presented by the package installer, suppressing the interactive prompt. This applies to the package’s own license terms only.” It “does not affect optional components or bundled software offered by the installer.” |
--accept-source-agreements | “Accepts the license agreement for the WinGet source (repository), suppressing the interactive prompt.” It is “separate from any package license” and “covers the terms of use for the source itself, such as the winget community repository.” |
The source agreement is the one that catches a new account, because the source reference page is blunt about the consequence of not accepting it: “If a user does not accept the agreements, WinGet will not be able to access the source.” The install reference page states the combination plainly: for a fully non-interactive install, combine --accept-package-agreements with --silent.
There is a third flag worth adding, documented simply as “Disable interactive prompts”:
winget install --id Git.Git --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
--silent is for, and where it is not enough, --override and --custom pass arguments straight to the installer.
Where the installer choice is actually decided
Three options look like they set a property and in fact express a preference that the package’s manifest has to be able to satisfy: --scope, --architecture and --installer-type. A manifest that offers only one installer has nothing to bind a request for another to, and the published list carries a code for exactly that situation: NO_APPLICABLE_INSTALLER, “None of the installers are applicable for the current system”.
# ask for the machine-wide installer rather than the per-user one
winget install --id Git.Git --exact --source winget --scope machine
# ask for a specific architecture and a specific installer technology
winget install --id Microsoft.PowerShell --exact --source winget --architecture x64 --installer-type wix
Scope is the one that behaves differently per package, and the troubleshooting page is unusually direct about it. MSIX-based packages give “reliable WinGet behavior”. MSI-based packages “typically support reliable WinGet configurations” but are sometimes nested inside an EXE installer, which adds variability. For EXE-based installers, behaviour around scope is “not necessarily deterministic”: in some cases the arguments to specify scope are not available at all, and in others the installer decides for itself based on whether the user is in the local administrators group.
winget show --id Git.Git --exact prints the installer WinGet has selected for the current system, and winget show --id Git.Git --exact --versions lists the versions available. Running show before scripting an install is the cheapest step on this page.
Elevation interacts with this. Run without administrator privileges, an installer that needs elevation triggers a UAC prompt, and declining it fails the install. Run from an administrator prompt, no elevation prompt appears at all. If the way a token is handed out is the part that surprises you, how UAC filters an administrator token covers the mechanism rather than the symptom.
The upgrade that skips two kinds of package
The phrase --all invites exactly one reading, and the documented behaviour is narrower than that reading. Two classes of installed package are excluded by default, each with its own opt-in flag.
| Class | Why it is skipped | Flag that includes it |
|---|---|---|
| Packages with no determinable version | Some applications do not provide a version, so WinGet cannot tell whether a newer one exists | -u, --include-unknown |
| Packages with a pin | A pin tells WinGet not to upgrade the package; with the flag set, only non-blocking pins are upgraded | --pinned, --include-pinned |
So the command that genuinely means “upgrade everything you can” is longer than the one most scripts contain, and the shorter form is the reason a patch report can show a clean estate while the packages it never looked at sit unpatched:
# the usual line, which skips unknown versions and pinned packages
winget upgrade --all
# what it takes to include both excluded classes, non-interactively
winget upgrade --all --include-unknown --include-pinned --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
The upgrade reference page also recommends looking before leaping: run winget upgrade with no arguments first to preview what would be upgraded. The list command answers the same question in a form that is easier to filter, and it takes the same two flags:
# what has an upgrade available, including packages with an unknown version
winget list --upgrade-available --include-unknown
--all has its own return code. 0x8A15002C, UPDATE_ALL_HAS_FAILURE, is the one the published list describes as winget upgrade --all having completed with failures. One package among many failing produces that single code, so a script that only tests for zero learns that something went wrong and nothing about what. Upgrading packages one identifier at a time costs more lines and tells you which one failed.
One more option deserves naming because its behaviour is delegated rather than defined: --uninstall-previous. The page explains that the effect depends on the individual package. Some installers place versions side by side; some manifests already specify uninstallPrevious so earlier versions go without the flag; and where the manifest says nothing and the flag is absent, the installer’s own default applies.
One exit code, three spellings
WinGet’s return codes are published as hex HRESULT values: 0x8A150014 and the like. What a shell records in $LASTEXITCODE or %ERRORLEVEL% is a 32-bit signed integer. The same thirty-two bits, two spellings, and a third if a tool reads them as unsigned. The spelling you have is rarely the spelling you are searching for, and that is where a search for the number goes wrong.
Save this as Convert-WingetExitCode.ps1 in C:\winget-lab and run it with .\Convert-WingetExitCode.ps1.
# Convert-WingetExitCode.ps1
# WinGet's published return codes are listed as a hex HRESULT. What a shell
# records is a 32-bit signed integer. These two functions move between the two
# spellings, and the last section shows which conversions refuse to do it.
function ConvertFrom-WingetHex {
# Convert::ToInt32 with base 16 reads the string as a two's complement bit
# pattern, so a value above Int32.MaxValue comes back negative instead of
# throwing. That is exactly the number a shell will have recorded.
param([Parameter(Mandatory)][string] $Hex)
[Convert]::ToInt32(($Hex -replace '^0x', ''), 16)
}
function ConvertTo-WingetHex {
# The format operator reads the Int32 bit pattern, so the sign needs no
# special handling. X8 pads to the eight digits the documentation uses.
param([Parameter(Mandatory)][int] $Code)
'0x{0:X8}' -f $Code
}
$codes = -1978335212, -1978335189, -1978334963, -1978334967, -1978335207, 0
'{0,-14}{1,-12}{2,14}' -f 'Signed', 'Documented', 'Unsigned'
'{0,-14}{1,-12}{2,14}' -f ('-' * 6), ('-' * 10), ('-' * 8)
foreach ($c in $codes) {
$hex = ConvertTo-WingetHex -Code $c
# The unsigned reading is the same 32 bits read as a UInt32 instead.
$unsigned = [Convert]::ToUInt32(($hex -replace '^0x', ''), 16)
'{0,-14}{1,-12}{2,14}' -f $c, $hex, $unsigned
}
''
'Round trip holds for every code above: {0}' -f
(@($codes | Where-Object { (ConvertFrom-WingetHex (ConvertTo-WingetHex $_)) -ne $_ }).Count -eq 0)
''
'Typing the documented hex straight into PowerShell:'
' 0x8A150014 is {0}, typed as {1}' -f 0x8A150014, (0x8A150014).GetType().Name
''
'The three conversions that refuse:'
try { ' [uint32] on the signed value : {0}' -f [uint32] -1978335212 }
catch { ' [uint32] on the signed value : {0}' -f $_.Exception.GetType().Name }
try { ' [int] on the unsigned value : {0}' -f [int] 2316632084 }
catch { ' [int] on the unsigned value : {0}' -f $_.Exception.GetType().Name }
try { ' ToInt32 on the decimal string : {0}' -f [Convert]::ToInt32('2316632084', 10) }
catch { ' ToInt32 on the decimal string : {0}' -f $_.Exception.GetType().Name }
Run it. The first table is five real codes plus zero in all three spellings; then a round-trip check; then what PowerShell does with the documented hex form typed directly; then three conversions that refuse.
Signed Documented Unsigned
------ ---------- --------
-1978335212 0x8A150014 2316632084
-1978335189 0x8A15002B 2316632107
-1978334963 0x8A15010D 2316632333
-1978334967 0x8A150109 2316632329
-1978335207 0x8A150019 2316632089
0 0x00000000 0
Round trip holds for every code above: True
Typing the documented hex straight into PowerShell:
0x8A150014 is -1978335212, typed as Int32
The three conversions that refuse:
[uint32] on the signed value : RuntimeException
[int] on the unsigned value : RuntimeException
ToInt32 on the decimal string : MethodInvocationException
The useful surprise is the middle section. Pasting 0x8A150014 straight into PowerShell gives -1978335212 typed as Int32, with no conversion written at all, because PowerShell reads a hex literal that wide as a bit pattern. That makes a comparison against a documented code a one-liner:
if ($LASTEXITCODE -eq 0x8A15002B) { "nothing to upgrade" }
The three refusals at the bottom of the output are worth knowing because they are the obvious things to try. [uint32] applied to the signed value throws, and so does [int] applied to the unsigned one: PowerShell’s casts are range-checked conversions, not reinterpretations of the same bits. [Convert]::ToInt32 on the decimal string throws for the same reason. The one that works, [Convert]::ToInt32($hex, 16), works because with base 16 that overload reads the string as a two’s complement bit pattern and hands back a negative number instead of complaining.
Sorting the codes into outcomes a script can act on
A deployment script does not need a description for each code. It needs to know which bucket a code falls into, and the awkward part is that several non-zero codes mean the machine is already in the requested state. Treating those as failures is how a healthy estate gets reported as broken.
Save this as Test-WingetExitCode.ps1 in C:\winget-lab and run it with .\Test-WingetExitCode.ps1.
# Test-WingetExitCode.ps1
# Sorts WinGet's return codes into the four outcomes a deployment script has to
# act on differently. The code and symbol columns are copied from the published
# return-code list; the Outcome column is this script's policy, not Microsoft's.
$known = @'
Code|Symbol|Meaning
-1978335216|NO_APPLICABLE_INSTALLER|None of the installers are applicable
-1978335215|INSTALLER_HASH_MISMATCH|Installer hash does not match the manifest
-1978335212|NO_APPLICATIONS_FOUND|No packages found
-1978335210|MULTIPLE_APPLICATIONS_FOUND|Multiple packages matched the criteria
-1978335207|COMMAND_REQUIRES_ADMIN|Command requires administrator privileges
-1978335189|UPDATE_NOT_APPLICABLE|No applicable update found
-1978335188|UPDATE_ALL_HAS_FAILURE|upgrade --all completed with failures
-1978335180|IMPORT_INSTALL_FAILED|One or more imported packages failed
-1978335179|NOT_ALL_PACKAGES_FOUND|One or more requested packages not found
-1978334975|INSTALL_PACKAGE_IN_USE|Application is currently running
-1978334967|INSTALL_REBOOT_REQUIRED_TO_FINISH|Restart to finish installation
-1978334966|INSTALL_REBOOT_REQUIRED_FOR_INSTALL|Install failed, restart then retry
-1978334965|INSTALL_REBOOT_INITIATED|The machine will restart to finish
-1978334963|INSTALL_ALREADY_INSTALLED|Another version is already installed
-1978334962|INSTALL_DOWNGRADE|A higher version is already installed
-1978334961|INSTALL_BLOCKED_BY_POLICY|Organization policies prevent installation
'@ | ConvertFrom-Csv -Delimiter '|'
# Codes that mean the machine already holds what was asked for. A script that
# treats these as failures will report a clean estate as broken.
$nothingToDo = -1978335189, -1978334963, -1978334962
# Codes that put a restart in the picture. They do not all mean the package
# was installed: see the table below the output in the article.
$rebootCodes = -1978334967, -1978334966, -1978334965
function Get-WingetOutcome {
param([Parameter(Mandatory)][int] $Code)
if ($Code -eq 0) { return 'Succeeded' }
if ($nothingToDo -contains $Code) { return 'AlreadyCurrent' }
if ($rebootCodes -contains $Code) { return 'RebootNeeded' }
return 'Failed'
}
'{0,-13}{1,-38}{2}' -f 'Code', 'Symbol', 'Outcome'
'{0,-13}{1,-38}{2}' -f ('-' * 4), ('-' * 6), ('-' * 7)
'{0,-13}{1,-38}{2}' -f 0, 'no error', (Get-WingetOutcome -Code 0)
foreach ($row in $known) {
$code = [int] $row.Code
'{0,-13}{1,-38}{2}' -f $code, $row.Symbol, (Get-WingetOutcome -Code $code)
}
''
$all = @(0) + @($known.Code | ForEach-Object { [int] $_ })
foreach ($outcome in 'Succeeded', 'AlreadyCurrent', 'RebootNeeded', 'Failed') {
$n = @($all | Where-Object { (Get-WingetOutcome -Code $_) -eq $outcome }).Count
'{0,-16}{1,3}' -f $outcome, $n
}
'{0,-16}{1,3}' -f 'codes classified', $all.Count
Run it. You should see sixteen documented codes plus zero, each with its symbol and the outcome this script assigns, then a count per outcome.
Code Symbol Outcome
---- ------ -------
0 no error Succeeded
-1978335216 NO_APPLICABLE_INSTALLER Failed
-1978335215 INSTALLER_HASH_MISMATCH Failed
-1978335212 NO_APPLICATIONS_FOUND Failed
-1978335210 MULTIPLE_APPLICATIONS_FOUND Failed
-1978335207 COMMAND_REQUIRES_ADMIN Failed
-1978335189 UPDATE_NOT_APPLICABLE AlreadyCurrent
-1978335188 UPDATE_ALL_HAS_FAILURE Failed
-1978335180 IMPORT_INSTALL_FAILED Failed
-1978335179 NOT_ALL_PACKAGES_FOUND Failed
-1978334975 INSTALL_PACKAGE_IN_USE Failed
-1978334967 INSTALL_REBOOT_REQUIRED_TO_FINISH RebootNeeded
-1978334966 INSTALL_REBOOT_REQUIRED_FOR_INSTALL RebootNeeded
-1978334965 INSTALL_REBOOT_INITIATED RebootNeeded
-1978334963 INSTALL_ALREADY_INSTALLED AlreadyCurrent
-1978334962 INSTALL_DOWNGRADE AlreadyCurrent
-1978334961 INSTALL_BLOCKED_BY_POLICY Failed
Succeeded 1
AlreadyCurrent 3
RebootNeeded 3
Failed 10
codes classified 17
The codes in the first bucket deserve naming individually, because the reason each is harmless is different. UPDATE_NOT_APPLICABLE means no applicable update was found, which is what a current machine returns. INSTALL_ALREADY_INSTALLED means another version of the application is already installed. INSTALL_DOWNGRADE means a higher version is already installed, which is a state an estate reaches whenever someone tests a preview build.
The three reboot codes do not mean the same thing
Three codes mention a restart and they describe three different situations. Collapsing them into one “reboot needed” bucket, as the script above does, is fine for a report but wrong for a retry loop, because only one of the three is worth retrying after the restart.
| Code | Symbol | Documented description |
|---|---|---|
0x8A150109 | INSTALL_REBOOT_REQUIRED_TO_FINISH | “Restart your PC to finish installation.” |
0x8A15010A | INSTALL_REBOOT_REQUIRED_FOR_INSTALL | “Installation failed. Restart your PC then try again.” |
0x8A15010B | INSTALL_REBOOT_INITIATED | “Your PC will restart to finish installation.” |
Read the second row again. That one says the installation failed, and the restart is a precondition for trying again rather than a finishing touch. The first and third rows describe an install that happened. A retry loop that reruns the install after every code in this group will do unnecessary work on two of the three and the only necessary work on one.
--allow-reboot is documented as “Allows a reboot if applicable”. Without it, WinGet reports that a restart is needed rather than taking one. On a fleet that is almost always the behaviour you want from an unattended run.
Measuring the published code list
The sixteen codes above are the set this page treats as the common ones. The published list is much longer, and it lives in the WinGet client repository rather than on Learn, which is why it is easy to miss. This script reads it, reports its size, and checks that every row’s hex value really is the two’s complement form of the decimal printed next to it.
Save this as Measure-WingetErrorList.ps1 in C:\winget-lab and run it with .\Measure-WingetErrorList.ps1.
# Measure-WingetErrorList.ps1
# Reads the published return-code list straight from the WinGet client
# repository, reports how many codes it holds, and checks that every row's hex
# value really is the two's complement form of the decimal printed beside it.
# Run this when a code turns up that is not in Test-WingetExitCode.ps1.
$url = 'https://raw.githubusercontent.com/microsoft/winget-cli/master/' +
'doc/windows/package-manager/winget/returnCodes.md'
$text = Invoke-RestMethod -Uri $url -UseBasicParsing
$stamp = ([regex]'(?m)^ms\.date:\s*(\S+)').Match($text).Groups[1].Value
$rows = ([regex]'(?m)^\|\s*(0x[0-9A-Fa-f]{8})\s*\|\s*(-?\d+)\s*\|\s*(\S+)\s*\|').Matches($text)
$mismatched = 0
foreach ($r in $rows) {
$fromHex = [Convert]::ToInt32(($r.Groups[1].Value -replace '^0x', ''), 16)
if ($fromHex -ne [int] $r.Groups[2].Value) { $mismatched++ }
}
$cli = @($rows | Where-Object { $_.Groups[3].Value -like 'APPINSTALLER_CLI_*' }).Count
'{0,-38}{1}' -f 'List last updated', $stamp
'{0,-38}{1}' -f 'Return codes listed', $rows.Count
'{0,-38}{1}' -f 'Of those, APPINSTALLER_CLI_*', $cli
'{0,-38}{1}' -f 'Rows where hex and decimal differ', $mismatched
''
'Sections in the list:'
foreach ($m in ([regex]'(?m)^##\s+(.+?)\s*$').Matches($text)) { ' ' + $m.Groups[1].Value }
Run it. You should see four labelled values and then the list’s section headings.
List last updated 05/13/2026
Return codes listed 203
Of those, APPINSTALLER_CLI_* 163
Rows where hex and decimal differ 0
Sections in the list:
General Errors
Install errors.
Check for package installed status
Configuration Errors
Configuration Processor Errors
The 203 codes, of which 163 carry an APPINSTALLER_CLI_ symbol; the remainder belong to the installed-status check and to the configuration processor. Every row’s two spellings agree, which is worth confirming rather than assuming, because the whole method in the previous section rests on it.
ms.date stamp so you can see which revision you read. The output above was captured from the revision stamped 05/13/2026. The arithmetic check is the part that should never change.
For a single code there is a faster route than reading a file. The troubleshooting page documents a winget error command that prints the description of a known code, and it covers WinGet, MSIX and MSI codes:
winget error 1603
0x8A15 range is WinGet’s own. A code outside it came from the installer WinGet launched, and tracing it means finding that vendor’s own documentation.
Exporting a package list, and trimming it
A machine can hand over its own package list, and the file it produces is the closest thing WinGet has to a declarative build. The export reference page describes the workflow in the same order: export, then edit the file, then import it elsewhere.
# every package WinGet can match to a configured source
winget export --output .\my-packages.json
# the same, with each installed version recorded rather than implied
winget export --output .\my-packages.json --include-versions
Two documented details change what that file is worth. The first is --include-versions: without it, a package entry carries no version and import installs whatever the source currently calls latest. The second is that export only lists packages it can match to a source, and warns about the rest, so an estate full of in-house MSI packages produces a shorter file than expected.
The next script needs a file to read, and a real export is a long one, so the file below is a short sample in that format, under a different name so that an export of your own cannot overwrite it. It is trimmed the way the documentation expects you to trim one. Its package identifiers are real, taken from the community repository manifests; its source block holds the real values the winget source reports; the client version is the example value the troubleshooting page uses, and the timestamp is the only invented part.
Save this as packages.json in C:\winget-lab. Nothing runs it; the next script reads it.
{
"$schema" : "https://aka.ms/winget-packages.schema.2.0.json",
"CreationDate" : "2026-10-03T09:14:52.118-00:00",
"Sources" :
[
{
"Packages" :
[
{
"PackageIdentifier" : "Git.Git",
"Version" : "2.51.0"
},
{
"PackageIdentifier" : "GitHub.cli"
},
{
"PackageIdentifier" : "Microsoft.PowerToys",
"Scope" : "machine"
},
{
"PackageIdentifier" : "Microsoft.VisualStudioCode",
"Version" : "1.96.2"
},
{
"PackageIdentifier" : "TortoiseGit.TortoiseGit"
}
],
"SourceDetails" :
{
"Argument" : "https://cdn.winget.microsoft.com/cache",
"Identifier" : "Microsoft.Winget.Source_8wekyb3d8bbwe",
"Name" : "winget",
"Type" : "Microsoft.PreIndexed.Package"
}
}
],
"WinGetVersion" : "1.9.25200"
}
Four fields carry the meaning. Sources groups packages by where they came from, which is why a hand-edited file must keep that wrapper rather than collapsing to a bare list. PackageIdentifier is the only required field per package. Version is optional. Scope is documented in the schema with the values user and machine and a default of user.
Reading the export file from PowerShell
An export file is worth auditing before it becomes an image build, because the thing that makes it non-reproducible is an absence rather than a value: a package with no Version will install at a different release next month. This script reports that, and writes a trimmed copy holding only the packages it is told to keep.
packages.json from its own folder and will fail if that file is not there.
Save this as Read-WingetExport.ps1 in C:\winget-lab and run it with .\Read-WingetExport.ps1.
# Read-WingetExport.ps1
# Reads a winget export file, reports what is actually pinned inside it, and
# writes a trimmed copy holding only the packages named in $keep. Trimming the
# file is the step the export documentation expects you to do by hand.
$in = Join-Path $PSScriptRoot 'packages.json'
$out = Join-Path $PSScriptRoot 'packages-core.json'
$keep = 'Git.Git', 'GitHub.cli', 'Microsoft.VisualStudioCode'
$doc = Get-Content -Path $in -Raw | ConvertFrom-Json
'{0,-30}{1}' -f 'Generated by winget version', $doc.WinGetVersion
'{0,-30}{1}' -f 'Sources in the file', @($doc.Sources).Count
''
foreach ($source in $doc.Sources) {
$pkgs = @($source.Packages)
'{0,-30}{1}' -f 'Source name', $source.SourceDetails.Name
'{0,-30}{1}' -f 'Source type', $source.SourceDetails.Type
'{0,-30}{1}' -f 'Packages listed', $pkgs.Count
# A package without a Version is installed at whatever the source now
# calls latest, so an import run months apart is not reproducible.
'{0,-30}{1}' -f 'With a pinned version', @($pkgs | Where-Object { $_.Version }).Count
'{0,-30}{1}' -f 'With a Scope set', @($pkgs | Where-Object { $_.Scope }).Count
''
'{0,-30}{1,-10}{2}' -f 'Package identifier', 'Version', 'Scope'
'{0,-30}{1,-10}{2}' -f ('-' * 18), ('-' * 7), ('-' * 5)
foreach ($p in $pkgs) {
$v = if ($p.Version) { $p.Version } else { '(latest)' }
# Scope is optional and the schema documents its default as user.
$s = if ($p.Scope) { $p.Scope } else { '(user)' }
'{0,-30}{1,-10}{2}' -f $p.PackageIdentifier, $v, $s
}
}
# Keep the file's shape intact: import reads Sources, not a bare package array.
foreach ($source in $doc.Sources) {
$source.Packages = @($source.Packages | Where-Object { $keep -contains $_.PackageIdentifier })
}
$doc | ConvertTo-Json -Depth 6 | Set-Content -Path $out -Encoding utf8
''
'Wrote packages-core.json with {0} packages' -f @($doc.Sources[0].Packages).Count
Run it. You should see two header values, five labelled values for the single source the file holds, a per-package table, and a line confirming the trimmed file was written.
Generated by winget version 1.9.25200
Sources in the file 1
Source name winget
Source type Microsoft.PreIndexed.Package
Packages listed 5
With a pinned version 2
With a Scope set 1
Package identifier Version Scope
------------------ ------- -----
Git.Git 2.51.0 (user)
GitHub.cli (latest) (user)
Microsoft.PowerToys (latest) machine
Microsoft.VisualStudioCode 1.96.2 (user)
TortoiseGit.TortoiseGit (latest) (user)
Wrote packages-core.json with 3 packages
Five packages listed, two with a version pinned, one with an explicit scope. The three without a version are the entries that make this file a wish rather than a specification, and the table marks them (latest) because that is what import will do with them.
The trimmed file is then what you import, and import has its own three options for the awkward cases:
winget import --import-file .\packages-core.json --accept-package-agreements --accept-source-agreements
| Option | Documented effect |
|---|---|
--ignore-unavailable | Suppresses errors if the app requested is unavailable. |
--ignore-versions | Ignores versions specified in the JSON file and installs the latest available version. |
--no-upgrade | Skips upgrade if an installed version already exists. |
0x8A150034, IMPORT_INSTALL_FAILED, for one or more packages failing to install, and 0x8A150035, NOT_ALL_PACKAGES_FOUND, for one or more that could not be found.
The wrapper script to keep
Everything above collapses into one script: build the flag set once, run the install, and classify the code rather than testing it against zero. The -DryRun switch prints the command line and runs nothing, which is how to check a flag set before it touches a machine.
Save this as Invoke-WingetInstall.ps1 in C:\winget-lab and run it with .\Invoke-WingetInstall.ps1 -PackageIdentifier Git.Git -DryRun. The package identifier is a mandatory parameter, so running the script with no arguments only makes it ask for one.
# Invoke-WingetInstall.ps1
# Installs one or more packages with the flag set a non-interactive run needs,
# and turns WinGet's return code into an outcome a deployment report can use.
# Run it with -DryRun first: that prints the command lines and runs nothing.
param(
[Parameter(Mandatory)][string[]] $PackageIdentifier,
[ValidateSet('user', 'machine')][string] $Scope,
[switch] $DryRun
)
# UPDATE_NOT_APPLICABLE, INSTALL_ALREADY_INSTALLED, INSTALL_DOWNGRADE.
$nothingToDo = -1978335189, -1978334963, -1978334962
# REBOOT_REQUIRED_TO_FINISH, REBOOT_REQUIRED_FOR_INSTALL, REBOOT_INITIATED.
$rebootCodes = -1978334967, -1978334966, -1978334965
function Get-WingetOutcome {
param([Parameter(Mandatory)][int] $Code)
if ($Code -eq 0) { return 'Succeeded' }
if ($nothingToDo -contains $Code) { return 'AlreadyCurrent' }
if ($rebootCodes -contains $Code) { return 'RebootNeeded' }
return 'Failed'
}
if (-not $DryRun) {
'{0,-30}{1,-14}{2,-14}{3}' -f 'Package identifier', 'Exit code', 'As hex', 'Outcome'
}
foreach ($id in $PackageIdentifier) {
$argv = @(
'install'
'--id', $id
# --exact pins the match to this one identifier, case included.
'--exact'
# --source stops a second configured source offering the same name.
'--source', 'winget'
# --silent suppresses the installer's own UI.
'--silent'
# The two agreement flags cover two different prompts: the package's
# licence and the source's terms of use.
'--accept-package-agreements'
'--accept-source-agreements'
# Nothing is watching, so refuse to ask anything at all.
'--disable-interactivity'
)
if ($Scope) { $argv += @('--scope', $Scope) }
if ($DryRun) {
'winget ' + ($argv -join ' ')
continue
}
& winget @argv
$code = $LASTEXITCODE
'{0,-30}{1,-14}{2,-14}{3}' -f $id, $code, ('0x{0:X8}' -f $code),
(Get-WingetOutcome -Code $code)
}
Run it in dry-run mode first, with three identifiers, from inside the lab folder:
.\Invoke-WingetInstall.ps1 -PackageIdentifier Git.Git,GitHub.cli,Microsoft.VisualStudioCode -DryRun
You should see one composed command line per identifier, and nothing else:
winget install --id Git.Git --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
winget install --id GitHub.cli --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
winget install --id Microsoft.VisualStudioCode --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
Drop -DryRun and it installs for real, printing one row per package with the exit code, its hex spelling and the assigned outcome. Add -Scope machine and an extra --scope machine appears on every line. No output is shown for the real run, because what it prints depends on what the machine already has.
Test-WingetExitCode.ps1 rather than shared, so if you change the policy you have to change it in both places; and the script installs each identifier in its own winget invocation on purpose. The install reference page does document passing several identifiers to one command, and WinGet then installs them in sequence, but one invocation returns one code, and separate invocations are what makes a per-package report possible.
winget under the SYSTEM account
This is the limit with no flag to get round it, and it is not a bug. WinGet is delivered inside the App Installer as a packaged application, and a packaged application depends on its package being registered for the user running it. The troubleshooting page states the consequence in one sentence: packages can be registered for any user except NT AUTHORITY\SYSTEM, “so the WinGet CLI is not supported in the system context.”
That rules out the obvious places a deployment script wants to live: a scheduled task configured to run as SYSTEM, and anything else running in that context. The documented alternative is a different client for the same service: “The Microsoft.WinGet.Client PowerShell module can be used in the system context with applications that are installed machine wide.”
# the module is published on the PowerShell Gallery
Install-Module -Name Microsoft.WinGet.Client -Scope AllUsers
Get-Command -Module Microsoft.WinGet.Client
winget.exe is the thing that is failing, the account it runs as is the first thing to check, before the flags and before the exit code. The common reasons a PowerShell scheduled task does not run covers the rest of that checklist, and managing scheduled tasks from the command line shows where the account is set.
The same packaging explains a smaller nuisance. A packaged application is not a program file in the place you would look for one, which is why winget --info prints the directories WinGet uses rather than leaving you to guess them. If you need to know which file a name on PATH actually resolves to, finding out which binary actually runs covers the execution aliases that packaged applications install and the order PATH is searched in.
Where the logs are
When an install fails for a reason the return code does not explain, the log is the next stop. The default location is documented, and winget --info prints it for the machine you are on:
%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir
Two options make it easier to reach. Either one can be appended to any command, and --verbose raises the detail level to include the communication with the sources and the content delivery network:
winget install --id Git.Git --exact --logs
winget install --id Git.Git --exact --verbose-logs
Cleanup is automatic and its limits are documented: log files older than 7 days or exceeding 128 MB in total are removed, and an individual file wraps at 16 MB. The limits are adjustable through the logging.file settings in settings.json, and the default level can be raised there too:
{
"$schema": "https://aka.ms/winget-settings.schema.json",
"logging": {
"level": "verbose"
}
}
winget settings opens it in the default JSON editor. Because it lives in the user profile, a setting that fixes logging for an interactive session does nothing for a service account running the same command.
Hidden gems
A nonzero exit code is often the right answer. UPDATE_NOT_APPLICABLE is what a fully patched machine returns from an upgrade. If a patch dashboard shows failures that disappear when you check by hand, the dashboard is probably testing for zero.
--exact is case-sensitive and that is the whole trap. It is the flag people add in order to be careful, and its documented behaviour can turn a working query into zero matches.
winget show answers the questions install will ask. It prints the installer WinGet has selected for the current system, which is where a --scope or --architecture request either has something to bind to or does not.
A 403 during a download is not necessarily your network. The troubleshooting page documents that an independent software vendor can block WinGet’s user agent string, which has the form winget-cli WindowsPackageManager/{Client Version} DesktopAppInstaller/Microsoft.DesktopAppInstaller {AppInstaller Version}. The page’s own test is whether the installer downloads in a browser while the client fails.
The source reference page lists three sources, not two. Its example output for winget source list shows msstore, winget and winget-font, and the font source is marked explicit, which that page defines as meaning commands must target it with --source instead of picking it up automatically.
winget download gets the installer without running it. It writes to the user’s Downloads folder unless --download-directory says otherwise, and it takes --architecture and --platform filters. For a machine that cannot reach the internet, that is the step that happens somewhere else. Verifying the file you carried across is the natural next step, and comparing a file hash on Windows covers the ways that comparison goes wrong.
Installing from a local manifest is off until an administrator turns it on. winget install --manifest needs the feature enabled with winget settings --enable LocalManifestFiles, which the install reference page describes as a precaution.
Where this matters
Image builds. An export file with no versions in it produces a different image each time it is used, and the difference is invisible until something breaks.
Patch reporting. winget upgrade --all without --include-unknown understates how far behind an estate is, because the packages it skips are skipped silently.
Unattended deployment. A run that stops on a source agreement looks identical in a report to a run that failed, and the fix is one flag that nobody adds until it bites.
Scheduled automation. The WinGet CLI is not supported for a task running as SYSTEM, which is a design limit rather than a configuration mistake, and it is why the PowerShell module exists.
Help desk triage. A ticket that quotes a long negative number is quoting a WinGet return code, and converting it to hex is the step that makes it searchable.
Tips and limitations
- The positional query matches name, ID and moniker as a case-insensitive substring. Naming a field with
--idnarrows the search; only-emakes the comparison exact, and it makes it case-sensitive at the same time. - For a fully non-interactive install, the documented combination is
--accept-package-agreementswith--silent. The source agreement is a separate prompt with a separate flag, and--disable-interactivityrefuses the rest. --scopeis reliable for MSIX packages, usually reliable for MSI, and documented as not necessarily deterministic for EXE installers, where the installer may decide for itself.winget upgrade --allexcludes packages whose version cannot be determined and packages with a pin. Both have an opt-in flag, and with the pin flag set only non-blocking pins are upgraded.- Return codes are published as hex and recorded as signed 32-bit integers. Pasting the documented hex form into PowerShell gives the signed value with no conversion written.
- Three return codes mean the machine already holds what was requested. A script that tests only for zero will report them as failures.
- A reference page for
winget pinis not in the Learn documentation set, although--include-pinnedis documented on bothupgradeandlist. - The WinGet CLI is not supported in the system context. The
Microsoft.WinGet.Clientmodule is the documented route there, for machine-wide applications. - An export only lists packages WinGet can match to a configured source, and warns about the others, so the file is not a complete software inventory.
winget errorexplains WinGet, MSIX and MSI codes. Many EXE installers use non-standard codes that it cannot explain.
Clean up the lab
Nothing on this page installed a package, added or removed a source, changed a setting outside the current PowerShell process, or wrote anywhere except the lab folder. One folder holds all of it, and this removes the folder.
Save this as Remove-WingetLab.ps1 in C:\winget-lab and run it with .\Remove-WingetLab.ps1.
# Remove-WingetLab.ps1
# Lists what the lab folder holds and then removes the folder. Nothing on this
# page installed a package, registered a source or changed a setting outside
# this folder, so deleting the folder undoes all of it.
$lab = $PSScriptRoot
'Files in the lab folder:'
$files = @(Get-ChildItem -Path $lab -File | Sort-Object -Property Name)
foreach ($f in $files) { ' ' + $f.Name }
'{0} files' -f $files.Count
# Step out of the folder first: a shell cannot delete its own current location.
Set-Location -Path (Split-Path -Path $lab -Parent)
Remove-Item -Path $lab -Recurse -Force
'Removed the lab folder: {0}' -f (-not (Test-Path -Path $lab))
Run it. It lists the nine files first, so you can see there is nothing else in there, and then confirms the folder is gone.
Files in the lab folder:
Convert-WingetExitCode.ps1
Invoke-WingetInstall.ps1
Measure-WingetErrorList.ps1
packages-core.json
packages.json
Read-WingetExport.ps1
Remove-WingetLab.ps1
Test-WingetExitCode.ps1
Test-WingetQuery.ps1
9 files
Removed the lab folder: True
Nine files: the eight you saved plus packages-core.json, which Read-WingetExport.ps1 wrote. The execution policy change was scoped to the process, so closing the window undoes that too.
Official documentation
- Use WinGet to install and manage applications: the overview, the supported installer formats and the App Installer registration command
- install command (winget): the matching rule, the full option list and the documented disambiguation idiom
- upgrade command (winget): what
--allexcludes, and what--uninstall-previousdelegates to the package - list command (winget): the filters, including
--upgrade-availableand--include-unknown - export command (winget): the file format and what export cannot match
- import command (winget): the three options for unavailable packages and pinned versions
- source command (winget): the source types, the default sources and what explicit means
- info command (winget): what
winget --inforeports, including Group Policy state - Debugging and troubleshooting issues with the WinGet tool: the log location, the scope caveats, the 403 user-agent note and the system context limit
- Return codes, in the Windows Package Manager Client repository: the full list this page measures
- Package list JSON schema, in the Windows Package Manager Client repository: the fields an export file may carry
Related tools
- Scheduled task builder: assemble the task that will run a wrapper like the one above, with the account and trigger set explicitly
- Hash generator: check an installer you downloaded separately against the hash a manifest records
Related guides
- Upgrading PowerShell 7 on Windows 10 and 11: the four install routes for one package, with the MSI and MSIX trade-offs that
--installer-typeselects between - Finding which binary actually runs on Windows: the execution aliases and the
WindowsAppsfolder a packaged install lands in - Why a PowerShell scheduled task does not run: the account, the working directory and the interpreter, which is where a WinGet task fails first
- Managing scheduled tasks from the command line: creating and inspecting the task that carries a deployment script
- How UAC filters an administrator token: why an install that needs elevation behaves differently in two prompts that look the same
- Comparing a file hash on Windows: the comparison that reports a good file as bad, and the operators that cause it
Five cheat sheets, one PDF
Subnet masks, PowerShell, Linux commands, HTTP status codes and the ESXi command line - one page each, free to keep. Leave an address and it arrives in a minute.