Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

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.

AT A GLANCERepair-WindowsHealth.ps1
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.

Repair-WindowsHealth.ps1Download
<#
.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

ParameterTypeDefaultWhat it's for
-ScanOnlyswitch—Check without repairing. Runs DISM ScanHealth and sfc /verifyonly, and skips the restore point.
-Sourcestring—A repair source for DISM, like wim:E:\sources\install.wim:1 from matching install media. Needed when Windows Update isn't reachable.
-LimitAccessswitch—With -Source, don't let DISM fall back to Windows Update or WSUS.
-SkipRestorePointswitch—Don't take a restore point first.
-EnableSystemRestoreswitch—Turn System Restore on for the system drive if it's off, so the restore point can be created.
-LogFolderstring%ProgramData%\WindowsHealthWhere the cleaned-up SFC output is saved.

Run it

The standard repair, from an elevated prompt.

.\Repair-WindowsHealth.ps1

Just look. No restore point, no repairs.

.\Repair-WindowsHealth.ps1 -ScanOnly

Repair from mounted install media on a machine that can't reach Windows Update.

.\Repair-WindowsHealth.ps1 -Source wim:E:\sources\install.wim:1 -LimitAccess

Run 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, RestartPending

What you'll see

Example outputvalues are illustrative
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

  1. 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.
  2. Take a restore point. Checkpoint-Computer doesn't exist in PowerShell 7, so it calls the SystemRestore WMI class directly with Invoke-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.
  3. Check and repair the store. Repair-WindowsImage -CheckHealth is quick and read-only, so it runs even under -WhatIf and gives a before picture. Then -RestoreHealth does 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.
  4. Run SFC and read its verdict. sfc /scannow runs 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.
  5. 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 -StartComponentCleanup trims superseded components from WinSxS. Add -ResetBase only 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 Sfc to 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.