Skip to content

Safe Deployment

Rod Christiansen edited this page Sep 5, 2026 · 2 revisions

Safe Deployment

ManageUsers deletes user accounts, profile directories and recycle-bin contents. There is no undo, no quarantine, and no staging area — a deleted profile is gone. This page is the rollout order that keeps that from being a story you tell later.

Work through the stages in order. Do not skip stage 0 because the tool "isn't armed yet" — installing the package is what arms it, if your packaging registers the schedule.

Stage 0 — install without a schedule

Install the binary but do not register the scheduled task yet. The MSI's postinstall deliberately does not create the task; it verifies the binary, creates C:\ProgramData\Management\ManageUsers and C:\ProgramData\ManagedUsers\logs, and seeds Sessions.yaml if it is missing. Scheduling is a separate, deliberate step (Scheduling). The postinstall creates only the logs root; the <yyyy-MM-dd> day directory the operational log lives in is created by the first run, so an empty logs directory after install is expected.

msiexec /i ManageUsers-x64-<version>.msi /qn

Confirm the binary is present and reports a version:

& 'C:\Program Files\sbin\manageusers.exe' --version

Stage 1 — write the configuration before the first run

If Config.yaml is absent, ManageUsers logs a warning and falls back to built-in defaults: 28 days, login_and_creation, end-of-term dates of 30 April, 31 August and 31 December, and an empty exclusion list beyond the five built-ins. That default will run against your device. Never let the first run be the one that discovers the config is missing.

Write C:\ProgramData\Management\ManageUsers\Config.yaml first. For the very first pass, use a never-delete policy so the tool can only report:

delete_admins: false
deletable_admins: []

exclusions:
  - Administrator
  - DefaultAccount
  - Guest
  - WDAGUtilityAccount
  - defaultuser0

policies:
  - name: "Observe only"
    match:
      usage: ".*"
    duration_days: -1
    strategy: login_and_creation
    force_at_end_of_term: false

default_policy:
  duration_days: -1
  strategy: login_and_creation
  force_at_end_of_term: false

end_of_term_dates: []

duration_days: -1 means never delete in the retention and stale-profile passes.

A never-delete policy does not stop the orphan pass or corrupt-profile remediation. Those two passes ignore duration_days entirely. This is exactly why stage 2 is a dry run and not a live "safe" run.

Keep end_of_term_dates: empty until you have decided you want forced end-of-term reaping. An end-of-term force on a matched rule deletes every non-excluded account on the device with no age check.

The exclusion list to set before the first run

Enumerate the machine's real accounts and decide, one by one. Start from what is actually there rather than from a template:

Get-LocalUser | Select-Object Name, Enabled, Description, LastLogon | Sort-Object Name
Get-LocalGroupMember -Group (Get-LocalGroup -SID S-1-5-32-544).Name
Get-CimInstance Win32_UserProfile | Select-Object LocalPath, SID, LastUseTime, Loaded

Every account in the following categories must be in exclusions: before any live run, because most of them have no profile and are therefore targets of the no-age-check orphan pass:

Category Why
SSH / remote-management accounts Authenticate non-interactively, usually no profile
Local service accounts for agents, backup, imaging, monitoring Same
Build/render worker accounts that run as a service Same
Break-glass local administrator accounts Protected by delete_admins: false, but belt and braces
Kiosk / autologon accounts Have a profile, but you never want them reaped mid-term
Any account a scheduled task runs as Deleting the account orphans the task silently

Remember the matching rules: exact, case-insensitive, no wildcards. One line per account.

Stage 2 — dry run, and actually read the output

Run from an elevated prompt on one representative device:

& 'C:\Program Files\sbin\manageusers.exe' --simulate

Then read the audit log, which is where the decisions are:

Get-Content 'C:\ProgramData\ManagedUsers\logs\manageusers.audit.log' -Tail 200

What to look for:

Audit action Meaning
RUN_START Confirms mode, force flag, resolved policy, and end-of-term state
DELETE_DECISION An account or profile that would be deleted, with the reason and the ages
USER_DELETE_SIMULATED An account a live run would delete
STALE_PROFILE_REMOVE_SIMULATED A profile folder a live run would delete
RECYCLE_BIN_REMOVE_SIMULATED / ORPHANED_RECYCLE_BIN_REMOVE_SIMULATED Bins a live run would clear, with byte counts
RUN_SUMMARY The full list of items in one line

Cross-check against the operational log for the guards:

Select-String -Path "C:\ProgramData\ManagedUsers\logs\$(Get-Date -Format yyyy-MM-dd)\manageusers.log" -Pattern 'Exclusions loaded|Administrator protection|Console user detected|No rule matched|Rule .* matched|Orphaned user'

The line Orphaned user (no profile): <name> is your warning that pass 2 has a target. In a dry run that account is still deleted — see the next section.

What --simulate does not cover

--simulate guards account deletion, stale/corrupt profile deletion and recycle-bin clearing. Two things run for real regardless:

  1. The orphan pass deletes local accounts even under --simulate. RemoveOrphanedUsers calls net user /delete with no simulate check. Any local account with no matching profile — and not excluded, and not a protected administrator — is really removed by a dry run.
  2. The login-screen registry write happens. Every excluded name is written as a DWORD 0 value under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList, hiding those accounts from the sign-in screen. Entries are only ever added, never removed.

Because of (1), do your very first --simulate on a device you are willing to lose accounts on, with the exclusion list already correct. That is the honest version of "dry run" here.

Stage 3 — narrow live scope

Pick one non-critical device, or a small group with an inventory area/usage you control, and give it a real but generous policy. Everything else stays on duration_days: -1.

policies:
  - name: "Pilot — one room"
    match:
      area: "^Pilot Lab$"
      room: "^B120$"
    duration_days: 60
    strategy: login_and_creation
    force_at_end_of_term: false

default_policy:
  duration_days: -1
  strategy: login_and_creation
  force_at_end_of_term: false

Anchoring with ^…$ matters: policy match: values are regexes and match as substrings otherwise, so area: "Lab" would also select Open Lab, Lab Annex and Collaboration.

Run it manually, live, once, and watch:

& 'C:\Program Files\sbin\manageusers.exe'

Then verify:

Select-String -Path 'C:\ProgramData\ManagedUsers\logs\manageusers.audit.log' -Pattern 'USER_DELETED|STALE_PROFILE_REMOVED|_FAILED|DEFERRED'
Get-LocalUser | Select-Object Name, Enabled
Get-CimInstance Win32_UserProfile | Where-Object { -not $_.Special } | Select-Object LocalPath, SID

The account list, the profile folders under C:\Users, and the ProfileList registry entries must agree with each other. A ProfileList entry with no folder is the state described in Troubleshooting → ProfileList orphan.

Leave the pilot on a manual or daily schedule for at least one full user cycle — a week for a teaching lab, a term if the policy is term-length — before widening.

Stage 4 — tighten and widen

Widen in one dimension at a time. Either add devices at the same threshold, or lower the threshold on the same devices — never both in one change.

A workable progression for a teaching lab:

Step duration_days Devices
Pilot 60 1 room
Widen 60 1 building
Tighten 42 1 building
Widen 42 all shared devices
Tighten to target 28 or 30 all shared devices

Only after that is stable should you consider force_at_end_of_term: true and populate end_of_term_dates:. Understand before you do that the forced branch deletes every non-excluded account and stale profile on matching devices, every run, for the whole window the date condition covers, and that force_at_end_of_term on default_policy: is parsed but never acted on — only a matched rule can force.

Stage 5 — schedule it

Register the scheduled task only when the manual live runs have been boring for a full cycle. See Scheduling for the task definition, the SYSTEM requirement, and the single-instance mutex.

Choose a run time when nobody is logged in. Accounts with an Active session are deferred rather than deleted, but a deferred account is retried on the next run, so a schedule that always overlaps with use will keep deferring and never converge.

Things to never do

Don't Why
Deploy --force to a fleet Threshold becomes 0 days: every non-excluded account on the device is deleted. It is a targeted recovery tool, not a policy.
Rely on a wildcard in exclusions: Not supported. svc-* protects nothing but an account literally named svc-*.
Leave Config.yaml absent and rely on defaults The fallback is a live 28-day policy, not a no-op.
Assume --simulate is inert The orphan pass and the login-screen registry write are not simulated.
Trust a Get-LocalUser snapshot alone The orphan pass keys on ProfileList, not on account age; check profiles too.
Set end_of_term_dates before you want forced deletion A matched rule with force_at_end_of_term: true empties the device on those dates.

Rollback

There is no rollback of deleted data. What you can roll back is the tool's authority:

Unregister-ScheduledTask -TaskName 'WinAdmins-ManageUsers' -Confirm:$false

Setting every policy and the default to duration_days: -1 stops passes 1 and 3 but not the orphan or corrupt-profile passes. To stop the tool completely, remove the schedule.

ManageUsers

Home

Before you deploy

Reference

Internals

Help


This tool deletes user accounts and profiles. Run --simulate first.

Clone this wiki locally