Repository navigation
Architecture
ManageUsers is a single self-contained .NET 10 executable targeting
net10.0-windows, published trimmed and compressed as one file per architecture.
It has no service, no agent, no network calls, and no database. All state is on
disk in %ProgramData%.
| Type | File | Responsibility |
|---|---|---|
Program |
Program.cs |
Command-line parsing (System.CommandLine), the Global\ManageUsers mutex, exit codes |
ManageUsersEngine |
Services/ManageUsersEngine.cs |
Orchestrates the run; owns the policy evaluation of each candidate |
ConfigService |
Services/ConfigService.cs |
YAML load/save, exclusion merge, console-user detection |
PolicyService |
Services/PolicyService.cs |
Rule matching, strategy parsing, end-of-term evaluation |
UserEnumerationService |
Services/UserEnumerationService.cs |
NetUserEnum, SID resolution, ProfileList reading, stale/corrupt profile discovery, Administrators membership |
UserDeletionService |
Services/UserDeletionService.cs |
Account and profile destruction, deferral, verification |
RecycleBinService |
Services/RecycleBinService.cs |
Per-SID recycle-bin clearing and the orphan sweep |
RepairService |
Services/RepairService.cs |
Profile-state warnings; login-screen hide list |
LogService |
Services/LogService.cs |
Day-directory operational log, structured event stream, audit log, retention, rotation, legacy log migration |
Models under Models/ are plain YamlDotNet-annotated DTOs plus AppConstants,
which holds every path and the built-in exclusion set.
flowchart TD
A[Mutex Global\ManageUsers] -->|already held| Z[exit 2]
A --> B[LogService: open day directory,<br/>prune past 30 days, migrate legacy logs, rotate]
B --> C[Load Config.yaml]
C --> D[Load Sessions.yaml + Inventory.yaml]
D --> E[Merge exclusions:<br/>built-in + config + sessions + console user]
E --> F[Process DeferredDeletes from last run]
F --> G[Enumerate local accounts<br/>skip disabled / excluded / protected admins]
G --> H[Repair pass: log-only profile warnings]
H --> I[Resolve policy from inventory<br/>first matching rule wins]
I --> J[Pass 1: retention<br/>delete accounts past threshold]
J --> K[Pass 2: orphans<br/>accounts with no profile — NO age check]
K --> L[Pass 3: stale profiles<br/>folders with no account, threshold applies]
L --> M[Pass 4: corrupt profiles<br/>NO age check]
M --> N[Sweep orphaned recycle bins]
N --> O[Write SpecialAccounts\UserList hide entries]
O --> P[RUN_SUMMARY audit line, exit 0]
Ordering is deliberate in three places:
- Deferred deletions run first, so an account that had a session yesterday is retried before the current run's evaluation adds more work.
- Corrupt-profile remediation runs after the stale-profile pass, so profiles just removed are not re-evaluated as corrupt.
- The recycle-bin sweep runs last, so bins emptied by this run's deletions are already handled and the sweep only sees genuine orphans.
No WMI, no COM, no ADSI, no PowerShell. Enumeration is P/Invoke; destruction is a mix of a supported API and console tools.
| Operation | Mechanism |
|---|---|
| Enumerate local accounts |
netapi32!NetUserEnum level 1, FILTER_NORMAL_ACCOUNT
|
| Administrators membership |
netapi32!NetLocalGroupGetMembers level 0, group name resolved from S-1-5-32-544 so it works on localized Windows; pages on ERROR_MORE_DATA
|
| Name → SID |
advapi32!LookupAccountName; NTAccount.Translate in the deletion path |
| Profile inventory |
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList, subkeys starting S-1-5-21- or S-1-12-1-
|
| Last profile load |
LocalProfileLoadTimeHigh / …Low combined into a FILETIME |
| Profile in use |
HKEY_USERS\<SID> present = hive loaded |
| Delete profile |
userenv!DeleteProfileW, falling back to manual folder + registry removal |
| Delete account |
net user "<name>" /delete, success detected by the word successfully in the output |
| Kill processes | taskkill /f /fi "USERNAME eq <MACHINE>\<user>" |
| End sessions |
quser to find the session id, then logoff <id>
|
| Console user |
quser, first line containing Active
|
| BitLocker status |
manage-bde -status C: (informational only) |
Shelling out to net, quser, logoff, taskkill and manage-bde means the
tool inherits their behaviour, including localization. The quser parsing looks
for the literal English Active, and success from net user is a substring
check on the word successfully. On a non-English Windows installation both
checks can misread. Each subprocess has a 30-second wait (5 seconds for quser).
The preferred path is DeleteProfileW, which removes the profile directory, the
ProfileList and ProfileGuid entries, and associated per-SID state as one
consistent operation. Manual deletion is the fallback and is ordered
folder first, registry second on purpose: a ProfileList entry pointing at a
gutted folder corrupts the next logon for that SID, while an orphaned folder is
harmless and is retried next run.
DeleteProfileW can also report success while leaving files behind, so a
residual-folder sweep follows it.
Recursive deletion is hand-rolled rather than Directory.Delete(recursive: true)
because the built-in fails on the legacy compatibility junctions in a profile
(Application Data, Local Settings, My Documents), whose ACLs deny
enumerate-children. The custom walker:
- clears read-only attributes on files and directories;
- unlinks reparse points without descending into them;
- swallows
UnauthorizedAccessExceptionandIOExceptionper item, so one locked file cannot abort the tree; - retries a failed subdirectory with a non-recursive delete before moving on.
The cost of that resilience is that a partially-deleted profile is possible;
VerifyDeletion catches it and the run records USER_DELETE_FAILED.
| Path | Written by | Contents |
|---|---|---|
C:\Program Files\sbin\manageusers.exe |
installer | The binary |
C:\ProgramData\Management\ManageUsers\Config.yaml |
you / config management | Policy |
C:\ProgramData\Management\ManageUsers\Sessions.yaml |
tool + you | Local exclusions, deferred deletions |
C:\ProgramData\Management\Inventory.yaml |
enrolment system | Device facts (read-only to this tool) |
C:\ProgramData\ManagedUsers\logs\<yyyy-MM-dd>\manageusers.log |
tool | That day's operational log, rotated at 10 MB, 5 generations |
C:\ProgramData\ManagedUsers\logs\<yyyy-MM-dd>\events.jsonl |
tool | The same entries structured, one JSON object per line, not size-rotated |
C:\ProgramData\ManagedUsers\logs\manageusers.audit.log |
tool | Append-only decision/outcome log, size-rotated, exempt from the 30-day sweep |
C:\ProgramData\Management\Logs\ManageUsers*.log |
earlier releases | Migrated once to the logs root on first run of a current build |
Day directories older than 30 days are deleted whole at the start of every run, along with any loose files left at the logs root. The audit log is the one thing that persists indefinitely — see Logging and Auditing.
The only mutable runtime state is Sessions.yaml (DeferredDeletes) and the
logs. There is no cache, no watermark file, and no memory of previous runs beyond
those — every run re-derives everything from the live system.
A single global mutex (Global\ManageUsers) makes concurrent runs impossible
machine-wide; a second instance exits 2 immediately. Within a run everything is
single-threaded. The logger serializes writes under one lock, and an audit line's
write to the audit log, the day's operational log and events.jsonl all happen
inside a single lock acquisition so they cannot interleave out of order.
The engine wraps the whole run in one try/catch. An unhandled exception aborts
the remaining passes, logs the message and stack trace, and returns 1. Below
that level, almost everything is best-effort and logged rather than fatal:
- A failed
Config.yamlparse falls back to defaults including empty exclusions. - A failed Administrators enumeration logs a warning and continues with whatever it gathered — protection may be incomplete.
- A failed
ProfileListread cancels the recycle-bin sweep rather than treating every bin as orphaned. - Individual file and registry failures are logged and stepped over.
Two of those defaults fail open rather than closed. That is the reason the Safe Deployment sequence exists, and the reason the audit log is worth shipping somewhere central.
- Selection Logic — the rules each pass applies
- Logging and Auditing — what each pass records
- Development — building and releasing
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.