PowerShell Windows Tools
Author production-grade PowerShell scripts for Windows system administration, cleanup utilities, service management, and interactive CLI tools.
When to Use
- Building an interactive CLI tool for Windows system management (Docker cleanup, registry management, service control)
- Writing a PowerShell script that needs to run on both Windows PowerShell 5.1 AND PowerShell 7
- Building a tool that needs auto-elevation for admin operations (UAC)
- Creating a recovery-capable tool (undo/restore points)
- Any task involving
Start-Process -Verb RunAsfor privilege escalation
Critical Gotchas
1. UTF-8 BOM or Die
Windows PowerShell 5.1 assumes ANSI encoding when reading .ps1 files without a BOM (Byte Order Mark).
If your script contains ANY Unicode characters (em-dash —, box-drawing ╔╗╚╝, emoji, non-ASCII), PS5 will
corrupt them into garbage, causing parse errors or silent data corruption.
Always save .ps1 files with UTF-8 BOM (utf-8-sig in Python, -Encoding UTF8 in PowerShell).
# Python: save with BOM
with open('script.ps1', 'w', encoding='utf-8-sig') as f:
f.write(content)
# PowerShell: save with BOM
$content | Out-File -FilePath 'script.ps1' -Encoding UTF8
2. $var: Colon Scope Trap
In PowerShell strings, $varname: is interpreted as a namespace-qualified variable reference
(like $env:PATH). Even $varname: text can trigger this in PowerShell 7, causing parser error.
BAD — crashes on PS7:
Write-Host " FAIL $svc: $_"
GOOD — use format operator:
Write-Host (" FAIL {0}: {1}" -f $svc, $_)
3. Start-Process -Verb RunAs Quoting
When using Start-Process -Verb RunAs, never pass -ArgumentList as an array.
The array-to-command-line conversion mangles quotes.
BAD — array-based:
Start-Process -FilePath pwsh.exe -Verb RunAs -ArgumentList @('-NoProfile', '-File', '"C:\path\script.ps1"') -Wait
GOOD — flat string:
$argString = '-NoProfile -File "' + $scriptPath + '" -Action ' + $actionName
Start-Process -FilePath pwsh.exe -Verb RunAs -ArgumentList $argString -Wait
4. #Requires -RunAsAdministrator Kills UX Silently
Causes PowerShell to immediately exit with no visible message if the user isn't admin. Remove it and handle elevation gracefully.
5. Exit Handling (Keep Window Open)
When right-click → "Run with PowerShell 7" (no -NoExit), the window closes immediately.
Add a pause in the finally block:
finally {
Write-Host "`n Press any key to close..." -ForegroundColor Gray
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
}
6. Capture Return Values
PowerShell emits uncaptured return values to stdout, leaking paths/objects to console.
$null = New-RestorePoint -Label "Snapshot"
Auto-Elevation Pattern (-AdminAction pattern)
When building a tool that needs admin for some operations but should not require it at startup:
1. Add -AdminAction parameter to the script
param([string]$AdminAction = '')
2. Guard in entry point — elevated child runs and exits
if ($AdminAction) {
Initialize-Environment
Invoke-AdminAction -Action $AdminAction
Write-Host "Press any key to close this window..."
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
exit 0
}
3. Invoke-Elevated — called from parent, spawns admin child
function Invoke-Elevated {
param([string]$Action, [string]$Description)
if (Test-IsAdmin) { return Invoke-AdminAction -Action $Action }
Write-Host "[ADMIN REQUIRED] $Description"
$psExe = if ($PSVersionTable.PSVersion.Major -ge 7) { 'pwsh.exe' } else { 'powershell.exe' }
# CRITICAL: flat string, NOT array
$argString = '-NoProfile -ExecutionPolicy Bypass -File "' + $scriptPath + '" -AdminAction ' + $Action
Start-Process -FilePath $psExe -Verb RunAs -ArgumentList $argString -Wait
}
4. Invoke-AdminAction — runs inside elevated child
function Invoke-AdminAction {
param([string]$Action)
switch ($Action) {
'Registry-Cleanup' { ... }
'Services-Stop' { ... }
'EnvSanitizer-Machine' { ... }
}
}
5. Startup elevation prompt
At launch when not admin, offer a choice:
[A] Auto-elevate now (relaunch entire script as admin)
[C] Continue (per-operation elevation via Invoke-Elevated)
[N] Exit
Restore Point Pattern
function New-RestorePoint {
# Create dir, snapshot files/registry/WSL2 exports, save manifest.json
}
function Invoke-Rollback {
# Restore WSL2 (wsl --import), registry (reg import), env vars
}
PS5 vs PS7 Differences
| Aspect | Windows PS 5.1 | PowerShell 7 |
|---|---|---|
| Encoding | ANSI default, needs BOM | UTF-8 without BOM default |
$var: in strings |
Tolerant | Strict — parser error |
Start-ThreadJob |
Not available | Available |
Null-coalescing ?? |
Not available | Available |
Templates
templates/admin-tool-skeleton.ps1— fully wired admin-tool starter combining all patterns above: param() block with AdminAction, Test-Admin, Invoke-Elevated with flat-string ArgumentList, Invoke-AdminAction, menu system, confirm/pause helpers, and a finally-block exit handler. Copy this file to start a new tool, then extend the switch statement and menu items.
Reference Transcripts
references/error-transcripts.md— detailed error reproduction recipes for all patterns above: #Requires silent exit, UTF-8 BOM corruption,$var:PS7 parser trap, flat-string ArgumentList, return value leakage, and Docker Desktop MSI installer cleanup (Error 6).
Verification
- Parse clean on both
powershell.exeandpwsh.exe - Non-admin startup shows elevation prompt, doesn't crash silently
Start-Process -Verb RunAsuses flat string, not array- All
$var:patterns use-fformat operator - File saved with UTF-8 BOM