Automating PC Provisioning With winget + PowerShell — Making the Runbook Executable
· Go Komura · winget, PowerShell, Windows, PC Provisioning, Corporate IT, Automation, Operational Improvement, Business Efficiency
Every time a new employee joins, or a PC is replaced, someone sits down with the runbook and configures machines one at a time — in the IT department of a small or medium-sized company, this is still the standard sight. And the problem is not just time. Manual work has no reproducibility, so trouble of the form “only this machine has different settings” catches up with you later. The runbook goes stale because nobody updates it, and when the person responsible changes, the details are lost.
Windows ships with the package manager winget as standard, and installing an application is a one-liner. Go further with WinGet Configuration and you can express applications and settings in a single declarative YAML file. And the areas winget does not cover — printers, network drives, company-standard registry settings, and so on — can be filled in with PowerShell.
This article sets out how to replace a provisioning runbook with an “executable file,” at a level of detail you can actually operate.
1. The Bottom Line First
- Leave application installation to winget.
winget installsupports silent installation as standard.1 - You can extract an existing PC’s configuration with
winget export. But it is limited to packages managed by winget, and settings are not included.2 - If you want to do it declaratively, use WinGet Configuration (
winget configure). It is based on PowerShell DSC and lets you write applications and settings into a single YAML file. It requires Windows 10 1809 or later + winget 1.6 or later.3 - Fill in what winget does not do with PowerShell. Printers, shared drives, the registry, Windows features, local accounts, and so on.
- To drive it from PowerShell there is the
Microsoft.WinGet.Clientmodule. It offers cmdlets such asInstall-WinGetPackage.4 - Execution in the system context needs care. Microsoft still lists it as a future development item, so verifying with the actual execution account is essential.3
- Always ensure idempotency. Provisioning fails partway through, and it must be possible to re-run it any number of times.
- Don’t force in-house applications onto winget — running a silent installer from PowerShell is the realistic option.
2. winget Basics — How to Write an Unattended Install
First, the standard form for installing reliably without any interaction.1
# Specify the ID exactly (-e means exact match, --id specifies the ID)
# --silent : show no UI
# --accept-package-agreements : accept the package's terms of use
# --accept-source-agreements : accept the source's terms of use
# --scope machine : install for all users (supported packages only)
# Put the line-continuation backtick at the end of the line. A comment after it breaks the continuation
winget install --id Google.Chrome -e --silent `
--accept-package-agreements --accept-source-agreements --scope machine
# Look up the ID
winget search "Visual Studio Code"
winget show --id Microsoft.VisualStudioCode
If you forget the --accept-* flags, an unattended run stalls waiting for consent. This is the first thing people trip over when automating provisioning.
--scope machine is not usable with every package; some applications can only be installed per user. In that case, structure things so they run in the user context at first logon.
3. Extracting the Configuration of an Existing PC — export / import
If you already have a fully prepared “standard PC,” you can turn its configuration into a file.2
# From the standard PC, write out a list of installed packages to JSON
winget export --output D:\kitting\apps.json --include-versions
# Restore it on the new PC
winget import --import-file D:\kitting\apps.json `
--accept-package-agreements --accept-source-agreements --ignore-unavailable
It is important to understand the limitations.
| What it can do | What it cannot do |
|---|---|
| Reproduce the list of winget-managed packages | Migrate in-app settings |
Pin versions (--include-versions) |
Applications installed outside winget, or in-house applications |
Skip packages that do not exist (--ignore-unavailable) |
Licence activation, sign-in state |
In other words, winget import is a starting point, not the whole of provisioning. How you fill in the rest is the real subject.
4. Writing It Declaratively — WinGet Configuration
winget configure is a mechanism for declaring in YAML the state you want to end up in and applying it through PowerShell DSC.3 Compared with a procedural script, it has three advantages.
- It does nothing if the desired state is already in place (idempotent)
- Application installation and Windows/application settings can be expressed in one file
- If it fails partway through, you just re-run the same file
# kitting.winget — declare the desired state of a standard machine
properties:
configurationVersion: 0.2.0
resources:
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: chrome
directives:
description: Install Google Chrome
allowPrerelease: true
settings:
id: Google.Chrome
source: winget
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vscode
directives:
description: Install Visual Studio Code
settings:
id: Microsoft.VisualStudioCode
source: winget
- resource: Microsoft.Windows.Developer/DeveloperMode
id: devmode
directives:
description: Enable developer mode (development machines only)
allowPrerelease: true
settings:
Ensure: Present
# Check the contents before applying (shows what will be executed)
winget configure show --file D:\kitting\kitting.winget
# Apply it
winget configure --file D:\kitting\kitting.winget --accept-configuration-agreements
The requirements are Windows 10 version 1809 (build 17763) or later, or Windows 11, plus winget 1.6.2631 or later.3 In an environment where older machines remain, the PowerShell-based configuration in the next section is more dependable.
5. Filling In the Gaps With PowerShell — What winget Doesn’t Do
In real-world provisioning, most of the effort is in fact everything other than installing applications. This is where PowerShell comes in. The key is to write everything in the form “do nothing if it has already been done.”
This script reads kitting.config.json. A complete example of the configuration file is included as kitting.config.json in this article’s sample code (the zip at the end of the article), but here is the structure up front.
{
"folders": [ "C:\\Work", "C:\\KsTools" ],
"registry": [
{ "key": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge",
"name": "HomepageLocation", "value": "https://intra.example.co.jp/", "type": "String" }
],
"userRegistry": [
{ "key": "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced",
"name": "HideFileExt", "value": 0, "type": "DWord" }
],
"windowsFeatures": [ "NetFx3" ],
"wingetPackages": [ { "id": "Google.Chrome", "scope": "machine" } ],
"internalApps": [
{ "displayName": "KsApp Business System Client",
"installer": "\\\\fileserver\\deploy\\KsApp\\KsAppSetup.msi",
"arguments": "/quiet /norestart INSTALLDIR=\"C:\\Program Files\\KsApp\"",
"version": "3.2.0" }
],
"drives": [ { "letter": "S", "path": "\\\\fileserver\\share" } ],
"printers": [ { "name": "\\\\printserver\\MFP-1F", "connection": "\\\\printserver\\MFP-1F" } ]
}
registry and userRegistry are kept separate because they apply to different places. A setting such as Explorer’s “show file extensions” (HideFileExt) reads the per-user HKCU, not an HKLM policy. Writing to HKLM during the administrator phase does not change the display. Put per-user settings like this into userRegistry and apply them in the non-elevated phase described later.
The displayName under internalApps must match exactly the name shown in “Programs and Features”. If you specify version, the check goes as far as confirming that version or later is installed (when omitted, the check is on the name alone).
Application installation itself can also be done with winget import or WinGet Configuration from the previous sections, but this script includes installing wingetPackages too. Keeping the configuration in a single file means the definition of “what goes on this machine” lives in one place and one run does everything. Conversely, if you leave items written in the configuration file unread by the script, you end up recording a machine that does not have them as “provisioning succeeded.”
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string] $ConfigPath = "$PSScriptRoot\kitting.config.json"
)
$ErrorActionPreference = 'Stop'
# Get-Content on 5.1 reads using the ANSI code page when there is no BOM.
# Reading UTF-8 JSON on a Japanese-locale machine garbles it, so state the encoding explicitly
$config = Get-Content $ConfigPath -Raw -Encoding utf8 | ConvertFrom-Json
$rebootRequired = $false
$rebootInitiated = $false
$log = "C:\ProgramData\KsKitting\kitting_$(Get-Date -f yyyyMMdd_HHmmss).log"
$null = New-Item -Path (Split-Path $log) -ItemType Directory -Force
# The per-user phase described later runs in a separate process, so distribute the
# configuration to a location every user can read.
# If you place the distribution package at this location and run it there, the copy source and
# destination are the same file. Copy-Item treats a copy onto itself as an error, so under
# $ErrorActionPreference = 'Stop' the whole script stops before provisioning even starts
$sharedConfig = 'C:\ProgramData\KsKitting\kitting.config.json'
$sourceFull = (Resolve-Path $ConfigPath).ProviderPath
$sharedFull = if (Test-Path $sharedConfig) { (Resolve-Path $sharedConfig).ProviderPath } else { '' }
if (-not [string]::Equals($sourceFull, $sharedFull, 'OrdinalIgnoreCase')) {
Copy-Item $ConfigPath $sharedConfig -Force
}
Start-Transcript -Path $log -Append
try {
# --- 1. Standard folders ----------------------------------------------
foreach ($dir in $config.folders) {
if (-not (Test-Path $dir)) {
$null = New-Item -Path $dir -ItemType Directory
Write-Verbose "Created: $dir"
}
}
# --- 2. Company-standard registry settings ----------------------------
foreach ($reg in $config.registry) {
if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }
$item = Get-Item -Path $reg.key
$exists = $item.GetValueNames() -contains $reg.name
# Compare the "kind" as well as the value. A REG_SZ "1" and a DWORD 1
# compare as equal in PowerShell, which makes a setting that is not actually
# in effect look like it has been "applied"
$sameValue = $exists -and $item.GetValue($reg.name) -eq $reg.value
$sameKind = $exists -and $item.GetValueKind($reg.name).ToString() -eq $reg.type
if (-not ($sameValue -and $sameKind)) {
Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
Write-Verbose "Set: $($reg.key)\$($reg.name) = $($reg.value) ($($reg.type))"
}
}
# --- 3. Windows features ----------------------------------------------
foreach ($feature in $config.windowsFeatures) {
$state = Get-WindowsOptionalFeature -Online -FeatureName $feature
if ($state.State -ne 'Enabled') {
# When you suppress the restart with -NoRestart, whether one is needed comes back
# in RestartNeeded on the return value. If you don't pick it up you end up
# "exiting with 0 even though a restart is required"
$result = Enable-WindowsOptionalFeature -Online -FeatureName $feature -NoRestart
if ($result.RestartNeeded) { $rebootRequired = $true }
}
}
# --- 4. winget packages -----------------------------------------------
# winget's exit code can be non-zero even when the package is "already installed",
# and conversely it can be 0 when the package is not actually there. Judge success by querying state.
# Without --scope you pick up the administrator's own per-user install and
# wrongly conclude it is "installed for the machine"
function Test-KsWingetPackage {
param([string] $Id, [string] $Scope)
$arguments = @('list', '--id', $Id, '--exact', '--accept-source-agreements')
if ($Scope) { $arguments += @('--scope', $Scope) }
$null = winget @arguments 2>&1
return ($LASTEXITCODE -eq 0)
}
foreach ($package in $config.wingetPackages) {
if (Test-KsWingetPackage -Id $package.id -Scope $package.scope) { continue }
# Put the line-continuation backtick at the end of the line. A comment after it breaks the continuation
winget install --id $package.id -e --silent `
--accept-package-agreements --accept-source-agreements `
--scope $package.scope
$wingetExit = $LASTEXITCODE
# If you merely emit a warning here and carry on, the deployment tool records a machine
# missing a required application as "provisioning succeeded"
if (-not (Test-KsWingetPackage -Id $package.id -Scope $package.scope)) {
throw "Failed to install $($package.id) (winget ExitCode=$wingetExit)"
}
}
# --- 5. In-house applications (silently run installers from a shared folder) ----
# Open the installed-applications list with both the 32-bit and 64-bit views stated explicitly.
# If 32-bit PowerShell runs on 64-bit Windows (which can happen depending on the Intune configuration),
# WOW64 redirection makes HKLM:\SOFTWARE\... point at the 32-bit view, so 64-bit applications
# are wrongly judged "not installed" and reinstalled every time
$installedApps = foreach ($view in 'Registry64', 'Registry32') {
$baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', $view)
try {
$uninstall = $baseKey.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
if (-not $uninstall) { continue }
try {
foreach ($name in $uninstall.GetSubKeyNames()) {
$appKey = $uninstall.OpenSubKey($name)
if (-not $appKey) { continue }
try {
$displayName = $appKey.GetValue('DisplayName')
if ($displayName) {
[pscustomobject]@{
DisplayName = [string] $displayName
DisplayVersion = [string] $appKey.GetValue('DisplayVersion')
}
}
}
finally { $appKey.Dispose() }
}
}
finally { $uninstall.Dispose() }
}
finally { $baseKey.Dispose() }
}
foreach ($app in $config.internalApps) {
$installed = $installedApps | Where-Object DisplayName -eq $app.displayName
# Don't judge "installed" from a DisplayName match alone. A machine still carrying an old
# version never gets the new one, and re-running never converges on the state in the
# configuration file. If the configuration has a version, check that that version or later is present
$upToDate = if ($app.version) {
$wanted = [version] $app.version
[bool]($installed | Where-Object {
$cur = $_.DisplayVersion -as [version] # notations that aren't version-shaped are out of scope
$cur -and $cur -ge $wanted
})
} else {
[bool] $installed
}
if ($upToDate) { continue }
# Start-Process's -ArgumentList joins the array with spaces into a single command line,
# so quote values containing spaces on the configuration-file side
# e.g. '/quiet /norestart INSTALLDIR="C:\Program Files\KsApp"'
# An .msi is not an executable, so passing it straight to Start-Process fails with
# "is not a valid application". Launch it via msiexec
if ([System.IO.Path]::GetExtension($app.installer) -eq '.msi') {
$msiArgs = @('/i', "`"$($app.installer)`"") + $app.arguments
$proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow
}
else {
$proc = Start-Process -FilePath $app.installer -ArgumentList $app.arguments -Wait -PassThru -NoNewWindow
}
# With Windows Installer, "only 0 is success" is wrong.
# 3010 and 1641 are both success; only the handling of the restart differs
switch ($proc.ExitCode) {
0 { } # success
3010 { $rebootRequired = $true } # success. Restart required (ERROR_SUCCESS_REBOOT_REQUIRED)
1641 { $rebootInitiated = $true } # success. The installer has initiated a restart
default {
throw "Failed to install $($app.displayName) (ExitCode=$($proc.ExitCode))"
}
}
# If a restart has already begun, subsequent installs would be interrupted anyway
if ($rebootInitiated) { break }
}
if ($rebootInitiated) {
# 1641 means "success, but a restart has already been initiated". If you return 0, the deployment
# tool treats it as "finished, then restarted on its own", so pass it through
Write-Host 'The installer has initiated a restart. Re-run after the restart' -ForegroundColor Yellow
exit 1641
}
if ($rebootRequired) {
# Returning 3010 as-is lets Intune or the deployment tool interpret it as "success, restart required"
# and schedule and report the restart. Returning 0 here means the restart gets forgotten
Write-Host 'Provisioning complete (restart required)' -ForegroundColor Yellow
exit 3010
}
Write-Host 'Provisioning complete' -ForegroundColor Green
exit 0
}
catch {
Write-Warning "Failed: $($_.Exception.Message)"
Write-Warning $_.InvocationInfo.PositionMessage
exit 1
}
finally {
Stop-Transcript
}
Note that this administrator script deliberately leaves out mapping network drives. Drive-letter mappings are per-logon-session settings, so one created in an elevated administrator session is not visible from the user’s ordinary Explorer under UAC. If you run it with system privileges from Intune or similar, it gets mapped into SYSTEM’s session in the first place, which is of no use to the user. The correct approach is to run user-specific settings non-elevated, at that user’s logon.
# Per-user settings (run from a logon script, or from HKCU's Run, non-elevated)
# This is a separate process from the administrator script, so $config is not inherited.
# Re-read the configuration that the administrator phase distributed to ProgramData
$configPath = 'C:\ProgramData\KsKitting\kitting.config.json'
if (-not (Test-Path $configPath)) {
Write-Warning "Configuration file not found: $configPath"
exit 1
}
$config = Get-Content $configPath -Raw -Encoding utf8 | ConvertFrom-Json
# Per-user registry settings (HideFileExt and the like. Writing to HKLM has no effect)
foreach ($reg in $config.userRegistry) {
if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }
$item = Get-Item -Path $reg.key
$exists = $item.GetValueNames() -contains $reg.name
$same = $exists -and
$item.GetValue($reg.name) -eq $reg.value -and
$item.GetValueKind($reg.name).ToString() -eq $reg.type
if (-not $same) {
Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
}
}
# Network drives
foreach ($drive in $config.drives) {
$local = "$($drive.letter):"
$existing = Get-SmbMapping -LocalPath $local -ErrorAction SilentlyContinue
if ($existing) {
# If you judge only on "is there a mapping", then when the share moves and you change the
# setting, the old mapping stays put and it is still reported as "success". Compare the target too
if ($existing.RemotePath.TrimEnd('\') -eq $drive.path.TrimEnd('\')) { continue }
Remove-SmbMapping -LocalPath $local -Force
}
New-SmbMapping -LocalPath $local -RemotePath $drive.path -Persistent $true
}
# Connecting to shared printers is per-user too. Run it as an administrator or as SYSTEM and
# the connection is created only for that account, invisible to the user
foreach ($printer in $config.printers) {
if (-not (Get-Printer -Name $printer.name -ErrorAction SilentlyContinue)) {
Add-Printer -ConnectionName $printer.connection
}
}
Connecting to a shared printer (Add-Printer -ConnectionName) is the same kind of thing. It creates a per-user connection, so doing it in the administrator script or in Intune’s SYSTEM execution leaves it invisible to the employee who logs on afterwards. If you want every machine to have it, either use a mechanism that deploys printers per machine (such as print server policy distribution), or run it in this non-elevated phase.
For the same reason, placing files under the user profile, writing to HKCU, and creating user shortcuts all belong in this non-elevated phase. “Machine-wide settings once, as administrator; user-specific settings at each logon, non-elevated” — this two-stage structure is the basic shape of a provisioning script. For the pitfalls of UNC paths and drive mappings, see also “Pitfalls of Network Drives and UNC Paths”.
Some design points:
- Separate settings that need elevation from per-user settings. As above, mixing them leads to the “the drive I mapped isn’t visible” accident
- Externalise the settings into JSON. Differences per department or per model can be expressed without changing the script
- Check the current state before each operation. This is what makes it safe to run any number of times
- Leave an audit trail with
Start-Transcript. “What was done on this machine” can be traced afterwards (see “PowerShell Output Streams and Logging Design”) - Return an exit code. Intune or your deployment tool can then judge success or failure. Note that with Windows Installer, 0 is not the only success code. Both 3010 (ERROR_SUCCESS_REBOOT_REQUIRED, restart required) and 1641 (ERROR_SUCCESS_REBOOT_INITIATED, restart already initiated) are successes.5 If you let these fall into
defaultand treat them as failures, successful installs are recorded in red in the deployment tool. Treat them as successes, and the key point is to return that same code to the caller at the end. Return 0 and the deployment tool loses any way of knowing a restart is needed (see “PowerShell Error Handling and Retry Design”) - Watch the quoting of installer arguments.
Start-Process -ArgumentListmerely joins the array with spaces, so argument boundaries are not preserved. Either quote paths containing spaces on the configuration-file side, or useProcessStartInfo.ArgumentList(PowerShell 7) (see “Calling External Executables Correctly From PowerShell”)
6. Using winget From the PowerShell Module
Using the PowerShell module is more robust than parsing command-line output as strings. Microsoft.WinGet.Client provides cmdlets such as Find-WinGetPackage / Install-WinGetPackage / Get-WinGetPackage / Update-WinGetPackage.4
Install-Module -Name Microsoft.WinGet.Client -Scope AllUsers -Force
# Check whether it is installed before installing (idempotent)
foreach ($id in 'Google.Chrome', 'Microsoft.VisualStudioCode', '7zip.7zip') {
if (Get-WinGetPackage -Id $id -ErrorAction SilentlyContinue) {
Write-Verbose "Already installed: $id"
continue
}
Install-WinGetPackage -Id $id -Mode Silent -Scope System
}
Because results come back as objects, success/failure checks and list reconciliation can be written directly — that is the advantage.
7. Cautions for Unattended and System-Context Execution
Try to fully automate provisioning and you will always run into the question of “which account does this run as.”
- Parts of winget assume execution in a user context. Execution in the system context is still something Microsoft lists as a future feature3
- Separate the processing that needs elevation from the user-specific processing. A structure where machine-wide settings run with administrator privileges and settings under the user profile run at first logon is easier to handle
- Always verify with the actual execution account. “It worked under my own administrator account, but not once it was deployed” is the most common failure in this area (see “When Task Scheduler Tasks Don’t Run”)
8. Practical Rules of Thumb (Decision Table)
| What to do | Means | Notes |
|---|---|---|
| Install commercial/open-source products | winget install / configure | --silent --accept-* are mandatory1 |
| Extract an existing standard machine’s configuration | winget export |
Settings are not included. Use it as a starting point2 |
| Applications + Windows settings, declaratively | winget configure (YAML) |
Win10 1809 or later + winget 1.6 or later3 |
| Registry, Windows features, in-house applications | PowerShell (as administrator) | Check state before changing (idempotent) |
| Shared drives, shared printers, user-specific settings | PowerShell (at logon, non-elevated) | Created elevated or as SYSTEM, they are invisible to the user |
| In-house line-of-business applications | PowerShell + silent installer | Building a dedicated repository is overkill at small scale |
| Driving it from PowerShell | Microsoft.WinGet.Client |
No need to parse output as strings4 |
| Unattended execution | Verify with the execution account | The system context has limitations3 |
| Records of what was done | Start-Transcript + exit code |
Leave a record of “what was done to this machine” |
| When a restart is required | Return exit code 3010 / 1641 | Both are successes. Return 0 and the deployment tool cannot recognise the restart5 |
9. Summary
- A provisioning runbook can be replaced with an executable file. A realistic split is winget for application installation and PowerShell for everything else.
- With
winget install, always include--silentplus--accept-package-agreementsand--accept-source-agreements. Forget them and an unattended run stalls. winget exportcan extract the configuration from an existing standard machine, but settings and applications outside winget’s management are not included.- Use WinGet Configuration (
winget configure) and you can put applications and settings into a single declarative file, giving you a configuration that stands up to being re-run. - Write the PowerShell side so that it always “checks the current state before changing it,” to ensure idempotency.
- Split the execution phases: machine-wide settings with administrator privileges, and user-specific settings such as network drives and shared printers at logon, non-elevated. Connections created in an elevated session or as SYSTEM are invisible to the user.
- For unattended execution, verifying the execution account matters most. Design on the assumption that running winget in the system context has limitations.
Downloading the Sample Code
The code covered in this article is distributed as a package you can run as-is. It includes the administrator phase, the user phase, and a complete example of the configuration file.
Download the sample code (zip)
Because the samples in this article depend on Windows and on your tenant, they have not been verified by execution. Syntax parsing and static analysis with PSScriptAnalyzer have been run against every file, but please always confirm the behaviour on your own test machine.
# Syntax parsing + static analysis (runs on non-Windows too)
./Invoke-SampleTests.ps1
Configuration values (paths, server names, tenant IDs, and so on) are examples. Do not run them against production as they stand — adapt them to your own environment.
Related Articles
- Calling External Executables Correctly From PowerShell — The Pitfalls of Argument Quoting, Exit Codes, and Garbled Output
- PowerShell Error Handling and Retry Design — From the try/catch Trap to Exit Codes and Retry Best Practices
- PowerShell Output Streams and Logging Design — Giving Up Write-Host
- When Task Scheduler Tasks Don’t Run or Exit with 0x1 — Isolating the Cause and Designing for Reliable Operation
- Locking Down Business Terminals With Kiosk Mode — Choosing Between Assigned Access and Shell Launcher, and Designing the Operations
- Options After Windows 10 End of Support — Deciding Between ESU, LTSC, and Migration
Related Consulting Areas
KomuraSoft LLC handles PC provisioning and automation of company-standard environments, turning operations that live only in runbooks and individual know-how into something executable, and design support for deployment scripts.
- Technical Consulting and Design Review
- Business System Development
- Modification and Maintenance of Existing Windows Software
- Contact
References
-
Microsoft Learn, install command (winget). On specifying the target with –id / -e, unattended installation with –silent, accepting terms of use with –accept-package-agreements / –accept-source-agreements, and specifying the installation scope (user / machine) with –scope. Also on the command list in Use WinGet to install and manage applications. ↩ ↩2 ↩3
-
Microsoft Learn, export command (winget). On being able to write the list of installed packages out to JSON, recording versions with –include-versions, restoring with the import command and the behaviour of –ignore-unavailable, and on the export being limited to packages managed by winget. ↩ ↩2 ↩3
-
Microsoft Learn, WinGet Configuration. On WinGet Configuration being a mechanism that declares the desired state in YAML and applies it using PowerShell DSC, on it being usable for unattended setup, on requiring Windows 10 version 1809 (build 17763) or later or Windows 11 and WinGet v1.6.2631 or later, on the handling of UAC when run from an administrator shell, and on execution in the system context being listed as a future development item. Also on show and –accept-configuration-agreements in the configure command. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
GitHub, microsoft/winget-cli — Microsoft.WinGet.Client PowerShell module. On being able to install the Microsoft.WinGet.Client module from the PowerShell Gallery, and on it providing cmdlets such as Find-WinGetPackage / Get-WinGetPackage / Install-WinGetPackage / Update-WinGetPackage / Uninstall-WinGetPackage whose results can be handled as objects. ↩ ↩2 ↩3
-
Microsoft Learn, Windows Installer Error Codes. On ERROR_SUCCESS_REBOOT_REQUIRED (3010) meaning “a restart is required for the changes to take effect; the installation itself succeeded”, and on ERROR_SUCCESS_REBOOT_INITIATED (1641) meaning “the installer has initiated a restart; a code indicating success”. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Parameter Design and Modularization for PowerShell Scripts — From a Script That Works to a Script You Can Hand Over
A step-by-step procedure for raising a PowerShell script to a quality you can hand to someone else. Covers the param block and [CmdletBin...
Hardening PowerShell — Logging, AMSI, Language Modes, and JEA
A practical guide to using PowerShell safely instead of banning it. Covers enabling script block logging and transcription, AMSI and disa...
Integrating With REST APIs From PowerShell — Invoke-RestMethod in Practice
A practical guide to calling in-house and SaaS REST APIs from PowerShell: passing authentication headers, avoiding mojibake in non-ASCII ...
Where to Look When a PowerShell Script Is Slow — Arrays, Pipelines and Matching
The classic causes of slow PowerShell scripts, laid out. Why += on an array is O(n^2), the difference between the pipeline and foreach, t...
Stop Using Write-Host — PowerShell Output Streams and Log Design
How to choose between PowerShell's six output streams, the problems with Write-Host and where it genuinely belongs, why function return v...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- Can winget import automate the whole of PC provisioning?
- It can automate the application installs, but that alone is not enough. What winget import reproduces is a list of packages; in-app settings, adding printers, mapping network drives, power settings, company-standard settings applied through the registry, and so on are all out of scope. Applications installed by means other than winget, and in-house line-of-business applications, are also not included in the export. In practice, the realistic approach is a two-stage setup: leave application installation to winget and fill in the remaining settings with a PowerShell script.
- What is the difference between winget and WinGet Configuration (winget configure)?
- winget's install/import are procedural instructions of the form "install these, in this order," whereas WinGet Configuration is a way of writing a declaration in a YAML file of "this is the state I want to end up in." Internally it uses PowerShell DSC, so a single file can express not only application installation but also Windows settings and application configuration. Because it does nothing when the desired state is already in place, you can simply re-run the same file if something fails partway through — which makes it robust when provisioning has to be redone. It requires Windows 10 1809 or later and winget 1.6 or later.
- Is it safe to run winget with SYSTEM privileges from Task Scheduler or Intune?
- Care is needed. Parts of winget assume execution in a user context, and execution in the system context is still something Microsoft lists as a future development item. In practice you work around it by using --scope machine to install for all users, by going through the PowerShell module (Microsoft.WinGet.Client), or by running it in the user context at first logon. Either way, always verify with the actual account the script will run under.
- Should a provisioning script be safe to run any number of times?
- Yes. Treat idempotency (getting the same result no matter how many times it runs) as mandatory. Provisioning routinely fails partway through, and each time you need to be able to start over from the beginning. If you write it so that folder creation checks for existence with Test-Path first, registry settings check the current value first, and application installs check whether the application is already installed first, you can resume from the point of failure. WinGet Configuration has this way of thinking built in from the start.
- Can we distribute in-house line-of-business applications through winget?
- It is possible if you set up an internal private repository (a REST API source), but that means building and maintaining a server for it. For just a handful of applications, silently running an installer from a shared folder with a PowerShell script is simpler. For small organisations, the most realistic split is to leave commercial and open-source products to winget and install in-house applications with PowerShell.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links