SCRIPT LIBRARY · POWERSHELL
DISM, Then SFC: Repairing Windows in the Right Order
Take a restore point, repair the component store with DISM, then run SFC, and get one object back that says what each step found instead of three screens of scrolling text.
- What it does
- Creates a System Restore point, checks and repairs the Windows component store with Repair-WindowsImage (DISM RestoreHealth), runs sfc /scannow and parses its verdict, checks for a pending restart, and returns a single summary object. -ScanOnly checks without repairing.
- Requires
- Windows PowerShell 5.1 or PowerShell 7+
- The Dism module (built into Windows)
- Internet access to Windows Update, or a -Source that matches the installed build
- Permissions
- Local administrator, in an elevated PowerShell.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked, then run in PowerShell 7.4 with mocked CIM and Repair-WindowsImage calls and a fake sfc that writes UTF-16 output, covering a created restore point, one skipped by the 24-hour limit, a DISM "source not found" failure, a repaired SFC result and -WhatIf. The real DISM and SFC weren't run here
Part 9 of the thread Spring cleaning for Windows PCs
"Run SFC" is the "have you tried turning it off and on again" of Windows repair, and half the time it comes back with "found corrupt files but was unable to fix some of them". That's usually because it was run in the wrong order. SFC repairs system files by copying good versions out of the component store in WinSxS. If the store itself is damaged, SFC has nothing good to copy from.
DISM's /RestoreHealth fixes the store, downloading clean copies from Windows Update. So the order is DISM first, then SFC. My old all-in-one cleanup script had this step and ran them the other way round, in among nineteen other things. Most of that script's disk-space jobs now live in the disk cleanup script and its neighbours in this thread. The repair part deserved its own script.
This one takes a restore point first, runs them in the right order, and then reads SFC's output for you (which is harder than it sounds, because SFC writes UTF-16 and PowerShell reads it with a NUL between every letter). You get one object back: what the store looked like, what DISM did, what SFC said, and whether a restart is waiting.
<#
.SYNOPSIS
Creates a restore point, repairs the Windows component store with DISM, then runs SFC, and sums it all up in one object.
.DESCRIPTION
The order matters. SFC repairs system files from the component store (WinSxS), so if the store
itself is damaged, SFC "fixes" files from broken copies or gives up. This script runs them the
right way round:
1. A System Restore point, so there's a way back (skip with -SkipRestorePoint).
2. DISM CheckHealth (quick, read-only) and then RestoreHealth, through Repair-WindowsImage.
3. sfc /scannow, with its output captured, cleaned up and turned into a plain result.
4. A check for a pending restart.
-ScanOnly swaps the repairs for DISM ScanHealth and sfc /verifyonly. -WhatIf runs only the
read-only CheckHealth and the pending-restart check.
.PARAMETER ScanOnly
Look but don't fix: DISM ScanHealth and sfc /verifyonly.
.PARAMETER Source
A repair source for DISM when Windows Update can't supply one, such as a mounted install.wim
(wim:E:\sources\install.wim:1) or a folder from the same Windows build.
.PARAMETER LimitAccess
With -Source, stop DISM from falling back to Windows Update or WSUS.
.PARAMETER SkipRestorePoint
Don't create a restore point first.
.PARAMETER EnableSystemRestore
If System Restore is off on the system drive, turn it on so the restore point can be made.
.PARAMETER LogFolder
Where the SFC output is saved. Default: %ProgramData%\WindowsHealth.
.EXAMPLE
.\Repair-WindowsHealth.ps1
.EXAMPLE
.\Repair-WindowsHealth.ps1 -Source wim:E:\sources\install.wim:1 -LimitAccess
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[switch]$ScanOnly,
[string]$Source,
[switch]$LimitAccess,
[switch]$SkipRestorePoint,
[switch]$EnableSystemRestore,
[string]$LogFolder = (Join-Path $env:ProgramData 'WindowsHealth')
)
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'DISM and SFC need an elevated PowerShell. Right-click, Run as administrator, and try again.'
}
$LogFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LogFolder)
$started = Get-Date
$summary = [ordered]@{
ComputerName = $env:COMPUTERNAME
RestorePoint = 'Skipped'
StoreBefore = $null
Dism = 'NotRun'
Sfc = 'NotRun'
SfcDetail = $null
RestartPending = $false
Duration = $null
SfcLog = $null
CbsLog = Join-Path $env:SystemRoot 'Logs\CBS\CBS.log'
DismLog = Join-Path $env:SystemRoot 'Logs\DISM\dism.log'
}
# 1. Restore point. Checkpoint-Computer only exists in Windows PowerShell 5.1, so go straight to the WMI class.
if (-not $SkipRestorePoint -and -not $ScanOnly -and $PSCmdlet.ShouldProcess($env:SystemDrive, 'Create a System Restore point')) {
try {
$drive = "$env:SystemDrive\"
if ($EnableSystemRestore) {
$on = Invoke-CimMethod -Namespace root/default -ClassName SystemRestore -MethodName Enable -Arguments @{ Drive = $drive } -ErrorAction Stop
if ($on.ReturnValue -ne 0) { Write-Warning "Enabling System Restore returned $($on.ReturnValue)." }
}
$before = @(Get-CimInstance -Namespace root/default -ClassName SystemRestore -ErrorAction SilentlyContinue).Count
$rp = Invoke-CimMethod -Namespace root/default -ClassName SystemRestore -MethodName CreateRestorePoint -ErrorAction Stop -Arguments @{
Description = "Before DISM and SFC repair $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
RestorePointType = [uint32]12 # MODIFY_SETTINGS
EventType = [uint32]100 # BEGIN_SYSTEM_CHANGE
}
$after = @(Get-CimInstance -Namespace root/default -ClassName SystemRestore -ErrorAction SilentlyContinue).Count
$summary.RestorePoint = switch ($true) {
($rp.ReturnValue -ne 0) { "Failed: error $($rp.ReturnValue). Is System Restore on? Try -EnableSystemRestore"; break }
($after -gt $before) { 'Created'; break }
default { 'NotCreated: Windows only allows one every 24 hours by default' }
}
}
catch {
$summary.RestorePoint = "Failed: $($_.Exception.Message)"
}
if ($summary.RestorePoint -notin 'Created') { Write-Warning "Restore point: $($summary.RestorePoint). Carrying on." }
}
# 2. DISM. CheckHealth is read-only and quick, so it runs even under -WhatIf.
try {
$summary.StoreBefore = [string](Repair-WindowsImage -Online -CheckHealth -ErrorAction Stop).ImageHealthState
}
catch { $summary.StoreBefore = "Error: $($_.Exception.Message)" }
$dismParams = @{ Online = $true; ErrorAction = 'Stop' }
if ($ScanOnly) { $dismParams.ScanHealth = $true; $dismAction = 'DISM ScanHealth (read-only, 5-15 minutes)' }
else {
$dismParams.RestoreHealth = $true; $dismParams.NoRestart = $true; $dismAction = 'DISM RestoreHealth (10-30 minutes)'
if ($Source) { $dismParams.Source = $Source }
if ($LimitAccess) { $dismParams.LimitAccess = $true }
}
if ($PSCmdlet.ShouldProcess('Windows component store', $dismAction)) {
try {
Write-Verbose "Running $dismAction."
$dism = Repair-WindowsImage @dismParams
$summary.Dism = [string]$dism.ImageHealthState # Healthy, Repairable or NonRepairable
if ($dism.RestartNeeded) { $summary.RestartPending = $true }
}
catch {
$msg = $_.Exception.Message
$summary.Dism = if ($msg -match '0x800f081f|source files could not be found') { 'SourceNotFound: give it -Source with a matching install.wim' } else { "Failed: $msg" }
}
}
# 3. SFC. Its output is UTF-16, which PowerShell reads as text with a NUL between every letter; strip those before matching.
$sfcArg = if ($ScanOnly) { '/verifyonly' } else { '/scannow' }
if ($PSCmdlet.ShouldProcess('Protected system files', "sfc $sfcArg (10-20 minutes)")) {
try {
if ($summary.Dism -like 'Failed*' -or $summary.Dism -like 'SourceNotFound*' -or $summary.Dism -eq 'NonRepairable') {
Write-Warning 'DISM did not finish cleanly, so SFC may not be able to repair everything.'
}
$null = New-Item -ItemType Directory -Path $LogFolder -Force -ErrorAction Stop
$raw = & "$env:SystemRoot\System32\sfc.exe" $sfcArg 2>&1
$text = (($raw | Out-String) -replace "`0", '') -replace '\r?\n\s*\r?\n', "`n"
$summary.SfcLog = Join-Path $LogFolder ('sfc-{0:yyyyMMdd-HHmmss}.txt' -f (Get-Date))
Set-Content -LiteralPath $summary.SfcLog -Value $text -Encoding utf8
$summary.Sfc = switch -Regex ($text) {
'did not find any integrity violations' { 'Clean'; break }
'found corrupt files and successfully repaired' { 'Repaired'; break }
'found corrupt files but was unable to fix' { 'NotAllRepaired'; break }
'found integrity violations' { 'ViolationsFound'; break }
'repair pending which requires reboot' { $summary.RestartPending = $true; 'RestartFirst'; break }
'could not perform the requested operation' { 'CouldNotRun'; break }
default { "Unrecognized (exit code $LASTEXITCODE)" }
}
$summary.SfcDetail = ($text -split "`n" | Where-Object { $_ -match 'Windows Resource Protection|There is a system repair' } | ForEach-Object Trim) -join ' '
}
catch {
$summary.Sfc = "Failed: $($_.Exception.Message)"
}
}
# 4. Anything still waiting on a restart?
$pendingKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
)
if ($pendingKeys | Where-Object { Test-Path -LiteralPath $_ }) { $summary.RestartPending = $true }
if ((Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name PendingFileRenameOperations -ErrorAction SilentlyContinue).PendingFileRenameOperations) { $summary.RestartPending = $true }
$summary.Duration = (Get-Date) - $started
[pscustomobject]$summary
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-ScanOnly | switch | — | Check without repairing. Runs DISM ScanHealth and sfc /verifyonly, and skips the restore point. |
-Source | string | — | A repair source for DISM, like wim:E:\sources\install.wim:1 from matching install media. Needed when Windows Update isn't reachable. |
-LimitAccess | switch | — | With -Source, don't let DISM fall back to Windows Update or WSUS. |
-SkipRestorePoint | switch | — | Don't take a restore point first. |
-EnableSystemRestore | switch | — | Turn System Restore on for the system drive if it's off, so the restore point can be created. |
-LogFolder | string | %ProgramData%\WindowsHealth | Where the cleaned-up SFC output is saved. |
Run it
The standard repair, from an elevated prompt.
.\Repair-WindowsHealth.ps1Just look. No restore point, no repairs.
.\Repair-WindowsHealth.ps1 -ScanOnlyRepair from mounted install media on a machine that can't reach Windows Update.
.\Repair-WindowsHealth.ps1 -Source wim:E:\sources\install.wim:1 -LimitAccessRun it on a remote PC and keep the result.
$code = Get-Content .\Repair-WindowsHealth.ps1 -Raw; $r = Invoke-Command -ComputerName PC-0142 -ScriptBlock { & ([scriptblock]::Create($using:code)) -SkipRestorePoint }; $r | Select-Object PSComputerName, StoreBefore, Dism, Sfc, RestartPendingWhat you'll see
ComputerName : PC-0142
RestorePoint : Created
StoreBefore : Repairable
Dism : Healthy
Sfc : Repaired
SfcDetail : Windows Resource Protection found corrupt files and successfully repaired them.
RestartPending : True
Duration : 00:31:47.2210934
SfcLog : C:\ProgramData\WindowsHealth\sfc-20260929-101512.txt
CbsLog : C:\WINDOWS\Logs\CBS\CBS.log
DismLog : C:\WINDOWS\Logs\DISM\dism.log
How it works
- Check elevation. DISM and SFC both need admin rights, so the script stops at once with a clear message if it hasn't got them.
- Take a restore point.
Checkpoint-Computerdoesn't exist in PowerShell 7, so it calls theSystemRestoreWMI class directly withInvoke-CimMethod. It counts restore points before and after, because Windows reports success even when its once-a-day limit means nothing was created. A failure here is a warning, not a stop. - Check and repair the store.
Repair-WindowsImage -CheckHealthis quick and read-only, so it runs even under-WhatIfand gives a before picture. Then-RestoreHealthdoes the real work and returns Healthy, Repairable or NonRepairable, with no text to parse. The classic "source files could not be found" error is recognized and turned into a hint. - Run SFC and read its verdict.
sfc /scannowruns with its output captured. The NUL characters are stripped, the result is saved to-LogFolder, and the final "Windows Resource Protection..." line is matched to Clean, Repaired, NotAllRepaired, RestartFirst or CouldNotRun. - Check for a pending restart. The Component Based Servicing and Windows Update reboot keys plus pending file renames tell you whether a restart is needed to finish the job.
If DISM keeps failing because Windows Update is switched off on the machine, find and clear every registry block on Windows Update and try again.
Take it further
- Pull the SFC details. The files SFC fixed or couldn't fix are the
[SR]lines in CBS.log:Select-String -Path $env:SystemRoot\Logs\CBS\CBS.log -Pattern '\[SR\]' | Select-Object -Last 50. - Clean up the store afterwards. Once everything's healthy,
Repair-WindowsImage -Online -StartComponentCleanuptrims superseded components from WinSxS. Add-ResetBaseonly if you're sure you won't uninstall any current updates. - Run it across a fleet. Send the summary objects from many machines to one CSV, then sort by
Sfcto see which ones need a closer look or a rebuild.
Things that'll trip you up
- One restore point a day. Windows silently skips a new restore point if one was made in the last 24 hours. The call still reports success, so the script counts restore points before and after and tells you NotCreated when that happens. If you need a fresh one, make it by hand in System Protection first.
- 0x800f081f means DISM had nowhere to get files from. It needs Windows Update, or a -Source of the exact same build. That fails on machines where Windows Update is blocked by policy or WSUS doesn't have the files. Mount an ISO of the same version and pass its install.wim, with the right index, as -Source.
- It's slow, and it looks stuck. DISM can sit at 62.3% for ten minutes, and SFC can take twenty on a slow disk. Neither is hung. Run it when the machine can be left alone, and on a laptop, plug it in.
- The SFC result is parsed from English text. SFC has no useful exit codes, so the script matches its final message. On a non-English Windows the Sfc field will say Unrecognized. The full output is still saved to the log, and the DISM part is language-neutral either way.