Automated Setup and Debloat Script

After reinstalling a clean copy of Windows 11 for the infinite time, I had an epiphancy. Why am I doing this over and over again, downloading software needed for development soon after Windows was finished installing, enabling options and removing bloat by hand? I already have experience creating batch scripts that I admit I was a bit rusty on, and looking at other developer’s similar work using Powershell, why can’t I do this automated? Yes, I could like most sensible individuals, install Windows 11 and soon after create a copy of (in my case) of the virtual disk or create a system restore point, but doing so I could bring something undetected over to the new copy especially if the copy was a later build.
So decided to create a set of Poweshell scripts to do the hard work for me, especially after getting inspiration from Nix packages. Also I wanted to be able to add/remove packages, disable/enable services and perform registry edits as I please without editing the main code.
Details
Welcome back Chocolatey
I first learnt of Chocolatey a few years ago when I first created a set of scripts to easily download and update my programs. Chocolatey is very similar to how Linux users download their packages using the terminal, in this case Chocolatey uses the PowerShell.
As mentioned earlier, I wanted to be able to update what packages I wanted without butchering the main code. This implementation has a ‘config’ file that package names can be added. This also allows different config files with different packages, so for example I can have a Windows VM soley for needing development packages, and then another VM just for gaming needs such as Steam. Then the luxury of Chocolatey, running the script again would allow the packages to be updated.
Below is the main powershell script, which downloads the main Chocolatey if it doesnt exist, and then does a loop through the packages.config file to download the required packages to the system.
#if not installed, install chocolatey
if (!(Test-Path -Path "$env:ProgramData\Chocolatey")) {
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
} else {
Write-Host "Chocolatey is already installed. Skipping installation."
}
#get the directory path of the script
$scriptDirectory = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
#specify the relative path to the packages.config file
$packagesConfigPath = Join-Path -Path $scriptDirectory -ChildPath "..\packages.config"
#function to process package installation
function InstallPackage($packageName) {
#run the package installation
choco install -y $packageName
}
#process package configuration
if (Test-Path $packagesConfigPath) {
#read the contents of the packages.config file
$packagesConfig = Get-Content $packagesConfigPath
#for each line in the packages.config file
foreach ($line in $packagesConfig) {
#skip comment lines that begin with #
if (-not ($line -match '^\s*#')) {
#extract the package name from the line
$packageName = ($line -split "\r\n")[0].Trim()
#process package installation
InstallPackage -packageName $packageName
}
}
}
else {
#will update this to grab latest from github repo
Write-Host "packages.config file not found in the expected location."
}
Below is the ‘packages.config’ where the user can define what programs is needed for the Windows installation. Lines beginning with a ‘#’ will be ignored by the script so that comments can be added.
python
nodejs
git
gh
firefox
googlechrome
vscode
visualstudio2022community
7zip
notepadplusplus
bitwarden-cli
choco-cleaner
flow-launcher
curl
#always have last to update
chocolatey

Window Services
The Windows machine can perform better and save valuable RAM when uneeded services are disabled from starting up and running in the background. RAM especially in a development environment is precious and the more there is the better.
Below the is main powershell script for the services section. The script loops through the ‘services.config’, reads the content and splits the data between the comma which is then parsed as variables to the process configuration powershell command where different services can be disabled if they are needed.
#get the directory path of the script
$scriptDirectory = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
#construct the path to the services.config file
$servicesConfigPath = Join-Path -Path $scriptDirectory -ChildPath "..\..\services.config"
#function to process service configuration
function ProcessServiceConfiguration($serviceName, $startupType) {
# Set the service with the specified startup type
Set-Service -Name $serviceName -StartupType $startupType
Write-Host "Service $serviceName startup has been set to $startupType"
}
#process service configuration
if (Test-Path $servicesConfigPath) {
#read the contents of the services.config file
$servicesConfig = Get-Content $servicesConfigPath
#for each line in the services.config file
foreach ($line in $servicesConfig) {
#skip comment lines that begin with #
if (-not ($line -match '^\s*#')) {
#split the line by comma to separate service name and startup type
$serviceData = $line -split ","
#check if the line is correctly formatted
if ($serviceData.Length -eq 2) {
$serviceName = $serviceData[0].Trim()
$startupType = $serviceData[1].Trim()
#process service configuration
ProcessServiceConfiguration -serviceName $serviceName -startupType $startupType
}
}
}
}
else {
#will update this to grab latest from github repo
Write-Host "services.config file not found."
}
Below are a few entries inside the ‘services.config’ file. The left is the name of the service and after the comma is the startup type. Again any lines beginning with the ‘#’ are ignored which allows comments to be added.
For my needs, I need to enable Desktop Remote so that I can remotely access the VM (as it is headless). Yes you can enable it from the settings, but doing it by a script saves a little bit of time!
Spooler,Disabled
SstpSvc,Manual
StateRepository,Manual
StiSvc,Manual
StorSvc,Manual
SysMain,Auto
SystemEventsBroker,Auto
TabletInputService,Manual
TapiSrv,Manual
#enable remote
TermService,Automatic
TextInputManagementService,Auto
Themes,Auto
TieringEngineService,Manual
Registry
Again like services, we can disable/enable options in the Windows environment. For example, instead of enable ‘Show file extensions…’ which takes a few clicks, why not just do it through a script? This also disables telemetry which is not needed unless you enjoy supplying Microsoft your information!

Below is the main powershell script for the registry section.
#get the directory path of the script
$scriptDirectory = Split-Path -Parent -Path $MyInvocation.MyCommand.Path
#specify the relative path to the registry.config file
$registryConfigPath = Join-Path -Path $scriptDirectory -ChildPath "..\..\registry.config"
#function to process registry configuration
function ProcessRegistryConfiguration($path, $name, $value, $force) {
#convert the force value to a boolean
$force = $force -eq 1
#check if the registry path and name are not empty
if (![string]::IsNullOrEmpty($path) -and ![string]::IsNullOrEmpty($name)) {
#check if the registry path exists
if (Test-Path $path) {
#set the registry item property using the specified values
Set-ItemProperty -Path $path -Name $name -Value $value -Force:$force
Write-Host "Registry item property set for $name"
} else {
#create a new registry item
New-Item -Path $path -Force | Out-Null
Write-Host "New registry item created: $path"
}
} else {
Write-Host "Invalid registry path or name specified."
}
}
#process registry configuration
if (Test-Path $registryConfigPath) {
#read the contents of the registry.config file
$registryConfig = Get-Content $registryConfigPath
#flag to indicate whether to process new registry items
$processNewItems = $false
#flag to indicate whether to remove items
$removeItems = $false
#for each line in the registry.config file
foreach ($line in $registryConfig) {
#skip comment lines that begin with #
if ($line -match '^\s*#') {
#check if the line is the marker for adding new items
if ($line -match '^\s*#ADDNEWITEMS#') {
$processNewItems = $true
$removeItems = $false
}
#check if the line is the marker for removing items
if ($line -match '^\s*#REMOVEITEMS#') {
$processNewItems = $false
$removeItems = $true
}
continue
}
#split the line by comma to separate registry path, name, value, and force
$registryData = $line -split ","
#check if the line is correctly formatted
if ($registryData.Length -ge 3) {
$path = $registryData[0].Trim()
$name = $registryData[1].Trim()
$value = $registryData[2].Trim()
$force = 1
#process registry configuration
if (!$removeItems) {
ProcessRegistryConfiguration -path $path -name $name -value $value -force $force
} else {
#remove the specified registry item
if (Test-Path $path) {
Remove-Item -Path $path -Recurse -ErrorAction SilentlyContinue
Write-Host "Registry item removed: $path"
}
}
} elseif ($processNewItems -and $registryData.Length -ge 1) {
#check if new items should be processed
$path = $registryData[0].Trim()
$name = $registryData[1].Trim()
#create a new registry item
if (![string]::IsNullOrEmpty($path)) {
New-Item -Path $path -Force | Out-Null
Write-Host "New registry item created: $path"
}
} else {
Write-Host "Invalid line format in registry.config: $line"
}
}
}
else {
#will update this to grab the latest from GitHub repo
Write-Host "registry.config file not found."
}
Below is are a few entries in the registry.config file. Each data between the comma is delimited in the powershell script which are parsed as variables to the Process Registry Configuration command.
#disable widgets on taskbar
HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced,TaskbarDa,0,DWord,0
#disable chat on taskbar
HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced,TaskbarMn,0,DWord,0
#laptop power tweaks, enable if installing this on a laptop
#HKLM:\SYSTEM\CurrentControlSet\Control\Power\PowerThrottling,PowerThrottlingOff,00000000,DWord,00000001
#HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power,HiberbootEnabled,0000001,DWord,0000000
Also the config allows to add and remove entries from the registry. This can break a system so caution is always advised when adding new or removing existing registry entries from the system.
#ADDNEWITEMS#
#Disabling driver offering through Windows Update
HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata,RegistryKey,1
#REMOVEITEMS#
HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\StorageSense\Parameters\StoragePolicy,,
Debloat
Another feature is that this also ‘debloats’ the Windows installation, removing unnecessary apps from the system. This was borrowed from other debloat scripts on especially Win10Debloat by Raphire so all right reserved to the author.
There is no config file for this section of the scripts as it is all inside the ‘removedefaultapps.ps1’ powershell script. Sometime in the future I will implement a simple config to allow apps to be removed/kept without editing the main script.
$apps = @(
@{
Name = "*AdobeSystemsIncorporated.AdobePhotoshopExpress*"
Uninstall = $true
}
@{
Name = "*Clipchamp.Clipchamp*"
Uninstall = $true
}
@{
Name = "*Dolby*"
Uninstall = $true
}
@{
Name = "*Duolingo-LearnLanguagesforFree*"
Uninstall = $true
}
@{
Name = "*Facebook*"
Uninstall = $true
}
)
foreach ($app in $apps) {
if ($app.Uninstall) {
Write-Host "Attempting to remove $($app.Name)"
Get-AppxPackage -Name $app.Name -AllUsers | Remove-AppxPackage
Get-AppxProvisionedPackage -Online | Where-Object { $_.PackageName -like $app.Name } | ForEach-Object { Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $_.PackageName }
}
}
Conclusion
You can find the Github repo for AutoWinScripts here.
Also would like to thank Raphire for Win10Debloat, Chris Titus Tech for Windows Utility & Alekoff for Chocolatey-fresh-install for allowing me to learn, understand and implement my own attempt at setting up a windows machine using powershell.