~/2025/09/10/powershell-intune-remediation-script-for-stuck-bitlocker-encryption.md

PowerShell: Intune – Remediation Script for Stuck BitLocker Encryption

---
author: 
date: 
read: 6 min
in:   [ps, scripts]
tags: [powershell, intune, windows]
---

$ grep -n '^#' post.md

BitLocker encryption doesn't fail loudly. It just stops moving. Get-BitLockerVolume reports EncryptionInProgress at 43% for days, or the volume sits in EncryptionPaused, or a fully encrypted drive shows ProtectionStatus Off long after whatever suspended it has finished. Nobody notices until a compliance report or a lost-laptop incident asks whether the drive was actually protected. There's no built-in alert for "encryption stalled," so this is an Intune Remediations pair: a Detect script that classifies the volume and remembers what it saw last time, and a Remediate script that applies the documented fix for each case.

The first version of this post restarted the BitLocker service and called Resume-BitLocker for every stall. That was wrong for the most common case. Resume-BitLocker restores protection on a volume that was suspended with Suspend-BitLocker, and has no effect on a volume that isn't suspended. It doesn't restart a paused conversion: that's manage-bde -resume. The scripts below treat those as separate conditions.

What the volume reportsWhat it meansFix in this script
VolumeStatus = EncryptionPausedConversion was paused partway throughmanage-bde -resume C:
VolumeStatus = FullyEncrypted, ProtectionStatus = Off, key protectors present, unchanged for longer than the grace periodProtectors are disabled (suspended)Resume-BitLocker -MountPoint C:
VolumeStatus = EncryptionInProgress, percentage unchanged for longer than the thresholdConversion is running but not progressingNone. Report the recent BitLocker-API errors and escalate

One trap to avoid: ProtectionStatus is Off on every volume that is still encrypting. Microsoft's definition of protection off includes "unencrypted, partially encrypted, or the volume's encryption key is available in the clear." Flagging "protection off with partial encryption" catches every healthy device in the middle of its first encryption, so neither script does.

Requirements

  • Deployed as an Intune remediation: Devices > Manage devices > Scripts and remediations, Create script package, upload both files. Run as SYSTEM (Run this script using the logged-on credentials = No), Enforce script signature check = No.
  • Licensing: device users need Windows Enterprise E3 or E5 (included in Microsoft 365 F3, E3 or E5), Windows Education A3 or A5, or Windows VDA per user.
  • Devices must be Microsoft Entra joined or hybrid joined, and either Intune-enrolled on Windows Enterprise, Professional or Education, or co-managed.
  • A BitLocker policy already targeted at the device (an endpoint security disk encryption profile or equivalent). This pair unsticks an encryption that policy started; it never calls Enable-BitLocker.
  • No extra modules: the BitLocker module and manage-bde.exe ship with Windows.

Parameters

Intune remediations don't pass arguments, so the scripts use variables at the top. The Detect script uses all four; the Remediate script only uses $MountPoint, so keep that value the same in both files.

NameTypeRequiredDescription
$MountPointStringNoVolume to check. Defaults to C:.
$StateRegPathStringNoRegistry key where the Detect script records the last status, percentage and when they last changed. Defaults to HKLM:\SOFTWARE\IntuneRemediations\BitLockerStuck.
$StallThresholdMinutesIntNoHow long EncryptionInProgress can sit at the same percentage before it counts as stalled. Defaults to 240.
$SuspendGraceHoursIntNoHow long a fully encrypted volume may stay suspended before it's flagged. Defaults to 24, so a suspension that is meant to clear itself after a restart gets time to do that.

Usage

Save this as Detect-StuckBitLocker.ps1 and upload it as the Detection script file. It exits 1 only when there's something for the Remediate script to look at; Intune runs the remediation only on exit code 1.

powershell
<#
.SYNOPSIS
    Detection script: flags a paused, suspended or stalled BitLocker volume.
.DESCRIPTION
    Reads Get-BitLockerVolume for the OS volume, compares it with the state
    recorded on the previous run, and records the new state. Exits 1 when the
    volume is EncryptionPaused, suspended past the grace period, or stuck at
    the same encryption percentage past the stall threshold. Exits 0 otherwise.
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2025-09-10)
    Requires: Runs as SYSTEM in an Intune remediation.
#>

$MountPoint = "C:"
$StateRegPath = "HKLM:\SOFTWARE\IntuneRemediations\BitLockerStuck"
$StallThresholdMinutes = 240
$SuspendGraceHours = 24

try {
    $volume = Get-BitLockerVolume -MountPoint $MountPoint -ErrorAction Stop
} catch {
    Write-Output "UNKNOWN: could not read BitLocker status for $MountPoint. $($_.Exception.Message)"
    exit 0
}

$status = [string]$volume.VolumeStatus
$protection = [string]$volume.ProtectionStatus
$percent = [int]$volume.EncryptionPercentage
$nowUtc = (Get-Date).ToUniversalTime()

if (-not (Test-Path -Path $StateRegPath)) {
    New-Item -Path $StateRegPath -Force | Out-Null
}

# Keep LastChangeUtc unless the status or percentage moved since the last run.
$state = Get-ItemProperty -Path $StateRegPath -ErrorAction SilentlyContinue
$lastChangeUtc = $nowUtc

if ($state -and $state.LastStatus -eq $status -and $state.LastPercent -eq $percent -and $state.LastChangeUtc) {
    $lastChangeUtc = [datetime]::Parse($state.LastChangeUtc).ToUniversalTime()
}

New-ItemProperty -Path $StateRegPath -Name "LastStatus" -Value $status -PropertyType String -Force | Out-Null
New-ItemProperty -Path $StateRegPath -Name "LastPercent" -Value $percent -PropertyType DWord -Force | Out-Null
New-ItemProperty -Path $StateRegPath -Name "LastChangeUtc" -Value $lastChangeUtc.ToString("o") -PropertyType String -Force | Out-Null

$since = $lastChangeUtc.ToString("yyyy-MM-ddTHH:mm:ssZ")
$minutesUnchanged = ($nowUtc - $lastChangeUtc).TotalMinutes

if ($status -eq "EncryptionPaused") {
    Write-Output "PAUSED: $MountPoint EncryptionPaused at $percent% since $since"
    exit 1
}

if ($status -eq "FullyEncrypted" -and $protection -eq "Off" -and @($volume.KeyProtector).Count -gt 0 -and $minutesUnchanged -ge ($SuspendGraceHours * 60)) {
    Write-Output "SUSPENDED: $MountPoint fully encrypted, protection off since $since"
    exit 1
}

if ($status -eq "EncryptionInProgress" -and $minutesUnchanged -ge $StallThresholdMinutes) {
    Write-Output "STALLED: $MountPoint EncryptionInProgress at $percent% since $since"
    exit 1
}

Write-Output "OK: $MountPoint $status, protection $protection, $percent%"
exit 0

Save the script in the next section as Repair-StuckBitLocker.ps1 and upload it as the Remediation script file. Assign the package to a device group with an Hourly schedule. To test on one machine first, run both from an elevated 64-bit PowerShell session, or use the Run remediation device action on a single device:

powershell
.\Detect-StuckBitLocker.ps1; if ($LASTEXITCODE -eq 1) { .\Repair-StuckBitLocker.ps1 }

What the detection and remediation output look like in the remediation's Device status report (the output columns can be added to the view or exported to CSV):

text
Detection   : PAUSED: C: EncryptionPaused at 43% since 2025-09-09T14:05:11Z
Remediation : C: EncryptionPaused at 43%. manage-bde -resume exit code 0. Status now EncryptionInProgress.

Detection   : STALLED: C: EncryptionInProgress at 61% since 2025-09-08T22:40:03Z
Remediation : C: stalled at 61%, no automatic fix. Recent BitLocker-API events: [2025-09-08 22:39] 854 Failed to enable Silent Encryption. WinRe is not configured. ...

Script

powershell
<#
.SYNOPSIS
    Remediation script: resumes paused BitLocker encryption or suspended
    protection, and reports why a stalled encryption can't be fixed.
.DESCRIPTION
    Runs after Detect-StuckBitLocker.ps1 exits 1. Re-reads the OS volume and
    acts on the documented cases only:
      - EncryptionPaused: runs manage-bde -resume, which resumes encryption
        or decryption after it has been paused.
      - FullyEncrypted with protection off and key protectors present: runs
        Resume-BitLocker, which restores protection on a suspended volume.
      - EncryptionInProgress with no progress: no supported fix exists, so it
        writes the most recent BitLocker-API Management errors and warnings to
        the output (trimmed to the 2,048-character remediation limit) and
        exits 1 so the device shows as failed in the report.
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2025-09-10)
    Requires: Runs as SYSTEM in an Intune remediation, paired with
              Detect-StuckBitLocker.ps1. BitLocker module, manage-bde.exe.
#>

$MountPoint = "C:"
$MaxOutput = 2000

# Write the output once, trimmed below Intune's 2,048-character limit, then exit.
function Complete-Remediation {
    param ([string]$Message, [int]$ExitCode)
    if ($Message.Length -gt $MaxOutput) {
        $Message = $Message.Substring(0, $MaxOutput)
    }
    Write-Output $Message
    exit $ExitCode
}

# Use the native manage-bde.exe even if Intune started a 32-bit PowerShell host.
$system32 = if ([Environment]::Is64BitOperatingSystem -and -not [Environment]::Is64BitProcess) { "Sysnative" } else { "System32" }
$manageBde = Join-Path -Path $env:SystemRoot -ChildPath "$system32\manage-bde.exe"

try {
    $volume = Get-BitLockerVolume -MountPoint $MountPoint -ErrorAction Stop
} catch {
    Complete-Remediation -Message "Could not read BitLocker status for $MountPoint. $($_.Exception.Message)" -ExitCode 1
}

$status = [string]$volume.VolumeStatus
$protection = [string]$volume.ProtectionStatus
$percent = [int]$volume.EncryptionPercentage
$summary = "${MountPoint} $status at $percent%."

# Case 1: conversion paused.
if ($status -eq "EncryptionPaused") {
    $result = & $manageBde -resume $MountPoint 2>&1
    $code = $LASTEXITCODE
    $after = Get-BitLockerVolume -MountPoint $MountPoint
    if ($code -eq 0) {
        Complete-Remediation -Message "$summary manage-bde -resume exit code 0. Status now $($after.VolumeStatus)." -ExitCode 0
    }
    Complete-Remediation -Message "$summary manage-bde -resume failed with exit code ${code}: $($result -join ' ')" -ExitCode 1
}

# Case 2: fully encrypted but protectors suspended.
if ($status -eq "FullyEncrypted" -and $protection -eq "Off" -and @($volume.KeyProtector).Count -gt 0) {
    try {
        Resume-BitLocker -MountPoint $MountPoint -ErrorAction Stop | Out-Null
        $after = Get-BitLockerVolume -MountPoint $MountPoint
        Complete-Remediation -Message "$summary Resume-BitLocker ran. ProtectionStatus now $($after.ProtectionStatus)." -ExitCode 0
    } catch {
        Complete-Remediation -Message "$summary Resume-BitLocker failed: $($_.Exception.Message)" -ExitCode 1
    }
}

# Case 3: stalled in progress. Report the evidence instead of guessing at a fix.
if ($status -eq "EncryptionInProgress") {
    $filter = @{
        LogName   = "Microsoft-Windows-BitLocker/BitLocker Management"
        Level     = 2, 3
        StartTime = (Get-Date).AddDays(-7)
    }
    $events = Get-WinEvent -FilterHashtable $filter -MaxEvents 5 -ErrorAction SilentlyContinue
    $lines = foreach ($logEntry in $events) {
        $firstLine = ($logEntry.Message -split "`r?`n")[0]
        "[{0:yyyy-MM-dd HH:mm}] {1} {2}" -f $logEntry.TimeCreated, $logEntry.Id, $firstLine
    }
    if (-not $lines) {
        $lines = @("No BitLocker-API errors or warnings in the last 7 days.")
    }
    Complete-Remediation -Message "${MountPoint} stalled at $percent%, no automatic fix. Recent BitLocker-API events: $($lines -join ' | ')" -ExitCode 1
}

# The state changed between detection and remediation (for example it finished).
Complete-Remediation -Message "$summary Nothing to do; state changed since detection." -ExitCode 0

Notes

  • Detection has to record state itself. Intune only runs the remediation when detection exits 1, so a design where only the Remediate script writes the "last seen" values never builds history on a device that looks healthy. Here the Detect script writes LastStatus, LastPercent and LastChangeUtc every run. A stall is flagged at the first run after the threshold passes.
  • Output is stable on purpose. Microsoft's guidance is to keep detection results stable and avoid values that change on every run. The detection line carries the time the state last changed, which stays the same across runs, not an elapsed-minutes counter.
  • Schedule and reporting lag. The Intune Management Extension retrieves remediation policy at restart, at user sign-in and every 8 hours. For recurring scripts the client reports within the first six days only when the result changes, then every seven days regardless. An hourly schedule catches a stall quickly on the device, but don't expect the admin center report to update every hour.
  • 64-bit host. I set Run script in 64-bit PowerShell to Yes for this pair, even though Microsoft's generic recommendation for custom packages is No. Both scripts depend on the BitLocker module and manage-bde.exe, and I'd rather they run in the native host. The remediation also resolves manage-bde.exe through Sysnative if it finds itself in a 32-bit process. Test on a pilot device either way.
  • Suspensions that should clear on their own. Suspend-BitLocker -RebootCount suspends protection for 1 to 15 restarts (the default is 1) and BitLocker restores it by itself afterwards; -RebootCount 0 suspends it until someone runs Resume-BitLocker. The $SuspendGraceHours window exists so the remediation only acts on suspensions that have outlived any planned restart. Don't set it shorter than your longest maintenance window.
  • When the remediation reports a stall, read the event IDs. The BitLocker-API Management log (Microsoft-Windows-BitLocker/BitLocker Management) is Microsoft's first stop for Intune BitLocker failures. The names don't line up, which trips people up: Event Viewer shows this log as Applications and Services Logs > Microsoft > Windows > BitLocker-API > Management, but its channel name, the one Get-WinEvent -LogName needs, is Microsoft-Windows-BitLocker/BitLocker Management (the file on disk is Microsoft-Windows-BitLocker%4BitLocker Management.evtx, with %4 standing in for the slash). Use that channel name, not the Event Viewer label, when you query it. Documented events include 853 (no compatible TPM found, or bootable media detected), 854 (WinRE not configured; check with reagentc.exe /info), 851 (contact the manufacturer for BIOS upgrade instructions, typically legacy BIOS instead of UEFI), and 846 with 778 (recovery information backup to Microsoft Entra ID failed and the volume was reverted to unprotected). Events 796 and 845 appear during normal operation. None of those is fixed by rerunning a script. Most of them need firmware, WinRE or policy changes.
  • Useful manual checks on an escalated device: manage-bde -status C: for conversion status and protectors, manage-bde -protectors -get C: to confirm the TPM protector includes PCR 7, Get-Tpm for TPM readiness, and the policy the device received under HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\BitLocker.
  • Scripts must be UTF-8. Upload the files rather than pasting into the browser, don't put reboot commands in either script, and keep output under 2,048 characters. Microsoft documents all three as Remediations requirements.

Source