Skip to main content

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

StatusMeaningAction
Healthy / OKS.M.A.R.T. reports no issuesNo action needed: continue monitoring
Warning / Pred FailS.M.A.R.T. predicts imminent failureBack up immediately. Plan replacement.
Unhealthy / DegradedDrive is experiencing errorsBack up immediately. Replace urgently.
UnknownStatus cannot be determinedInvestigate: may be a USB enclosure or driver issue
Predicted Failure Is an Emergency

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:

  • wmic is deprecated since Windows 10 21H1 and produces \r-corrupted output.
  • wmic diskdrive get status only returns a simple "OK" or "Pred Fail" string. Get-PhysicalDisk returns HealthStatus (Healthy, Warning, Unhealthy) AND OperationalStatus (OK, Degraded, Error), two independent indicators.
  • Get-PhysicalDisk also reports MediaType (SSD vs. HDD) and BusType (NVMe, SATA, USB, SAS), which are essential for interpreting the health data and planning replacements.
  • Get-PhysicalDisk works with NVMe drives, which wmic diskdrive often 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 CodeMeaning
0All disks are healthy
1One 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:

MetricWhat It MeansConcern Level
Wear LevelPercentage of flash cell write endurance consumedAbove 90%: plan replacement
Read ErrorsTotal read errors since manufactureRising count indicates degradation
Write ErrorsTotal write errors since manufactureRising count indicates degradation
Power On HoursTotal hours the drive has been poweredContext for wear and warranty
info

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.

warning

"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.

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.