Active Setup handles the gap between machine-wide installation and per-user configuration on Windows. It executes a command once for each user during sign-in, before the desktop loads and at a higher privilege level than RunOnce. That makes it useful when software must be installed under the system account but still needs settings written in each user’s context, such as a Git default branch. section 11.4.2 section 11.4.3
The mechanism is registered under HKLM\SOFTWARE\Microsoft\Active Setup\Installed Components, so a system-context process can create it. Each component stores a version and an execution command. At login, Windows compares the machine version with the copy held in the current user hive; a missing or older user value causes the command to run, after which the user-side version is recorded. Raising the component version therefore causes the updated command to run again for users. section 11.4.3
Treat the system installation and user customization as separate tasks. The system task can install shared binaries and machine-level settings, while the Active Setup command applies user-level state. This separation matters because a process running as system has no user profile or HKCU access, so configuration that appears global to an interactive user may otherwise be absent. section 11.4.2 section 11.4.3
A maintainable Active Setup implementation keeps registration, script storage, and observability aligned:
- Create a consistently named component and assign an explicit version so that changes can be deliberately redeployed to users. section 11.4.3
- Store the generated script under ProgramData, using its component name and version, rather than leaving an untracked command in the registry. section 11.4.3
- Write each user’s log under that user’s local application-data folder with the same component name, creating a one-to-one mapping among registry key, script, and log. section 11.4.3
- Wrap execution with PowerShell Start-Transcript so failures can be investigated after sign-in. section 11.4.3
Logging is essential because Active Setup runs before the desktop is available: errors are not visible to the user. A current-user registry value can confirm that the mechanism ran, but it cannot confirm that the command succeeded. It also does not preserve the contents of an external script if that script is later removed or changed. Persisted scripts and per-user transcripts provide the missing audit trail. section 11.4.3
Listing 11.4 New-ActiveSetup
Function New-ActiveSetup {
param(
[string]$Name,
[System.Management.Automation.ScriptBlock]$ScriptBlock,
[version]$Version = '1.0.0.0'
)
$ActiveSetupReg = #1
'HKLM:\Software\Microsoft\Active Setup\Installed Components'
$Item = @{ #2
Path = $ActiveSetupReg
Name = $Name
Force = $true
}
$ActiveSetup = New-Item @Item | Select-Object -ExpandProperty PSPath
$DefaultPath = 'ActiveSetup\{0}_v{1}.ps1' #3
$ChildPath = $DefaultPath -f $Name, $Version
$ScriptPath = Join-Path -Path $env:ProgramData -ChildPath $ChildPath
$ScriptFolder = Split-Path -Path $ScriptPath
if (-not(Test-Path -Path $ScriptFolder)) { #4
New-Item -type Directory -Path $ScriptFolder | Out-Null
}
$WrapperScript = { #5
param($Name,$Version)
$Path = "ActiveSetup\$($Name)_$($Version).log"
$log = Join-Path $env:APPDATA $Path
$Transcript = @{ Path = $log; Append = $true;
IncludeInvocationHeader = $true; Force = $true}
Start-Transcript @Transcript
try{
{0}
}
catch{ Write-Host $_ }
finally{ Stop-Transcript }
}
$WrapperString = $WrapperScript.ToString() #6
$WrapperString = $WrapperString.Replace('{','{{')
$WrapperString = $WrapperString.Replace('}','}}')
$WrapperString = $WrapperString.Replace('{{0}}','{0}')
$WrapperString -f $ScriptBlock.ToString() | #7
Out-File -FilePath $ScriptPath -Encoding utf8
$args = @{ #8
Path = $ActiveSetup
Force = $true
}
$ActiveSetupValue = 'powershell.exe -ExecutionPolicy bypass ' +
"-File ""$($ScriptPath.Replace('\', '\\'))""" +
" -Name ""$($Name)"" -Version ""$($Version)"""
Set-ItemProperty @args -Name '(Default)' -Value $Name
Set-ItemProperty @args -Name 'Version' -Value $Version
Set-ItemProperty @args -Name 'StubPath' -Value $ActiveSetupValue
}
New-ActiveSetup packages component creation, versioned ProgramData script generation, and registration into a reusable PowerShell function. Its parameters make the component name, script block, and version explicit deployment inputs. section 11.4.3
Git installation with per-user defaults
Install Git and set machine-wide behavior while running as system, then register an Active Setup script that runs `git config --global init.defaultBranch main` for each signing-in user. Before applying the per-user command, configuration shows only the system default branch (`master`); after the user phase, the global default branch is `main`. Test from a clean installation and user configuration, then inspect the scoped Git configuration after login. section 11.4.2 section 11.4.3
Listing 11.5 Git install with Active Setup
Function New-ActiveSetup {
<#
Code from listing 4
#>
}
choco install git.install -y #1
$alias = @{ #2
Name = 'git'
Value = (Join-Path $Env:ProgramFiles 'Git\bin\git.exe')
}
New-Alias @alias -force
git config --system core.autocrlf true #3
$ScriptBlock = { #4
git config --global init.defaultBranch main
git config --global --list
}
New-ActiveSetup -Name 'Git' -ScriptBlock $ScriptBlock -Version '1.0'
The Git example separates Chocolatey installation and system configuration from the script block that establishes the user-level global Git setting, then registers that block as version 1.0. section 11.4.3
