Automated PowerShell module maintenance for Windows. Updates all PSResourceGet-managed modules and prunes old versions on a weekly schedule with comprehensive logging.
- 🔄 Automatic Updates — Updates all installed PowerShell modules via PSResourceGet
- 🧹 Version Pruning — Removes old module versions, keeping only the latest
- ☁️ OneDrive Migration — Automatically migrates modules out of OneDrive-synced folders to AllUsers scope
- 📋 Comprehensive Logging — Structured logs with transcripts and JSON summaries
- ⚙️ Configurable Exclusions — Skip specific modules via config file
- ⏰ Scheduled Execution — Runs weekly via Windows Task Scheduler
- 🔔 Toast Notifications — Optional Windows toast notifications after each run
- Windows 10/11 or Windows Server 2019+
- PowerShell 7.0 or later
- Microsoft.PowerShell.PSResourceGet module
git clone https://github.com/haakonwibe/PSModuleMaintenance.git
cd PSModuleMaintenanceEdit config.json to exclude specific modules:
{
"ExcludedModules": [
"Az.Accounts",
"SomeModuleIPinToSpecificVersion"
],
"LogRetentionDays": 180,
"TrustPSGallery": true,
"NotificationMode": "Always",
"MigrateFromOneDrive": false
}Run as Administrator:
.\Install-ModuleMaintenance.ps1This creates a weekly task running Sundays at 3:00 AM.
.\Install-ModuleMaintenance.ps1 -DayOfWeek Saturday -Time "04:30"Run the maintenance script directly:
# Full maintenance (update + prune)
.\Invoke-PSModuleMaintenance.ps1
# Full maintenance with OneDrive migration
.\Invoke-PSModuleMaintenance.ps1 -MigrateFromOneDrive
# Update only
.\Invoke-PSModuleMaintenance.ps1 -UpdateOnly
# Prune only
.\Invoke-PSModuleMaintenance.ps1 -PruneOnly
# Migrate modules out of OneDrive only (no updates or pruning)
.\Invoke-PSModuleMaintenance.ps1 -MigrateOnly -MigrateFromOneDrive
# Dry run - see what would happen
.\Invoke-PSModuleMaintenance.ps1 -WhatIf
# Verbose output
.\Invoke-PSModuleMaintenance.ps1 -Verbose| Setting | Type | Default | Description |
|---|---|---|---|
ExcludedModules |
string[] | [] |
Module names to skip during updates and pruning |
LogRetentionDays |
int | 180 |
Days to keep log files before auto-cleanup |
TrustPSGallery |
bool | true |
Trust PSGallery during updates (avoids prompts) |
NotificationMode |
string | "Always" |
Toast notifications: "Always", "OnFailure", or "Never" |
MigrateFromOneDrive |
bool | false |
Migrate modules from OneDrive to AllUsers scope (see below) |
PSModuleMaintenance can show a Windows toast notification after each run. Set NotificationMode in config.json:
"Always"— Notification after every run with a summary of updates and pruning (default)"OnFailure"— Notification only when modules fail to update or versions fail to prune"Never"— No notifications
The toast uses the built-in Windows "Security and Maintenance" notification channel — no additional setup required.
Logs are written to %ProgramData%\PSModuleMaintenance\Logs\:
C:\ProgramData\PSModuleMaintenance\Logs\
├── maintenance_2024-01-15_030000.log # Structured log
├── transcript_2024-01-15_030000.log # Full verbose transcript
└── summary_2024-01-15_030512.json # Machine-readable summary
{
"StartTime": "2024-01-15T03:00:00.0000000+01:00",
"EndTime": "2024-01-15T03:05:12.0000000+01:00",
"ModulesChecked": 79,
"ModulesUpdated": 12,
"ModulesFailed": [],
"ModulesMigrated": 0,
"MigrationFailed": [],
"VersionsPruned": 45,
"PrunesFailed": [],
"ExcludedModules": ["Az.Accounts"]
}Remove the scheduled task:
.\Install-ModuleMaintenance.ps1 -UninstallOptionally remove logs:
Remove-Item "$env:ProgramData\PSModuleMaintenance" -Recurse -ForcePowerShell 7 installs CurrentUser-scope modules to $HOME\Documents\PowerShell\Modules. On enterprise devices with Known Folder Move enabled, the Documents folder is redirected to OneDrive. This causes OneDrive to sync module files, leading to:
- File locks during sync that block
Update-PSResourceandUninstall-PSResourcewith "Cannot remove package path" and "Access denied" errors - Cloud placeholders (reparse points) — OneDrive replaces local files with cloud-only stubs that standard file APIs cannot delete
- Deletion confirmation popups when pruning old module versions
- Inability to exclude the folder from sync on managed devices (organizational policy)
Solving this required fighting four systems at once, each with undocumented edge cases that only revealed themselves when the previous layer was fixed:
-
PSResourceGet's scope model —
Get-PSResourcewithout-Scopedefaults to CurrentUser only (finds nothing after migration).Uninstall-PSResourcewithout-Scopetargets any scope (deletes the wrong copies).InstalledLocationreturns inconsistent paths. Each API call needed different scope handling. -
OneDrive Known Folder Move — Silently redirects a system path that PowerShell depends on. No API to detect it directly — you have to infer it by comparing
[Environment]::GetFolderPath('MyDocuments')against OneDrive environment variables. -
OneDrive cloud placeholders — Files that look normal to
Get-ChildItembut are actually NTFS reparse points with no local data. They return "Access denied" on delete, buthandle.exeshows no locks and ACLs show FullControl. The fix: strip cloud attributes withattrib -P -U -O, then usecmd.exe rd /s /qwhich handles reparse points where PowerShell'sRemove-Itemcannot. -
The cascading reveal — Each fix exposed the next bug. Fix the migration → scope mismatch deletes AllUsers modules. Fix the scope →
Get-PSResourcereturns 1 module instead of 160. Fix that →InstalledLocationpoints to wrong path. Fix that → "Access denied" on cloud placeholders. No single system was "wrong" — the bugs only existed at the intersections.
When MigrateFromOneDrive is enabled, the script automatically:
- Detects if your CurrentUser module path is inside a OneDrive-synced folder
- Copies all modules to AllUsers scope (
$env:ProgramFiles\PowerShell\Modules) — safely, without deleting the originals - Updates modules with
-Scope AllUsersso new versions install outside OneDrive - Cleans up the old OneDrive copies during the prune pass, with four-stage force-removal for cloud placeholders and locked files (see Troubleshooting)
The migration is idempotent and gradual — modules that already exist at the destination are skipped, and OneDrive copies that can't be removed are either force-deleted or scheduled for reboot deletion.
# Dry-run first to see what would happen
.\Invoke-PSModuleMaintenance.ps1 -MigrateFromOneDrive -WhatIf
# One-time migration + full maintenance (no config change needed)
.\Invoke-PSModuleMaintenance.ps1 -MigrateFromOneDrive
# Migrate only, skip updates and pruning
.\Invoke-PSModuleMaintenance.ps1 -MigrateOnly -MigrateFromOneDriveTo enable migration permanently, set "MigrateFromOneDrive": true in config.json. The -MigrateFromOneDrive switch overrides the config for a single run. When disabled (the default), or when the module path is not in OneDrive, the script behaves exactly as before.
- Load Configuration — Reads
config.jsonfor exclusions and settings - Initialize Logging — Creates timestamped log files and starts transcript
- Clean Old Logs — Removes logs older than retention period
- Migrate Modules — If OneDrive is detected on the module path, copies modules to AllUsers scope
- Update Modules — Bulk checks PSGallery for available updates, then only updates modules that have newer versions (targets AllUsers scope when OneDrive is detected)
- Prune Versions — Groups modules by name, keeps newest, removes the rest (skips built-in modules like PackageManagement). When OneDrive is detected, also removes all migrated copies from the OneDrive path
- Save Summary — Writes JSON summary for monitoring/alerting integration
- Toast Notification — Shows a Windows toast with the run summary (if enabled via
NotificationMode)
Check Task Scheduler history. Common issues:
- Script path changed after installation
- User password changed (re-run installer)
- Network not available at scheduled time
Check the log files for specific errors. Common causes:
- Module removed from PSGallery
- Dependency conflicts
- Network/proxy issues
The script requires Administrator privileges when using OneDrive migration (AllUsers scope). The scheduled task is configured to run with highest privileges. For manual runs, use an elevated PowerShell prompt.
OneDrive can block file deletion in two ways: sync locks during active syncing, and cloud placeholders (reparse points) where OneDrive replaces local files with cloud-only stubs. Both cause "Access denied" errors. The script handles this with a four-stage escalation:
- Normal deletion —
Remove-Item -Recurse -Force - Cloud attribute strip +
rd /s /q— Strips OneDrive cloud-file attributes (attrib -P -U -O) to convert placeholders back to normal files, then usescmd.exe rd /s /qwhich handles reparse points differently than PowerShell'sRemove-Item - File-by-file deletion — Deletes individual files, skipping those still locked
- Reboot-scheduled deletion — Uses
kernel32.dll MoveFileExwithMOVEFILE_DELAY_UNTIL_REBOOTto schedule remaining locked files for deletion by the Windows kernel on next reboot (before any user-mode process starts)
Most OneDrive cleanup completes at stage 2. Stage 4 is the nuclear option for truly stubborn files.
OneDrive may warn about mass deletions when cleaning up migrated module copies. This is expected — the modules have already been copied to AllUsers scope. Click "Delete" to allow OneDrive to sync the removal.
Issues and PRs welcome! Please include log output when reporting bugs.
MIT License - See LICENSE for details.
Haakon Wibe
- Blog: alttabtowork.com
- Twitter: @HaakonWibe