Skip to content

How to Check Disk Health with S.M.A.R.T. Data in Batch Script

Learn how to monitor hard drive health using S.M.A.R.T. data in Windows Batch scripts, including comprehensive health reports, automated alerts, and fleet-wide CSV exports.

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

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

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.

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.

Navigation