Repository navigation
Troubleshooting
Symptom → cause → fix, grounded in the paths that actually exist in the code. Start every investigation in the same two places.
The operational log for today, which lives in a directory named for the local date the run started:
Get-Content "C:\ProgramData\ManagedUsers\logs\$(Get-Date -Format yyyy-MM-dd)\manageusers.log" -Tail 200The audit log, which stays at the logs root and is not per-day:
Select-String -Path 'C:\ProgramData\ManagedUsers\logs\manageusers.audit.log' -Pattern '_FAILED|_SKIPPED|_DEFERRED'Substitute the date to look at an earlier day; day directories are kept for 30 days and then deleted whole. To search the whole retained window at once, glob the day directories:
Get-ChildItem 'C:\ProgramData\ManagedUsers\logs\*\manageusers.log' |
Select-String -Pattern 'ERROR'Every entry also exists in machine-readable form in events.jsonl beside the
day's log — useful when you want to filter by level or pull one invocation out of
a day that ran on a schedule. See Logging and Auditing
for the record shape.
There is no verbose flag, no --debug, and no log-level setting; the default
output already includes a line per account evaluated with its ages and the
keep/delete reason.
Check the resolved policy first.
Select-String -Path "C:\ProgramData\ManagedUsers\logs\$(Get-Date -Format yyyy-MM-dd)\manageusers.log" -Pattern 'Deletion policy:|Rule .* matched|No rule matched'| Cause | Evidence | Fix |
|---|---|---|
| Never-delete policy in effect |
Never-delete policy: <user> — keep, Policy(Duration=-1d, …)
|
Set a non-negative duration_days
|
| No rule matched, default is long | No rule matched area='…'/room='…'/usage='…' → default policy |
Check the inventory values against your regexes; remember room compares to the inventory's location key |
| Inventory missing | Inventory file not found |
Fix the enrolment system or pass --inventory
|
| Rule has no criteria | Rule silently never matches | A rule with an empty match: matches nothing; use default_policy for a catch-all |
| Everything is excluded |
Skipping excluded user: … for each account |
Trim exclusions:
|
| Everything is a protected admin | Skipping administrator account (protected; …) |
Use deletable_admins for specific accounts |
| Accounts are disabled | No User: <name> line at all for them |
Disabled accounts are skipped by pass 1 (but not by the orphan pass) |
| Ages read as zero | Could not determine creation date for <user>, using current time |
The profile folder is missing or unreadable; the account looks 0 days old to pass 1 |
| Cause | Evidence | Notes |
|---|---|---|
| Orphan pass | ORPHAN_USER_REMOVED |
No age check at all. Any non-excluded local account without a ProfileList entry ending in \<name> is removed. Add it to exclusions:. |
| Collision-suffixed profile folder | Account jsmith with folder jsmith.MACHINENAME
|
Pass 1's profile join is a \<username> suffix match, so the account looks profile-less and falls to the orphan pass |
| Regex matched more than intended |
Rule '<name>' matched on an unexpected device |
match: values are unanchored regexes. area: "Lab" matches Collaboration. Anchor with ^…$
|
| End-of-term force | reason=end-of-term force deletion |
A matched rule had force_at_end_of_term: true and today satisfies month == M && day >= D
|
--force was used |
Force mode — duration set to 0 days |
Threshold 0 makes every account eligible |
| Config parse failure discarded exclusions | Failed to parse Config.yaml: … — using built-in defaults |
Only the five built-in exclusions applied for that run |
| Admin guard did not apply | administrator protection may be incomplete |
NetLocalGroupGetMembers failed; the run continued unprotected |
A wildcard was used in exclusions:
|
The account still shows a User: <name> evaluation line |
Exclusions are exact, case-insensitive strings. No patterns |
Symptom: STALE_PROFILE_SKIPPED … reason=hive loaded, profile in use.
Cause: the SID's registry hive is loaded under HKEY_USERS, which means an
active or disconnected session, a service running as that user, or a scheduled
task holding the profile open. The code refuses to delete underneath a loaded
hive, and treats an error reading HKEY_USERS as "in use" too.
Fix: end the session and let the next run retry.
quserlogoff <session id>Get-ChildItem Registry::HKEY_USERS | Select-Object NameA disconnected RDP session is the usual culprit and does not show as Active.
Symptom: USER_DELETE_DEFERRED … reason=console session active on every run,
and the name stays in DeferredDeletes in Sessions.yaml.
Cause: quser reports an Active session for that name at the moment of
deletion. On a device that is in use whenever the task runs, the deferral repeats
indefinitely. Note the check matches the name anywhere on the quser line, so a
short account name can be matched by another user's session name or by part of a
longer name.
Fix: move the schedule to a time when the device is idle, or add a startup trigger. Inspect the queue:
Get-Content 'C:\ProgramData\Management\ManageUsers\Sessions.yaml'An entry can be removed by hand; it is re-added on the next run if the condition still holds.
Symptom: Failed to remove profile directory C:\Users\<name>: …, then
STALE_PROFILE_REMOVE_FAILED or USER_DELETE_FAILED …reason=verification failed,
and a folder still present under C:\Users.
Cause: the recursive delete swallows UnauthorizedAccessException and
IOException per item so one locked file cannot abort the tree. Files held open
by antivirus, search indexing, a backup agent, or a process that survived
taskkill remain, and the folder cannot be removed.
Fix: find the holder and let the next run finish the job.
Get-Process | Where-Object { $_.Path -like 'C:\Users\<name>\*' } | Select-Object Id, ProcessName, Pathhandle.exe -nobanner -u "C:\Users\<name>"If the account is already gone and only the folder remains, that state is
harmless — an orphaned folder with no ProfileList entry is retried as a stale
profile on the next run. The dangerous inverse is covered below.
Symptom: Could not open ProfileList registry key,
Could not create SpecialAccounts\UserList registry key,
Failed to remove registry entry for …, Could not enumerate <drive>:\$Recycle.Bin,
or NetUserEnum failed with code 5.
Cause: the process is not running with sufficient privilege. The manifest
requests requireAdministrator, but several operations — writing HKLM,
deleting other users' profile trees, clearing $Recycle.Bin subfolders — want
SYSTEM, not merely an elevated administrator.
Fix: run it the way it is meant to run.
Get-ScheduledTask -TaskName 'WinAdmins-ManageUsers' | Select-Object -ExpandProperty PrincipalUserId should be SYSTEM and RunLevel Highest. For interactive testing,
use an elevated prompt and accept that some steps may behave differently than
they do under the task. See Scheduling.
Windows error code 5 is access denied; code 2221 from NetUserEnum is
"user not found".
Two distinct broken states, both handled by pass 4.
| State | What the user sees | Audit/log evidence |
|---|---|---|
ProfileList\<SID> exists, folder is gone |
Next logon for that SID gets a temporary profile; changes are discarded at sign-out | Corrupt profile state: <name> (SID: …) — dangling ProfileList entry (profile folder missing) |
Folder exists, NTUSER.DAT is gone |
Temporary profile, Class not registered errors in Explorer |
… — profile folder is missing NTUSER.DAT (partial delete) |
Both are remediated automatically on the next run, regardless of retention
policy, by removing the remaining half. That is why the tool can delete
something on a device configured with duration_days: -1.
One exception: if the folder still exists and its name is in the exclusion
set, nothing is done and you get
Corrupt profile state for excluded account <name>: … — not remediating, manual review needed.
Handle that one by hand — the folder may still hold user data.
Inspect the current state:
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' |
ForEach-Object {
[pscustomobject]@{
SID = $_.PSChildName
Path = (Get-ItemProperty $_.PSPath).ProfileImagePath
Exists = Test-Path (Get-ItemProperty $_.PSPath).ProfileImagePath
}
} | Where-Object { -not $_.Exists }| Symptom | Cause | Fix |
|---|---|---|
ProfileList could not be read; skipping orphaned recycle bin sweep |
The registry read failed or returned nothing. The sweep refuses to run rather than treat every bin as orphaned | Fix registry access; run as SYSTEM |
No SID for <name>; recycle bin could not be located and may remain |
The account's SID could not be resolved before deletion. Bins are named by SID only, so guessing is unsafe | The end-of-run orphan sweep should catch it on a later run once the profile is gone |
Recycle bin <SID> skipped — hive loaded, profile in use |
The SID still has a loaded hive | End the session |
| Bins on a removable or network drive untouched | Enumeration is limited to DriveType.Fixed
|
Out of scope by design |
The $Recycle.Bin\<SID> folder still exists after a successful clear |
Only the contents are deleted; the folder and its desktop.ini are left because Windows owns them |
Expected |
Measure what is there:
Get-ChildItem 'C:\$Recycle.Bin' -Force -Directory |
ForEach-Object {
[pscustomobject]@{
SID = $_.Name
MB = [math]::Round((Get-ChildItem $_.FullName -Recurse -Force -File -EA SilentlyContinue |
Measure-Object Length -Sum).Sum / 1MB, 1)
}
} | Sort-Object MB -Descending| Exit code | Cause | Fix |
|---|---|---|
2 |
Another instance of ManageUsers is already running. — the Global\ManageUsers mutex is held |
Wait for the other run; check for an earlier task instance that is stuck on a slow profile delete |
1 |
Unhandled exception; message and stack trace are in the log at ERROR
|
Read the trace. The usual causes are an unreadable config path or a registry access failure early in the run |
1, with no log entries |
Invalid command line, or the log directory could not be created | Check the arguments, and that C:\ProgramData\ManagedUsers\logs and today's <yyyy-MM-dd> subdirectory under it are creatable |
Work through these in order:
-
Spelling. Exclusions are exact and case-insensitive.
svc_backupdoes not protectsvc-backup. -
Wrong file or wrong key.
Config.yamlusesexclusions:(lowercase);Sessions.yamlusesExclusions:(capital E). A key spelled the other way is silently ignored — unmatched YAML properties are dropped without error. -
Config did not parse.
Failed to parse Config.yamlmeans built-in defaults were used and your list was discarded for that run. -
Sessions.yamlwas rewritten. The tool serializes onlyExclusionsandDeferredDeletes; comments and extra keys do not survive a deferral write. -
It was a profile folder, not an account. The exclusion set is compared
against the folder name in the stale-profile pass.
jsmith.MACHINENAMEis not protected by an entry ofjsmith. -
Corrupt-profile remediation. A dangling
ProfileListentry is removed even for an excluded name, because there is no folder left to preserve.
Confirm which of these applies:
Select-String -Path "C:\ProgramData\ManagedUsers\logs\$(Get-Date -Format yyyy-MM-dd)\manageusers.log" -Pattern 'Exclusions loaded|Skipping excluded user|Failed to parse Config'This is expected behaviour, not a bug, in exactly two cases:
-
Orphan account removal has no simulate guard.
net user /deleteruns for real. Look forORPHAN_USER_REMOVEDin the audit log. -
Login-screen hiding writes
SpecialAccounts\UserListregardless of mode.
Everything else — account deletion, profile deletion, recycle-bin clearing — is
suppressed under --simulate and reported with _SIMULATED actions.
| Where | Why | Fix |
|---|---|---|
| Scheduled tasks still registered to the account | Nothing removes per-user tasks | Remove them yourself: Get-ScheduledTask | Where-Object { $_.Principal.UserId -eq '<name>' }
|
| Credential Manager / IdentityStore entries | The credential step enumerates and logs only; it deletes nothing | Clear by hand if it matters |
| BitLocker key protectors | The BitLocker step is informational; manage-bde -status C: only |
manage-bde -protectors -delete by hand |
| Sign-in screen still hides an account you re-created |
SpecialAccounts\UserList entries are added, never removed |
Delete the value from that registry key |
| The account reappears after a sync | ManageUsers only removes local state; a directory or MDM policy may recreate it | Fix upstream |
& 'C:\Program Files\sbin\manageusers.exe' --simulate *>&1 | Tee-Object -FilePath "$env:TEMP\manageusers-dryrun.txt"Get-LocalUser | Select-Object Name, Enabled, LastLogon | Format-Table -AutoSizeGet-CimInstance Win32_UserProfile | Select-Object LocalPath, SID, LastUseTime, Loaded, Special | Format-Table -AutoSizeThose three outputs plus the tail of the audit log are enough to reason about almost any behaviour on this page.
ManageUsers — local account and profile retention for shared Windows devices · source repository · see the repository for license terms
Before you deploy
Reference
Internals
Help
This tool deletes user
accounts and profiles.
Run --simulate first.