Windows rebooted my machine again to install an update. No permission, no useful warning at the moment I actually needed it, just a fresh desktop where all of my open windows used to be.
Yes, I know using open windows as a reminder system is not exactly a formal productivity methodology. I also do not care. They are my windows, on my computer, and I want to decide when the machine gets restarted.
I do still want Windows Update. Security updates are important, and I am not trying to turn updates off. I simply want Windows to download and install what it needs, then wait until I choose Restart.
The approach
This script takes a belt-and-suspenders approach. It does four things:
- Sets the Windows Update policy to use scheduled installation behavior.
- Enables
NoAutoRebootWithLoggedOnUsers, which tells Windows not to automatically restart after an update while a user is signed in. - Disables Update Orchestrator scheduled tasks whose names contain
RebootorRestart. - Creates two SYSTEM scheduled tasks of its own so the settings are re-applied at startup and every 15 minutes.
The reason for the last step is simple: Windows Update has a habit of recreating or re-enabling things it believes it owns. Rather than trusting the setting to remain untouched forever, the script periodically checks again.
Important: this is intended for a personally managed Windows PC. If your computer is controlled by an employer, domain policy, Intune, WSUS or another management system, those policies can conflict with local settings. Microsoft also documents deadline-based restart behavior that can affect how update restart policies behave.
Download and install
Download the PowerShell script, then run it once. It will elevate itself and install the persistent copy and scheduled tasks.
Download NoWindowsAutoRestart.ps1
From PowerShell, assuming the file is in Downloads:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$env:USERPROFILE\Downloads\NoWindowsAutoRestart.ps1"
After installation, the script keeps its persistent copy here:
C:\ProgramData\NoWindowsAutoRestart\NoWindowsAutoRestart.ps1
What gets changed
The script writes the following Windows Update policy values under:
HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU
AUOptions = 4NoAutoRebootWithLoggedOnUsers = 1AlwaysAutoRebootAtScheduledTime = 0
Microsoft currently documents NoAutoRebootWithLoggedOnUsers for both Windows 10 and Windows 11 when automatic updates use option 4. The AlwaysAutoRebootAtScheduledTime setting is a legacy setting on Windows 11, but leaving it disabled also covers Windows 10 behavior.
The scheduled-task guard
The script creates these two tasks and runs them as SYSTEM:
- NoWindowsAutoRestart-Boot, runs every time Windows starts.
- NoWindowsAutoRestart-Guard, runs every 15 minutes.
Each pass also looks under \Microsoft\Windows\UpdateOrchestrator\ and disables tasks whose names contain Reboot or Restart.
This task-disabling portion is intentionally more aggressive than Microsoft's normal supported update-management policies. Task names can also change between Windows builds, which is why the script searches by name rather than assuming one exact task exists forever.
The complete script
#Requires -Version 5.1
param(
[switch]$EnforceOnly
)
$ErrorActionPreference = 'Stop'
$InstallDir = Join-Path $env:ProgramData 'NoWindowsAutoRestart'
$InstalledScript = Join-Path $InstallDir 'NoWindowsAutoRestart.ps1'
$BootTaskName = 'NoWindowsAutoRestart-Boot'
$GuardTaskName = 'NoWindowsAutoRestart-Guard'
$LogFile = Join-Path $InstallDir 'NoWindowsAutoRestart.log'
function Test-IsAdministrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Write-Log {
param([string]$Message)
if (-not (Test-Path $InstallDir)) {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "[$stamp] $Message"
}
function Set-Dword {
param(
[string]$Path,
[string]$Name,
[int]$Value
)
if (-not (Test-Path $Path)) {
New-Item -Path $Path -Force | Out-Null
}
New-ItemProperty `
-Path $Path `
-Name $Name `
-Value $Value `
-PropertyType DWord `
-Force | Out-Null
}
function Enforce-NoAutoRestart {
$wu = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
$au = Join-Path $wu 'AU'
# Microsoft-documented Windows Update restart controls.
Set-Dword -Path $au -Name 'AUOptions' -Value 4
Set-Dword -Path $au -Name 'NoAutoRebootWithLoggedOnUsers' -Value 1
Set-Dword -Path $au -Name 'AlwaysAutoRebootAtScheduledTime' -Value 0
# Extra protection: disable Update Orchestrator tasks whose names
# explicitly indicate reboot/restart. Task names vary by Windows build.
$rebootTasks = Get-ScheduledTask -ErrorAction SilentlyContinue |
Where-Object {
$_.TaskPath -eq '\Microsoft\Windows\UpdateOrchestrator\' -and
$_.TaskName -match '(?i)(reboot|restart)'
}
foreach ($task in $rebootTasks) {
try {
Disable-ScheduledTask -InputObject $task -ErrorAction Stop | Out-Null
Write-Log "Disabled scheduled task: $($task.TaskPath)$($task.TaskName)"
}
catch {
Write-Log "Could not disable task $($task.TaskPath)$($task.TaskName): $($_.Exception.Message)"
}
}
Write-Log 'Windows automatic-restart policy enforced.'
}
# First manual run elevates itself.
if (-not (Test-IsAdministrator)) {
if (-not $PSCommandPath) {
throw 'Save this script as a .ps1 file before running it.'
}
$arg = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
if ($EnforceOnly) {
$arg += ' -EnforceOnly'
}
Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList $arg
exit
}
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
# A normal manual run installs the persistent copy and Scheduled Tasks.
if (-not $EnforceOnly) {
$sourcePath = [IO.Path]::GetFullPath($PSCommandPath)
$destPath = [IO.Path]::GetFullPath($InstalledScript)
if ($sourcePath -ne $destPath) {
Copy-Item -LiteralPath $sourcePath -Destination $InstalledScript -Force
}
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$taskCommand = "`"$powershell`" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$InstalledScript`" -EnforceOnly"
# Re-apply at every boot.
& schtasks.exe /Create `
/TN $BootTaskName `
/SC ONSTART `
/RU SYSTEM `
/RL HIGHEST `
/TR $taskCommand `
/F | Out-Null
# Re-apply every 15 minutes in case Windows Update recreates/re-enables a reboot task.
& schtasks.exe /Create `
/TN $GuardTaskName `
/SC MINUTE `
/MO 15 `
/RU SYSTEM `
/RL HIGHEST `
/TR $taskCommand `
/F | Out-Null
Write-Log "Installed persistent script at $InstalledScript"
Write-Log "Created Scheduled Tasks: $BootTaskName and $GuardTaskName"
}
Enforce-NoAutoRestart
if (-not $EnforceOnly) {
Write-Host ''
Write-Host 'Installed and enforced.' -ForegroundColor Green
Write-Host "Persistent script: $InstalledScript"
Write-Host "Startup task: $BootTaskName"
Write-Host "Guard task: $GuardTaskName"
Write-Host "Log: $LogFile"
Write-Host ''
Write-Host 'Manual Restart and Shut down still work normally.'
}
Verify that it is installed
Check the scheduled tasks:
Get-ScheduledTask -TaskName "NoWindowsAutoRestart*"
Check the Windows Update policy values:
Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" |
Select-Object AUOptions, NoAutoRebootWithLoggedOnUsers, AlwaysAutoRebootAtScheduledTime
You should see:
AUOptions : 4 NoAutoRebootWithLoggedOnUsers : 1 AlwaysAutoRebootAtScheduledTime: 0
Logs
The script logs what it changes here:
C:\ProgramData\NoWindowsAutoRestart\NoWindowsAutoRestart.log
What this does not disable
Your normal Restart and Shut down commands still work. This is not intended to make the machine incapable of rebooting. The goal is to remove Windows Update's ability to casually make that decision for you.
There is one RDP wrinkle worth knowing: Microsoft says only an active Remote Desktop session counts as a signed-in user for the NoAutoRebootWithLoggedOnUsers policy. A disconnected RDP session does not receive the same protection from that policy.
Why not just use Active Hours?
Active Hours are useful if all you want is a quieter update schedule. They are not what I wanted. They still define a window in which Windows is allowed to decide that a restart is appropriate.
I want a much simpler rule: install the update, tell me a restart is required, then wait.