How to Check Disk Health with S.M.A.R.T. Data in Batch Script
A failing hard drive is the most common cause of catastrophic data loss. Before a drive fails completely, it often shows warning signs through its S.M.A.R.T. (Self-Monitoring, Analysis and Reporting Technology) data. Windows can read a simplified version of this health status, which reports the drive's overall condition as "Healthy," "Warning," or "Unhealthy." A Batch script can use this to build an early warning system that alerts you to replace a drive before it dies, saving you from an unrecoverable data disaster.
This guide will explain how to monitor disk health via the command line.
Understanding Disk Health Status Values
| Status | Meaning | Action |
|---|---|---|
| Healthy / OK | S.M.A.R.T. reports no issues | No action needed: continue monitoring |
| Warning / Pred Fail | S.M.A.R.T. predicts imminent failure | Back up immediately. Plan replacement. |
| Unhealthy / Degraded | Drive is experiencing errors | Back up immediately. Replace urgently. |
| Unknown | Status cannot be determined | Investigate: may be a USB enclosure or driver issue |
If any drive reports "Pred Fail", "Warning", or "Unhealthy", treat it as a red alert. Back up all data immediately. The drive could fail within hours or weeks: there is no way to predict the exact moment. Every minute of delay increases the risk of permanent data loss.
Method 1: Comprehensive Disk Health Report
This method checks all physical disks and displays health status, model, size, bus type, and media type, providing the complete picture needed for a health assessment.
Implementation
@echo off
setlocal
echo [INFO] Checking physical disk health...
echo --------------------------------------------------
powershell -NoProfile -Command ^
"$disks = Get-PhysicalDisk -ErrorAction SilentlyContinue;" ^
"if (-not $disks) {" ^
" Write-Host 'Could not retrieve disk information.';" ^
" Write-Host 'Trying alternative method...';" ^
" Get-CimInstance Win32_DiskDrive |" ^
" ForEach-Object {" ^
" [PSCustomObject]@{" ^
" Disk = $_.Index;" ^
" Model = $_.Model;" ^
" 'Size (GB)' = [math]::Round($_.Size / 1GB, 1);" ^
" Interface = $_.InterfaceType;" ^
" Status = $_.Status" ^
" }" ^
" } | Format-Table -AutoSize;" ^
" exit 0" ^
"};" ^
"$disks | ForEach-Object {" ^
" [PSCustomObject]@{" ^
" Disk = $_.DeviceId;" ^
" Model = $_.FriendlyName;" ^
" 'Size (GB)' = [math]::Round($_.Size / 1GB, 1);" ^
" Media = [string]$_.MediaType;" ^
" Bus = [string]$_.BusType;" ^
" Health = [string]$_.HealthStatus;" ^
" Status = [string]$_.OperationalStatus" ^
" }" ^
"} | Format-Table -AutoSize"
echo --------------------------------------------------
endlocal
exit /b 0
Sample output:
Disk Model Size (GB) Media Bus Health Status
---- ----- --------- ----- --- ------ ------
0 Samsung SSD 970 EVO Plus 500GB 465.8 SSD NVMe Healthy OK
1 WDC WD40EFAX-68JH4N1 3726.0 HDD SATA Healthy OK
2 Kingston DataTraveler 3.0 28.9 SSD USB Healthy OK
Why Get-PhysicalDisk instead of wmic diskdrive:
wmicis deprecated since Windows 10 21H1 and produces\r-corrupted output.wmic diskdrive get statusonly returns a simple "OK" or "Pred Fail" string.Get-PhysicalDiskreturnsHealthStatus(Healthy, Warning, Unhealthy) ANDOperationalStatus(OK, Degraded, Error), two independent indicators.Get-PhysicalDiskalso reportsMediaType(SSD vs. HDD) andBusType(NVMe, SATA, USB, SAS), which are essential for interpreting the health data and planning replacements.Get-PhysicalDiskworks with NVMe drives, whichwmic diskdriveoften cannot query for health status.
Fallback to Win32_DiskDrive:
On some systems (particularly older Windows Server versions or systems with specific storage controllers), Get-PhysicalDisk may not be available. The script falls back to Get-CimInstance Win32_DiskDrive, which provides basic health information through the Status property.
Method 2: Health Alert with Automatic Detection
This method scans all drives and generates a clear pass/fail result, suitable for scheduled tasks and monitoring integration.
@echo off
setlocal EnableDelayedExpansion
set "LogFile=%~dp0disk_health.log"
echo [INFO] Scanning for disk health anomalies...
:: Initialize variables
set "TotalDisks=0"
set "HealthyDisks=0"
set "FailingDisks=0"
set "FailingNames="
set "PSResult=0"
:: Build PowerShell command with proper escaping
set "PSCmd=$disks = Get-PhysicalDisk -ErrorAction SilentlyContinue;"
set "PSCmd=!PSCmd! if (-not $disks -or $disks.Count -eq 0) {"
set "PSCmd=!PSCmd! $wmi = Get-CimInstance Win32_DiskDrive;"
set "PSCmd=!PSCmd! $unhealthy = @($wmi) | Where-Object { $_.Status -ne 'OK' };"
set "PSCmd=!PSCmd! $total = @($wmi).Count;"
set "PSCmd=!PSCmd! $healthyCount = @(@($wmi) | Where-Object { $_.Status -eq 'OK' }).Count;"
set "PSCmd=!PSCmd! $failCount = @($unhealthy).Count;"
set "PSCmd=!PSCmd! $failNames = ($unhealthy | ForEach-Object { $_.Model }) -join '; ';"
set "PSCmd=!PSCmd! Write-Output ('{0}~{1}~{2}~{3}' -f $total, $healthyCount, $failCount, $failNames);"
set "PSCmd=!PSCmd! if ($failCount -gt 0) { exit 1 } else { exit 0 }"
set "PSCmd=!PSCmd! };"
set "PSCmd=!PSCmd! $unhealthy = @($disks) | Where-Object { $_.HealthStatus -ne 'Healthy' };"
set "PSCmd=!PSCmd! $total = @($disks).Count;"
set "PSCmd=!PSCmd! $healthyCount = @(@($disks) | Where-Object { $_.HealthStatus -eq 'Healthy' }).Count;"
set "PSCmd=!PSCmd! $failCount = @($unhealthy).Count;"
set "PSCmd=!PSCmd! $failNames = ($unhealthy | ForEach-Object { \"$($_.FriendlyName) [$($_.HealthStatus)]\" }) -join '; ';"
set "PSCmd=!PSCmd! Write-Output ('{0}~{1}~{2}~{3}' -f $total, $healthyCount, $failCount, $failNames);"
set "PSCmd=!PSCmd! if ($failCount -gt 0) { exit 1 } else { exit 0 }"
:: Run PowerShell and capture output (using ~ as delimiter to avoid pipe issues)
for /f "tokens=1-4 delims=~" %%a in (
'powershell -NoProfile -ExecutionPolicy Bypass -Command "!PSCmd!"'
) do (
set "TotalDisks=%%a"
set "HealthyDisks=%%b"
set "FailingDisks=%%c"
set "FailingNames=%%d"
)
:: Capture exit code
set "PSResult=!errorlevel!"
:: Generate timestamp
for /f "delims=" %%t in (
'powershell -NoProfile -Command "Get-Date -Format 'yyyy-MM-dd HH:mm:ss'"'
) do set "Timestamp=%%t"
:: Display results
echo.
echo [!Timestamp!] Disks: !TotalDisks! total, !HealthyDisks! healthy, !FailingDisks! failing
if "!TotalDisks!"=="0" (
echo [WARNING] No disks detected. Check WMI/Storage services.
endlocal
exit /b 1
)
if !PSResult! equ 0 (
echo [OK] All !TotalDisks! disk^(s^) are healthy.
echo [!Timestamp!] OK: All !TotalDisks! disks healthy on %COMPUTERNAME% >> "%LogFile%"
) else (
echo.
echo [CRITICAL] Failing disk^(s^) detected: !FailingNames!
echo.
echo *** BACK UP ALL DATA IMMEDIATELY ***
echo *** PLAN DRIVE REPLACEMENT ***
echo.
echo [!Timestamp!] ALERT: !FailingDisks! failing disk^(s^) on %COMPUTERNAME%: !FailingNames! >> "%LogFile%"
:: Write to Event Log
eventcreate /T ERROR /ID 910 /L APPLICATION /SO "DiskHealthMonitor" /D "DISK HEALTH ALERT: !FailingDisks! disk(s) failing on %COMPUTERNAME%: !FailingNames!" >nul 2>&1
)
endlocal
exit /b %PSResult%
Exit codes for automation:
| Exit Code | Meaning |
|---|---|
0 | All disks are healthy |
1 | One or more disks are failing or degraded |
Scheduling for continuous monitoring:
Run this script as a scheduled task daily or weekly. The exit code allows Task Scheduler's "on failure" action to trigger an email, a ticket, or an alert in your monitoring system.
Method 3: Detailed Drive Report for Asset Management
Generates a comprehensive report including model, serial number, firmware version, health status, and S.M.A.R.T. data availability, useful for hardware inventory and warranty tracking.
@echo off
setlocal
for /f "delims=" %%t in (
'powershell -NoProfile -Command "Get-Date -Format ''yyyyMMdd_HHmmss''"'
) do set "Stamp=%%t"
set "ReportFile=%~dp0DiskHealth_%COMPUTERNAME%_%Stamp%.txt"
echo [INFO] Generating detailed disk health report...
:: Report header
(
echo ==================================================
echo DISK HEALTH REPORT
echo ==================================================
) > "%ReportFile%"
for /f "delims=" %%t in (
'powershell -NoProfile -Command "Get-Date -Format ''yyyy-MM-dd HH:mm:ss''"'
) do echo Generated: %%t >> "%ReportFile%"
echo Computer: %COMPUTERNAME% >> "%ReportFile%"
echo. >> "%ReportFile%"
:: Physical disk details
echo --- Physical Disk Details --- >> "%ReportFile%"
echo. >> "%ReportFile%"
powershell -NoProfile -Command ^
"$disks = Get-PhysicalDisk -ErrorAction SilentlyContinue;" ^
"if (-not $disks) {" ^
" Write-Output ' Get-PhysicalDisk not available. Using Win32_DiskDrive.';" ^
" Get-CimInstance Win32_DiskDrive | ForEach-Object {" ^
" Write-Output \" Disk $($_.Index): $($_.Model)\";" ^
" Write-Output \" Serial: $($_.SerialNumber)\";" ^
" Write-Output \" Firmware: $($_.FirmwareRevision)\";" ^
" Write-Output \" Size: $([math]::Round($_.Size / 1GB, 1)) GB\";" ^
" Write-Output \" Interface: $($_.InterfaceType)\";" ^
" Write-Output \" Status: $($_.Status)\";" ^
" Write-Output ''" ^
" };" ^
" exit 0" ^
"};" ^
"foreach ($d in $disks) {" ^
" Write-Output \" Disk $($d.DeviceId): $($d.FriendlyName)\";" ^
" Write-Output \" Serial: $(if ($d.SerialNumber) { $d.SerialNumber.Trim() } else { 'N/A' })\";" ^
" Write-Output \" Firmware: $($d.FirmwareVersion)\";" ^
" Write-Output \" Size: $([math]::Round($d.Size / 1GB, 1)) GB\";" ^
" Write-Output \" Media Type: $($d.MediaType)\";" ^
" Write-Output \" Bus Type: $($d.BusType)\";" ^
" Write-Output \" Health: $($d.HealthStatus)\";" ^
" Write-Output \" Op Status: $($d.OperationalStatus)\";" ^
" if ($d.MediaType -eq 'SSD') {" ^
" $wear = Get-StorageReliabilityCounter -PhysicalDisk $d -ErrorAction SilentlyContinue;" ^
" if ($wear) {" ^
" Write-Output \" Wear Level: $($wear.Wear)%%\";" ^
" Write-Output \" Read Errors: $($wear.ReadErrorsTotal)\";" ^
" Write-Output \" Write Errors: $($wear.WriteErrorsTotal)\";" ^
" Write-Output \" Power On: $($wear.PowerOnHours) hours\"" ^
" }" ^
" };" ^
" Write-Output ''" ^
"}" >> "%ReportFile%"
echo. >> "%ReportFile%"
echo ================================================== >> "%ReportFile%"
echo [OK] Report saved to: %ReportFile%
endlocal
exit /b 0
SSD-specific metrics:
For SSDs, the report includes data from Get-StorageReliabilityCounter:
| Metric | What It Means | Concern Level |
|---|---|---|
| Wear Level | Percentage of flash cell write endurance consumed | Above 90%: plan replacement |
| Read Errors | Total read errors since manufacture | Rising count indicates degradation |
| Write Errors | Total write errors since manufacture | Rising count indicates degradation |
| Power On Hours | Total hours the drive has been powered | Context for wear and warranty |
Get-StorageReliabilityCounter is available on Windows 10/11 and Server 2016+ but may not return data for all SSDs, particularly those connected via USB enclosures or using non-standard controllers. The script handles this gracefully by checking for null results.
Method 4: Fleet-Wide Disk Health CSV
For monitoring disk health across multiple machines, export status data to a shared CSV.
@echo off
setlocal
set "CSVFile=\\Server\Audit\disk_health.csv"
if not exist "%CSVFile%" (
echo "Timestamp","Computer","Disk","Model","SizeGB","MediaType","BusType","Health","OpStatus","Serial" > "%CSVFile%" 2>nul
)
powershell -NoProfile -Command ^
"$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss';" ^
"$disks = Get-PhysicalDisk -ErrorAction SilentlyContinue;" ^
"if (-not $disks) {" ^
" Get-CimInstance Win32_DiskDrive | ForEach-Object {" ^
" $sizeGB = [math]::Round($_.Size / 1GB, 1);" ^
" $sn = if ($_.SerialNumber) { $_.SerialNumber.Trim() } else { 'N/A' };" ^
" Write-Output ('\"' + $ts + '\",\"' + $env:COMPUTERNAME + '\",\"' + $_.Index + '\",\"' + $_.Model + '\",\"' + $sizeGB + '\",\"N/A\",\"' + $_.InterfaceType + '\",\"' + $_.Status + '\",\"OK\",\"' + $sn + '\"')" ^
" }" ^
"} else {" ^
" foreach ($d in $disks) {" ^
" $sizeGB = [math]::Round($d.Size / 1GB, 1);" ^
" $sn = if ($d.SerialNumber) { $d.SerialNumber.Trim() } else { 'N/A' };" ^
" Write-Output ('\"' + $ts + '\",\"' + $env:COMPUTERNAME + '\",\"' + $d.DeviceId + '\",\"' + $d.FriendlyName + '\",\"' + $sizeGB + '\",\"' + $d.MediaType + '\",\"' + $d.BusType + '\",\"' + $d.HealthStatus + '\",\"' + $d.OperationalStatus + '\",\"' + $sn + '\"')" ^
" }" ^
"}" >> "%CSVFile%" 2>nul
echo [OK] Disk health data exported for %COMPUTERNAME%.
endlocal
exit /b 0
What to look for in the fleet CSV:
- Health ≠ Healthy: Any disk not reporting "Healthy" needs immediate attention.
- Status changes over time: A disk that was "Healthy" last week but is "Warning" this week is actively degrading.
- SSD wear levels: If you include SSD wear data, drives approaching 90%+ wear should be scheduled for replacement during the next maintenance window.
- Old HDDs with high power-on hours: Mechanical drives over 40,000 hours (approximately 4.5 years of continuous operation) are at elevated failure risk.
How to Avoid Common Errors
Wrong Way: Using wmic diskdrive get status as the Primary Check
:: DEPRECATED and provides minimal information
wmic diskdrive get model, status
wmic is deprecated, produces \r-corrupted output, and returns only a simple "OK" / "Pred Fail" status. It cannot report HealthStatus and OperationalStatus independently, cannot identify media type (SSD vs. HDD), and often fails to query NVMe drives for health data.
Correct Way: Use Get-PhysicalDisk which returns comprehensive health data for all drive types including NVMe.
"OK" does not mean the drive is perfect. The simplified WMIC/WMI status is a summary of the S.M.A.R.T. prediction algorithm. Individual S.M.A.R.T. attributes (like Reallocated Sector Count or Pending Sector Count) can be degraded while the overall status still shows "OK." For deep S.M.A.R.T. analysis, use dedicated tools like CrystalDiskInfo, smartctl (from smartmontools), or the drive manufacturer's diagnostic utility.
Problem: NVMe SSDs Not Showing Health Data
Some NVMe SSDs report health through a different path that Win32_DiskDrive cannot access. The status may show as "Unknown" even though the drive is healthy.
Solution: Get-PhysicalDisk (used in all methods) correctly queries NVMe drives via the Windows Storage Subsystem, which supports modern NVMe health reporting.
Problem: USB Drives Showing "Unknown" Health
USB-connected drives often report "Unknown" health status because the USB bridge chip doesn't pass through S.M.A.R.T. data from the actual drive controller.
Solution: This is a hardware limitation, not a software issue. For reliable health monitoring of external drives, connect them directly via SATA or NVMe when possible. The scripts handle "Unknown" status without failing.
Problem: Virtual Machine Disks
Virtual disks (in Hyper-V, VMware, VirtualBox) report health of the virtual disk, not the underlying physical storage. They typically always show "Healthy."
Solution: Monitor the physical host's disks, not the VM's virtual disks, for actual hardware health.
Best Practices and Rules
1. Schedule Regular Health Checks
Don't wait for a crash. Run Method 2 as a daily or weekly scheduled task. A drive that shows "Warning" today may fail tomorrow, the monitoring script gives you a window to act.
2. Monitor Trends, Not Just Snapshots
A single "Healthy" reading means nothing in isolation. Track health status over time (Method 4's CSV). A drive that transitions from "Healthy" to "Warning" is actively degrading and needs immediate attention.
3. Use Get-PhysicalDisk Over wmic diskdrive
Get-PhysicalDisk provides more data (health, operational status, media type, bus type, wear level), works with NVMe drives, and returns clean typed data. Use Win32_DiskDrive as a fallback only when Get-PhysicalDisk is not available.
4. Treat "Pred Fail" / "Warning" as an Emergency
S.M.A.R.T. prediction algorithms are conservative, i.e. they only trigger warnings when failure is highly probable. When a warning appears, the drive has been degrading for some time. Immediate backup and replacement planning is the correct response.
5. Check SSD Wear Level for SSDs
SSDs don't fail like HDDs (gradual mechanical degradation). They have a finite number of write cycles. The wear level percentage from Get-StorageReliabilityCounter tells you how much write endurance remains. Plan replacement at 80–90% wear.
6. Write Alerts to the Event Log
Text file logs require someone to actively check them. Writing critical health alerts to the Windows Event Log (as shown in Method 2) makes them visible to enterprise monitoring tools (SCOM, Zabbix, Splunk) that can trigger automated notifications.
Conclusions
Checking disk health via S.M.A.R.T. data is the simplest and most impactful proactive measure against data loss. By using Get-PhysicalDisk for comprehensive health reporting, scheduling automated checks, tracking health trends over time, and alerting on degradation, you gain the precious window of time needed to back up data and replace hardware before catastrophic failure. This professional-grade vigilance is essential for anyone responsible for data integrity.