How to Delete a Hyper-V Virtual Machine in Batch Script
Deleting virtual machines that are no longer needed is essential for reclaiming host resources, maintaining a clean Hyper-V environment, and preventing configuration drift. Orphaned VMs consume disk space through their virtual hard disks, configuration files, and checkpoint data, even when powered off. Automating the deletion process ensures consistent cleanup with proper safety checks.
In this guide, we will explore how to delete Hyper-V virtual machines from a Batch Script using PowerShell, including pre-deletion validation, associated resource cleanup, and bulk removal operations.
Understanding What Gets Deleted
When you run Remove-VM, Hyper-V deletes:
- The VM configuration files (
.vmcx,.vmrs) - The VM registration from Hyper-V Manager
Remove-VM does NOT delete:
- Virtual hard disk files (
.vhdx,.vhd) - Checkpoint/snapshot files (
.avhdx) - ISO files attached to DVD drives
- The VM's folder structure on disk
You must clean up virtual hard disks separately if you want to reclaim all disk space.
Method 1: Simple VM Deletion
@echo off
setlocal
set "vm_name=OldTestVM"
:: Verify Admin
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Administrator privileges required.
pause
exit /b 1
)
echo Deleting VM "%vm_name%"...
powershell -NoProfile -Command "Remove-VM -Name '%vm_name%' -Force"
if %errorlevel% equ 0 (
echo [SUCCESS] VM "%vm_name%" removed from Hyper-V.
echo NOTE: Virtual hard disk files are NOT deleted.
) else (
echo [ERROR] Failed. VM may not exist or may be running.
)
pause
exit /b 0
The -Force Flag
Without -Force, PowerShell prompts for confirmation. Adding -Force suppresses the prompt, making the script non-interactive.
Method 2: Complete Deletion (VM + VHDs)
For a thorough cleanup that also removes the virtual hard disks:
@echo off
setlocal EnableDelayedExpansion
set "vm_name=DecommissionedServer"
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Admin required.
pause
exit /b 1
)
echo =============================================
echo DECOMMISSIONING: %vm_name%
echo =============================================
echo.
:: Check if VM exists
set "vm_found="
for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-VM -Name '%vm_name%' -ErrorAction SilentlyContinue).Name"`) do set "vm_found=%%S"
if not defined vm_found (
echo [INFO] VM "%vm_name%" does not exist.
pause
exit /b 0
)
:: Get VM details before deletion
echo VM Details:
powershell -NoProfile -Command ^
"$vm = Get-VM -Name '%vm_name%';" ^
"Write-Host (' State: ' + $vm.State);" ^
"$vm.HardDrives | ForEach-Object { Write-Host (' VHD: ' + $_.Path) }"
echo.
:: Confirm
set /p "confirm=Delete this VM and its VHDs? Type YES to proceed: "
if /i not "!confirm!"=="YES" (
echo [CANCELLED]
pause
exit /b 0
)
:: Stop the VM if running
echo.
echo Stopping VM if running...
powershell -NoProfile -Command ^
"$vm = Get-VM -Name '%vm_name%';" ^
"if ($vm.State -ne 'Off') { Stop-VM -Name '%vm_name%' -TurnOff -Force }"
:: Collect VHD paths before deletion
echo Collecting VHD paths...
set "vhd_count=0"
for /f "usebackq delims=" %%P in (`powershell -NoProfile -Command "(Get-VM -Name '%vm_name%').HardDrives | ForEach-Object { $_.Path }"`) do (
set /a vhd_count+=1
set "vhd[!vhd_count!]=%%P"
)
:: Remove the VM
echo Removing VM from Hyper-V...
powershell -NoProfile -Command "Remove-VM -Name '%vm_name%' -Force"
:: Delete VHD files
if !vhd_count! gtr 0 (
echo Deleting virtual hard disks...
for /L %%i in (1,1,!vhd_count!) do (
if exist "!vhd[%%i]!" (
echo Deleting: !vhd[%%i]!
del /f "!vhd[%%i]!"
) else (
echo [SKIP] Not found: !vhd[%%i]!
)
)
) else (
echo No VHDs associated with this VM.
)
echo.
echo [COMPLETE] VM and VHDs deleted.
pause
exit /b 0
Method 3: Safe Deletion with Pre-Flight Checks
A production-grade script with comprehensive validation:
@echo off
setlocal EnableDelayedExpansion
set "vm_name=TargetVM"
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Admin required.
pause
exit /b 1
)
echo =============================================
echo SAFE VM DELETION
echo =============================================
echo.
:: 1. Check existence
echo [CHECK] Verifying VM exists...
set "vm_found="
for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-VM -Name '%vm_name%' -ErrorAction SilentlyContinue).Name"`) do set "vm_found=%%S"
if not defined vm_found (
echo [ABORT] VM "%vm_name%" not found.
pause
exit /b 1
)
echo Found.
:: 2. Check state
set "state="
for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-VM -Name '%vm_name%').State"`) do set "state=%%S"
echo [CHECK] Current state: !state!
if /i "!state!"=="Running" (
echo [WARNING] VM is currently running!
set /p "stop_it=Stop the VM first? (Y/N): "
if /i "!stop_it!"=="Y" (
powershell -NoProfile -Command "Stop-VM -Name '%vm_name%' -TurnOff -Force"
timeout /t 10 /nobreak >nul
) else (
echo [ABORT] Cannot delete a running VM.
pause
exit /b 1
)
)
:: 3. Check for checkpoints
set "cp_count=0"
for /f "usebackq delims=" %%C in (`powershell -NoProfile -Command "@(Get-VMCheckpoint -VMName '%vm_name%' -ErrorAction SilentlyContinue).Count"`) do set "cp_count=%%C"
if !cp_count! gtr 0 (
echo [WARNING] VM has !cp_count! checkpoint(s^). These will be merged/deleted.
)
:: 4. Display disk usage
echo.
echo Disk usage:
powershell -NoProfile -Command ^
"(Get-VM -Name '%vm_name%').HardDrives | ForEach-Object {" ^
" $item = Get-Item $_.Path -ErrorAction SilentlyContinue;" ^
" if ($item) {" ^
" $size = [math]::Round($item.Length / 1GB, 2);" ^
" Write-Host (' ' + $_.Path + ' (' + $size + ' GB)')" ^
" } else {" ^
" Write-Host (' ' + $_.Path + ' (file not found)')" ^
" }" ^
"}"
:: 5. Final confirmation
echo.
set /p "confirm=PERMANENTLY DELETE %vm_name%? Type DELETE to confirm: "
if /i not "!confirm!"=="DELETE" (
echo [CANCELLED]
pause
exit /b 0
)
:: Execute deletion
echo.
echo Removing VM...
powershell -NoProfile -Command "Remove-VM -Name '%vm_name%' -Force"
if %errorlevel% equ 0 (
echo [OK] VM configuration removed.
) else (
echo [ERROR] Failed to remove VM.
)
pause
exit /b 0
Method 4: Bulk Deletion
For cleaning up lab environments or decommissioning multiple VMs:
@echo off
setlocal EnableDelayedExpansion
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Admin required.
pause
exit /b 1
)
set "vms=LabVM-1 LabVM-2 LabVM-3 LabVM-4 LabVM-5"
echo Bulk deleting VMs: %vms%
echo.
set /p "confirm=Type YES to confirm: "
if /i not "!confirm!"=="YES" (
echo [CANCELLED]
pause
exit /b 0
)
set "deleted=0"
set "skipped=0"
for %%V in (%vms%) do (
echo Deleting %%V...
:: Check if VM exists
set "vm_found="
for /f "usebackq delims=" %%N in (`powershell -NoProfile -Command "(Get-VM -Name '%%V' -ErrorAction SilentlyContinue).Name"`) do set "vm_found=%%N"
if not defined vm_found (
echo [SKIP] Not found.
set /a skipped+=1
) else (
:: Stop if running
powershell -NoProfile -Command ^
"$vm = Get-VM -Name '%%V';" ^
"if ($vm.State -eq 'Running') { Stop-VM -Name '%%V' -TurnOff -Force }" >nul 2>&1
:: Delete
powershell -NoProfile -Command "Remove-VM -Name '%%V' -Force"
if !errorlevel! equ 0 (
echo [OK]
set /a deleted+=1
) else (
echo [FAIL] Could not remove.
set /a skipped+=1
)
)
)
echo.
echo [DONE] Deleted: !deleted! Skipped: !skipped!
pause
exit /b 0
Method 5: Delete by Pattern
Delete all VMs whose names match a pattern (useful for removing test/temp VMs):
@echo off
setlocal EnableDelayedExpansion
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Admin required.
pause
exit /b 1
)
set "pattern=Test*"
echo Searching for VMs matching "%pattern%"...
echo.
:: Check if any VMs match
set "match_count=0"
for /f "usebackq delims=" %%N in (`powershell -NoProfile -Command "Get-VM -Name '%pattern%' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name"`) do (
set /a match_count+=1
echo %%N
)
if !match_count! equ 0 (
echo No VMs found matching "%pattern%".
pause
exit /b 0
)
echo.
echo Found !match_count! VM(s^).
set /p "confirm=Delete ALL matching VMs? Type YES: "
if /i not "!confirm!"=="YES" (
echo [CANCELLED]
pause
exit /b 0
)
powershell -NoProfile -Command ^
"Get-VM -Name '%pattern%' | ForEach-Object {" ^
" if ($_.State -ne 'Off') { Stop-VM -Name $_.Name -TurnOff -Force };" ^
" Remove-VM -Name $_.Name -Force;" ^
" Write-Host ('[Deleted] ' + $_.Name)" ^
"}"
pause
exit /b 0
Common Mistakes
The Wrong Way: Deleting a Running VM Without Stopping
:: WRONG - May fail or leave orphaned resources
powershell -Command "Remove-VM -Name 'RunningVM' -Force"
Output Concern:
While Remove-VM -Force can remove a running VM in some versions, it is not reliable and may leave the worker process running. Always stop the VM first, either gracefully with Stop-VM -Force or immediately with Stop-VM -TurnOff -Force.
The Wrong Way: Assuming VHDs Are Deleted
:: WRONG assumption - VHDs are still on disk after Remove-VM
powershell -Command "Remove-VM -Name 'OldVM' -Force"
echo "All cleaned up!"
:: VHDs still consuming 80+ GB on disk
Remove-VM only removes the Hyper-V configuration. The VHDX files remain on disk and must be deleted separately.
Best Practices
- Stop VMs before deleting: Always ensure the VM is in the
Offstate before removal. - Record VHD paths first: Capture the VHD file paths before running
Remove-VM, since the association is lost after deletion. - Confirm interactively: For destructive operations, always require explicit typed confirmation.
- Remove checkpoints first: Merging checkpoints before deletion prevents orphaned
.avhdxfiles. - Verify cleanup: After deletion, check the disk for leftover VHD and configuration files.
Conclusion
Deleting Hyper-V virtual machines from a Batch Script is handled by PowerShell's Remove-VM cmdlet, but a complete decommission goes beyond removing the VM registration. By collecting VHD paths before deletion, stopping running VMs, handling checkpoints, and confirming the operation interactively, administrators can build safe, thorough cleanup scripts. Whether removing a single obsolete server or bulk-deleting an entire lab environment, the patterns in this guide ensure no resources are left orphaned on the host.