~/2025/09/10/powershell-intune-remediation-script-for-stuck-bitlocker-encryption.md
PowerShell: Intune – Remediation Script for Stuck BitLocker Encryption
--- author: Tom Lasswell 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 reports | What it means | Fix in this script |
|---|---|---|
VolumeStatus = EncryptionPaused | Conversion was paused partway through | manage-bde -resume C: |
VolumeStatus = FullyEncrypted, ProtectionStatus = Off, key protectors present, unchanged for longer than the grace period | Protectors are disabled (suspended) | Resume-BitLocker -MountPoint C: |
VolumeStatus = EncryptionInProgress, percentage unchanged for longer than the threshold | Conversion is running but not progressing | None. 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
BitLockermodule andmanage-bde.exeship 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.
| Name | Type | Required | Description |
|---|---|---|---|
$MountPoint | String | No | Volume to check. Defaults to C:. |
$StateRegPath | String | No | Registry key where the Detect script records the last status, percentage and when they last changed. Defaults to HKLM:\SOFTWARE\IntuneRemediations\BitLockerStuck. |
$StallThresholdMinutes | Int | No | How long EncryptionInProgress can sit at the same percentage before it counts as stalled. Defaults to 240. |
$SuspendGraceHours | Int | No | How 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.
<#
.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:
.\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):
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
<#
.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 writesLastStatus,LastPercentandLastChangeUtcevery 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 resolvesmanage-bde.exethroughSysnativeif it finds itself in a 32-bit process. Test on a pilot device either way. - Suspensions that should clear on their own.
Suspend-BitLocker -RebootCountsuspends protection for 1 to 15 restarts (the default is 1) and BitLocker restores it by itself afterwards;-RebootCount 0suspends it until someone runsResume-BitLocker. The$SuspendGraceHourswindow 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 oneGet-WinEvent -LogNameneeds, isMicrosoft-Windows-BitLocker/BitLocker Management(the file on disk isMicrosoft-Windows-BitLocker%4BitLocker Management.evtx, with%4standing 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 withreagentc.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-Tpmfor TPM readiness, and the policy the device received underHKLM\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
- Use Remediations to detect and fix support issues: script requirements, licensing, schedules, client reporting cycle
- Resume-BitLocker, Suspend-BitLocker and Get-BitLockerVolume
- manage-bde resume and manage-bde status
- GetConversionStatus and GetProtectionStatus (Win32_EncryptableVolume): the status values behind
VolumeStatusandProtectionStatus - Enforcing BitLocker policies by using Intune: known issues: event IDs and resolutions
- Troubleshooting BitLocker policies from the client side: log locations and registry keys