Skip to content

Architecture & Design

An overview of how WinUtil is put together: the source layout, the build step that compiles everything into a single PowerShell script, the WPF interface and the configuration files that describe each app, tweak and feature.

WinUtil is a PowerShell-based Windows utility with a WPF (Windows Presentation Foundation) GUI. This document explains the architecture, code structure, and how different components work together.

┌─────────────────────────────────────────────────────┐
│ Winutil GUI │
│ (WPF XAML Interface) │
└──────────────────┬──────────────────────────────────┘
┌─────────┴─────────┐
│ │
┌────────▼──────┐ ┌───────▼────────┐
│ Public APIs │ │ Private APIs │
│ (User-facing)│ │ (Internal) │
└───────┬───────┘ └───────┬────────┘
│ │
└────────┬──────────┘
┌────────────▼────────────┐
│ Configuration Files │
│ (JSON definitions) │
└────────────┬────────────┘
┌────────────▼────────────┐
│ External Tools │
│ (WinGet, Chocolatey) │
└─────────────────────────┘
winutil/
├── Compile.ps1 # Build script that combines all files
├── winutil.ps1 # Compiled output (generated)
├── scripts/
│ ├── main.ps1 # Entry point and GUI initialization
│ └── start.ps1 # Startup logic
├── functions/
│ ├── private/ # Internal helper functions
│ │ ├── Get-WinUtilVariables.ps1
│ │ ├── Install-WinUtilWinget.ps1
│ │ └── ...
│ ├── public/ # User-facing functions
│ │ ├── Initialize-WPFUI.ps1
│ │ └── ...
├── config/ # JSON configuration files
│ ├── applications.json # Application definitions
│ ├── tweaks.json # Tweak definitions
│ ├── feature.json # Windows feature definitions
│ └── preset.json # Preset configurations
├── xaml/
│ └── inputXML.xaml # GUI layout definition
└── docs/ # Documentation

Purpose: Combines all separate script files into a single winutil.ps1 for distribution.

Process:

  1. Reads all function files from /functions/
  2. Includes configuration JSON files
  3. Embeds XAML GUI definition
  4. Combines into a single script
  5. Outputs winutil.ps1

Why: Makes distribution easier (single file) and improves load time.

Purpose: Entry point that manages the run.

Responsibilities:

  • Load configurations
  • Run the headless -Preset and -Config paths
  • Start the interface on a dedicated STA runspace and wait for it
  • Report anything the interface thread failed with, then clean up

The interface itself lives in Start-WinUtilUserInterface, not here. See Threading Model.

Purpose: User-facing functions that implement main features.

Key Functions:

  • Initialize-WPFUI.ps1: Sets up the GUI
  • Invoke-WPFTweak*: Applies system tweaks
  • Invoke-WPFFeature*: Enables Windows features
  • Install-WinUtilProgram*: Installs applications

Naming Convention: Functions start with WPF or Winutil to be loaded into the runspace.

Purpose: Internal helper functions not directly called by users.

Key Functions:

  • Get-WinUtilVariables.ps1: Retrieves UI element references
  • Install-WinUtilWinget.ps1: Ensures WinGet is installed
  • Get-WinUtilCheckBoxes.ps1: Gets checkbox states
  • Invoke-WinUtilCurrentSystem.ps1: Gets system information

Purpose: Define available applications, tweaks, and features declaratively.

Files:

  • applications.json: Application definitions with WinGet/Choco IDs
  • tweaks.json: Registry tweaks and their undo actions
  • feature.json: Windows features that can be enabled/disabled
  • preset.json: Predefined tweak combinations
  • dns.json: DNS provider configurations

Purpose: WPF GUI layout and design.

Structure:

  • Buttons with event handlers
  • TextBoxes for input
  • CheckBoxes for options
  • ListBoxes for selections

The Win11 Creator is a specialized subsystem within Winutil that creates customized Windows 11 ISOs. It operates independently of the main package installation and tweak system.

Core Functions (functions/private/):

  • Invoke-WinUtilISO.ps1: Main orchestrator containing all Win11 Creator functions

    • Invoke-WinUtilISOBrowse: ISO file selection dialog
    • Invoke-WinUtilISOMountAndVerify: Validates and mounts ISO, verifies it is an official Windows 11 ISO
    • Invoke-WinUtilISOModify: Launches modification in background runspace
    • Invoke-WinUtilISOExport: Handles ISO and USB export
    • Invoke-WinUtilISOCheckExistingWork: Recovers incomplete work sessions
    • Invoke-WinUtilISOCleanAndReset: Cleans up temp directories and resets UI
  • Invoke-WinUtilISOScript.ps1: Applies modifications to mounted install.wim

    • Removes provisioned AppX packages (40+ bloatware apps)
    • Injects drivers (optional) from the current system
    • Removes OneDrive setup files
    • Applies offline registry tweaks (hardware bypass, privacy, telemetry, OOBE)
    • Deletes telemetry scheduled task definitions
    • Pre-stages setup scripts from autounattend.xml
User selects official Windows 11 ISO
Invoke-WinUtilISOBrowse → OpenFileDialog, validates file size
Invoke-WinUtilISOMountAndVerify
├─ Mount ISO via Mount-DiskImage
├─ Verify install.wim or install.esd exists
├─ Check for "Windows 11" in image metadata
├─ Extract available editions (Home, Pro, Enterprise, etc.)
└─ Store ISO path, drive letter, WIM path, image info in $sync
User optionally enables the Driver Injection checkbox
Invoke-WinUtilISOModify (runs in background runspace)
├─ Create work directory: ~WinUtil_Win11ISO_[timestamp]
├─ Copy ISO contents to disk (~5-6 GB)
├─ Mount install.wim at selected edition/index
├─ Invoke-WinUtilISOScript:
│ ├─ Remove 40+ bloat AppX packages
│ ├─ Export and inject drivers (if enabled)
│ ├─ Remove OneDrive setup
│ ├─ Load offline registry hives
│ ├─ Apply 50+ registry tweaks (hardware bypass, privacy, telemetry, OOBE, etc.)
│ ├─ Delete telemetry scheduled task files
│ ├─ Pre-stage setup scripts from autounattend.xml to C:\Windows\Setup\Scripts\
│ └─ Unload registry hives
├─ Dismount and save the modified install.wim (~10+ minutes, slowest step)
├─ Dismount source ISO
└─ Report completion, enable export options
Invoke-WinUtilISOExport (user chooses output)
├─ Option 1: Save as ISO
│ ├─ Build bootable ISO via oscdimg.exe (BIOS/UEFI dual-boot)
│ └─ Output: Win11_Modified_[date].iso (close to the source ISO size)
└─ Option 2: Write to USB
├─ Format USB as GPT
├─ Create 512 MB EFI partition
├─ Copy modified ISO contents
└─ Output: Bootable USB (minimum 8 GB)
Invoke-WinUtilISOCleanAndReset (optional)
└─ Delete temp working directory (~10-15 GB)
└─ Reset UI to initial state

ISO Validation:

  • Only accepts official Microsoft Windows 11 ISOs
  • Validates presence of install.wim or install.esd
  • Checks image metadata for “Windows 11” string
  • Rejects custom, modified, or non-Windows 11 ISOs

Work Session Recovery:

  • Auto-detects incomplete work from previous sessions
  • Allows resuming Step 4 (export) without re-running Steps 1-3
  • Prevents redundant modifications

Modification Safety:

  • All registry changes are documented in a script (reversible)
  • Original ISO never modified; only working copy
  • Logged to WinUtil_Win11ISO.log for debugging
  • DISM handles image dismount with automatic cleanup on error

The Invoke-WinUtilISOScript function applies 50+ offline registry tweaks:

Hardware Bypass:

  • TPM 2.0 check bypass
  • Secure Boot requirement bypass
  • CPU compatibility bypass
  • RAM requirement bypass
  • Storage check bypass

Privacy & Telemetry:

  • Disable advertising ID
  • Disable tailored experiences
  • Disable input personalization
  • Disable speech online privacy
  • Disable cloud content suggestions
  • Disable app suggestion subscriptions
  • Remove CEIP, Appraiser, WaaSMedic, etc.

OOBE & Setup:

  • Enable local account setup
  • Skip Microsoft account requirement
  • Dark mode by default
  • Empty taskbar and Start Menu

Post-Setup Installations:

  • Prevent DevHome auto-installation
  • Prevent new Outlook Mail app installation
  • Prevent Teams auto-installation

System Features:

  • Disable BitLocker and device encryption
  • Disable Chat icon from the Taskbar
  • Disable OneDrive folder backup
  • Disable Copilot
  • Disable Windows Update during OOBE (re-enabled at first login)

Optional Enhancement: When enabled, exports all drivers from the running system and injects them into both:

  • install.wim (main OS image)
  • boot.wim index 2 (Windows Setup PE environment)

Use Case: Enables offline installation on systems with missing drivers.

  • Temporary working directory: ~10-15 GB
  • Original ISO: 4-6 GB
  • Modified ISO: close to the source ISO size
  • Total needed: ~25 GB for safe operation
User clicks "Install"
Get-WinUtilCheckBoxes → Retrieves selected apps
For each selected app:
Check if WinGet/Choco is installed
Install-WinUtilWinget/Choco (if needed)
Install-WinUtilProgramWinget/Choco → Install app
Update UI with progress
Display completion message
User selects tweaks and clicks "Run Tweaks"
Get-WinUtilCheckBoxes → Get selected tweaks
For each selected tweak:
Load tweak definition from tweaks.json
Invoke-WPFTweak → Apply registry/service changes
Log changes
Store original values (for undo)
Update UI
Display completion
User selects tweaks and clicks "Undo"
Get-WinUtilCheckBoxes → Get selected tweaks
For each tweak:
Retrieve "OriginalState" from tweak definition
Invoke-WPFUndoTweak → Restore original values
Remove from the applied tweaks log
Update UI
config/applications.json
{
"WPFInstall<AppName>": {
"category": "Browsers",
"choco": "googlechrome",
"content": "Google Chrome",
"description": "Google Chrome browser",
"link": "https://chrome.google.com",
"winget": "Google.Chrome"
}
}

Fields:

  • category: Which section in the Install tab
  • content: Display name in GUI
  • description: Tooltip/description text
  • winget: WinGet package ID
  • choco: Chocolatey package name
  • link: Official website
config/tweaks.json
{
"WPFTweaksTelemetry": {
"Content": "Disable Telemetry",
"Description": "Disables Microsoft Telemetry",
"category": "Essential Tweaks",
"panel": "1",
"registry": [
{
"Path": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\DataCollection",
"Name": "AllowTelemetry",
"Type": "DWord",
"Value": "0",
"OriginalValue": "1"
}
]
}
}

Fields:

  • Content: Display name
  • Description: What it does
  • category: Essential/Advanced/Customize
  • registry: Registry changes to make
  • registry[].Values: Per-state values for a registry-backed combobox
  • registry[].DefaultValue: Effective value when the registry entry is absent
  • service: Services to change
  • OriginalValue/State: For undo functionality

WinUtil runs on three kinds of thread, and each has one job:

Thread Runspace Responsibility
Main The one the script started in Start the interface, wait for it, surface its errors, clean up
Interface $sync.UIRunspace, a dedicated STA runspace Own the window. Paint and dispatch, nothing else
Workers $sync.runspace, a shared pool Run everything long: installs, tweaks, features, AppX, Win11 Creator

All three are created from the same starting point, New-WinUtilSessionState, which carries $sync, the compiled script’s globals, and every WinUtil function. That is what lets any thread call any helper without the caller injecting function definitions.

Terminal window
# main.ps1 - the interface gets its own thread
$sync.UIRunspace = [runspacefactory]::CreateRunspace($Host, (New-WinUtilSessionState))
$sync.UIRunspace.ApartmentState = "STA"
$sync.UIRunspace.Open()
$uiShell = [powershell]::Create()
$uiShell.Runspace = $sync.UIRunspace
[void]$uiShell.AddScript({ Start-WinUtilUserInterface })
$uiHandle = $uiShell.BeginInvoke()
$uiHandle.AsyncWaitHandle.WaitOne()

Why: the window never blocks on work, and a failure on the interface thread is reported instead of disappearing.

Every long action goes through Start-WinUtilJob, which owns everything a running operation needs: refusing to start while another job runs, the busy flag ($sync.ActiveJob), the progress bar and taskbar item, the boxed console banner, a start/finish/failure line in the log, and restoring the interface in a finally whatever happens.

Invoke-WPFButton decides what counts as a long action. Anything that changes the system gets a job; anything that only changes what the interface is showing runs on the interface thread. That single classification is why no workflow arranges its own progress, banner or busy state.

Terminal window
Start-WinUtilJob -Name "Features" -Description "Installing Windows Features" -Parameters @{
Features = @($sync.selectedFeatures)
} -ScriptBlock {
param($Features)
$total = @($Features).Count
$completed = 0
foreach ($feature in $Features) {
$completed++
Step-WinUtilJob -Status "Installing $feature ($completed/$total)" -Percent ([int](($completed / $total) * 100))
Invoke-WinUtilFeatureInstall $feature
}
}

The body only has to do the work and call Step-WinUtilJob. Anything it throws is caught, logged, and shown on the taskbar as a failure.

Values, not closures: the body is rebuilt inside the worker from its text, so it receives what it needs through -Parameters rather than capturing the caller’s variables.

Events are wired up via XAML element names:

Terminal window
# Get all named elements
$sync.keys | ForEach-Object {
if($sync.$_.GetType().Name -eq "Button") {
$sync.$_.Add_Click({
$button = $sync.$($args[0].Name)
& "Invoke-$($args[0].Name)"
})
}
}

Convention: Button named WPFInstallButton calls function Invoke-WPFInstallButton.

Terminal window
# Check if installed
if (!(Get-Command winget -ErrorAction SilentlyContinue)) {
Install-WinUtilWinget
}
# Install package
winget install --id $app.winget --silent --accept-source-agreements
Terminal window
# Check if installed
if (!(Get-Command choco -ErrorAction SilentlyContinue)) {
Install-WinUtilChoco
}
# Install package
choco install $app.choco -y

A job body does not need its own error handling. Start-WinUtilJob catches whatever the body throws, logs it, and marks the run as failed on the taskbar, so a failure can never leave the interface stuck busy. Only catch inside a body when you have something specific to do first, such as cleaning up a mounted image, and then rethrow.

Terminal window
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "winget install failed: $($_.Exception.Message)"

Entries and console diagnostics go to the same %LocalAppData%\winutil\logs\winutil_<timestamp>.log session file. While Start-Transcript owns that file, Write-WinUtilLog writes through the host so the transcript captures the entry without a competing direct file write.

At startup, Winutil loads all configurations:

Terminal window
# Load JSON configs
$sync.configs = @{}
$sync.configs.applications = Get-Content "config/applications.json" | ConvertFrom-Json
$sync.configs.tweaks = Get-Content "config/tweaks.json" | ConvertFrom-Json
$sync.configs.features = Get-Content "config/feature.json" | ConvertFrom-Json

Sync Hash: $sync hashtable shares state across runspaces.

Controls may only be touched from the thread that owns the window, so background work reaches them through Invoke-WPFUIThread:

Terminal window
Invoke-WPFUIThread -Parameters @{ Count = $installed.Count } -ScriptBlock {
param($Count)
$sync.WPFselectedAppsButton.Content = "Selected Apps: $Count"
}

Add -Async to post the update instead of waiting for it. Step-WinUtilJob and the Win11 Creator status log use that so a per-item update never stalls the worker.

Values, not closures: the body is rebuilt inside the interface runspace, so it takes what it needs through -Parameters. Handing over a scriptblock from a worker instead would keep that worker’s session state, which loses the caller’s variables on an async post and costs roughly twenty times as much per command - enough to turn a checkbox refresh into a visible freeze.

  1. Edit config/applications.json:
config/applications.json
{
"WPFInstallNewApp": {
"category": "Utilities",
"content": "New App",
"description": "Description of new app",
"winget": "Publisher.AppName",
"choco": "appname"
}
}
  1. Recompile: .\Compile.ps1
  2. The app appears automatically in the Install tab
  1. Edit config/tweaks.json:
config/tweaks.json
{
"WPFTweaksNewTweak": {
"Content": "New Tweak",
"Description": "What it does",
"category": "Essential Tweaks",
"registry": [
{
"Path": "HKLM:\\Path\\To\\Key",
"Name": "ValueName",
"Type": "DWord",
"Value": "1",
"OriginalValue": "0"
}
]
}
}
  1. Recompile: .\Compile.ps1
  2. Tweak appears in the Tweaks tab
  1. Create file in functions/public/ or functions/private/:
functions/public/Invoke-WPFNewFeature.ps1
function Invoke-WPFNewFeature {
<#
.SYNOPSIS
Does something new
#>
# Implementation
}
  1. File naming must include “WPF” or “Winutil” to load
  2. Recompile: .\Compile.ps1
Terminal window
# Compile and run with -run flag
.\Compile.ps1 -run

Tests are in /pester/:

  • configs.Tests.ps1: Validates JSON configurations
  • functions.Tests.ps1: Tests PowerShell functions

Run tests:

Terminal window
Install-Module -Name Pester -RequiredVersion 5.8.0 -Scope CurrentUser -Force -SkipPublisherCheck
Import-Module Pester -RequiredVersion 5.8.0 -Force
Invoke-Pester -Path 'pester/*.Tests.ps1' -Output Detailed -CI
Terminal window
.\Compile.ps1

Outputs winutil.ps1 in the root directory.

  1. Tag release in Git
  2. GitHub Actions builds and uploads winutil.ps1
  3. Release appears on GitHub Releases
  4. Users download via irm christitus.com/win

Required:

  • PowerShell 5.1+
  • .NET Framework 4.5+
  • Windows 11

Optional (auto-installed):

  • WinGet (Windows Package Manager)
  • Chocolatey

Optimization Strategies:

  • Lazy-load configurations (only when needed)
  • Use runspaces for long operations
  • Cache expensive lookups
  • Minimize registry reads/writes
  • Batch operations when possible

Safety Measures:

  • All operations logged
  • Registry backups for undo
  • No credential storage
  • Open source (auditable)
  • Digitally signed (future)

Code Standards:

  • Use proper PowerShell cmdlet naming (Verb-Noun)
  • Include comment-based help
  • Follow existing code style
  • Test thoroughly before PR
  • Document significant changes

File Naming:

  • Public functions: Invoke-WPF*.ps1 or Invoke-Winutil*.ps1
  • Private functions: Get-WinUtil*.ps1 or verb-WinUtil*.ps1`
  • Must include “WPF” or “Winutil” to load

Roadmap Considerations:

  • Plugin system for community extensions
  • Config import/export
  • Cloud sync for configurations
  • Enhanced logging dashboard
  • Modular compilation (choose features)

Last Updated: January 2026 Maintainers: Chris Titus Tech and contributors