PowerShell tooling to identify and quarantine orphaned FSLogix profile folders on a file share. Profiles are matched against Active Directory by SID (not by sAMAccountName), so renamed accounts are detected reliably.
The interactive HTML report groups every detected profile by status, lets you filter by deletable vs. active, and shows live size totals at the top. One click on Deletable FSLogix-Folders narrows the table down to just the cleanup candidates and updates the totals accordingly:
The toolkit ships with two scripts:
Find-OrphanedFSLogixProfiles.ps1— scans a profile share, looks each profile up in AD via LDAP, and produces an interactive HTML report plus a CSV.Move-OrphanedFSLogixProfiles.ps1— reads that CSV and moves the orphaned profile folders into a quarantine directory. Supports-WhatIf,-Confirm, and writes an audit log of every operation.
No RSAT/ActiveDirectory module required. No ADWS (TCP 9389) required. All AD lookups
go through standard LDAP (TCP 389/636) using System.DirectoryServices.
FSLogix profile folders are named <sAMAccountName>_<SID>, e.g.
jdoe_S-1-5-21-1111-2222-3333-1001. The sAMAccountName is mutable (rename on
marriage, naming-convention change, typo fix), but the SID is immutable for the
lifetime of the account. Matching by sAMAccountName produces false positives every
time someone is renamed; matching by SID does not.
| Status | Meaning | CanBeDeleted |
|---|---|---|
Active |
Account exists in AD, enabled, sAMAccountName matches the folder name | No |
Deleted |
No AD account exists for this SID anymore | Yes |
Disabled |
Account exists but is disabled | Yes |
Renamed |
Account exists, but its current sAMAccountName differs from the folder's name. FSLogix has created a new folder under the new name; the old folder is orphaned | Yes |
OtherDomain |
The folder's SID does not belong to the connected domain | No (default) |
ADError |
LDAP lookup error | No |
The CanBeDeleted column carries a short reason such as Yes - Deleted,
Yes - Disabled, or Yes - Renamed so the basis for each decision is visible at
a glance.
- Windows PowerShell 5.1 or PowerShell 7+
- Read access to the FSLogix profile share
- Read access to AD over LDAP (port 389 or 636)
- Write access to the quarantine target (Move script only)
The Find script does not need the RSAT ActiveDirectory module and does not
talk to AD Web Services on port 9389. It uses System.DirectoryServices directly.
.\Find-OrphanedFSLogixProfiles.ps1 -ProfileRoot '\\fileserver\FSLogix$'This scans the share, reads each profile folder name, looks the SID up in AD, and produces three files in the current directory:
FSLogix-Inventory-<timestamp>.csv— every detected FSLogix folderFSLogix-Report-<timestamp>.csv— non-Active profiles only (input for the Move script)FSLogix-Report-<timestamp>.html— interactive HTML report (see below)
Open FSLogix-Report-<timestamp>.html in any browser. The page provides:
- Live tiles at the top — counts and total size for the current filter, plus separate tiles for "Deletable" and "Active" profiles
- Filter buttons —
All,Deletable,Active,Deleted,Disabled,Renamed,ADError - Search box — case-insensitive substring match across all columns
- Sortable columns — click any column header (numeric and date columns sort correctly)
- Color highlighting — rows with
CanBeDeleted = Yesare tinted red
.\Move-OrphanedFSLogixProfiles.ps1 `
-ReportCsv '.\FSLogix-Report-20260508-105143.csv' `
-ProfileRoot '\\fileserver\FSLogix$' `
-QuarantineRoot '\\fileserver\FSLogix$\_Quarantine' `
-IncludeStatus Deleted,Disabled,Renamed `
-WhatIfDrop -WhatIf to perform the actual move. Each run creates a new subdirectory
<QuarantineRoot>\<timestamp>\ and an audit log
FSLogix-Move-<timestamp>.log.csv next to the report CSV.
Tip: put
QuarantineRooton the same volume asProfileRoot. ThenMove-Itemis a fast atomic rename. Across volumes it becomes a copy + delete, which is slower and has a wider failure window.
.\Find-OrphanedFSLogixProfiles.ps1
-ProfileRoot <path> # mandatory: profile share root
[-OutputDir <path>] # default: current directory
[-Server <fqdn>] # specific DC (e.g. dc01.contoso.com)
[-Credential <pscredential>] # for cross-domain runs
[-AssumeOtherDomainDeleted] # rare; only after confirmed migration
[-SkipSize] # skip per-folder size scan (faster)
- Enumerates
ProfileRootfor subfolders matching<sam>_<S-1-5-21-...>. - Reads
RootDSEto discoverdefaultNamingContextand the domain SID. - For each unique SID, runs an LDAP search by binary
objectSidand readssamAccountName,userAccountControlandlastLogonTimestamp. - Classifies each profile (see status table above).
- Measures profile size (
Get-ChildItem -Recurse | Measure-Object -Sum Length) unless-SkipSizeis set. Most FSLogix folders contain only 1–2 VHDX files, so this is usually fast even over SMB. - Writes Inventory CSV, Report CSV and Report HTML.
The script expects the default FSLogix folder pattern <sam>_<SID>. The reverse
form <SID>_<sam> produced by FlipFlopProfileDirectoryName=1 is not
supported; folders not matching the expected regex are silently skipped.
The script uses lastLogonTimestamp (replicated, ~14-day tolerance), not
lastLogon (per-DC, requires querying every DC). For cleanup purposes the
14-day fuzz is acceptable.
.\Move-OrphanedFSLogixProfiles.ps1
-ReportCsv <path> # mandatory
-ProfileRoot <path> # mandatory
-QuarantineRoot <path> # mandatory
[-IncludeStatus Deleted | Disabled | Renamed ...] # default: Deleted
[-CheckOpenFiles] # SMB lock check
[-WhatIf] [-Confirm]
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]— every move honors-WhatIfand-Confirm.- Sources are reconstructed as
Join-Path $ProfileRoot $row.FolderName. If the folder no longer exists at move-time, the entry is skipped withResult = Skipped. - The script never moves
Active,OtherDomain, orADErrorrows.RenamedandDisabledrequire explicit opt-in via-IncludeStatus. - The script never deletes anything. Quarantined folders sit in a dated subfolder ready for restore or for manual deletion after a retention period.
- An audit-log CSV records source path, destination path, status, result, and any error message for every operation.
Calls Get-SmbOpenFile to skip profiles whose VHDX is currently mounted. This
only works when the script runs on the file server itself (or via PowerShell
Remoting onto it). On a remote admin workstation Get-SmbOpenFile returns
nothing useful; the lock check then silently passes through, so prefer running
during a maintenance window in that case.
| File | Purpose |
|---|---|
FSLogix-Inventory-<ts>.csv |
Every detected FSLogix folder, raw inventory |
FSLogix-Report-<ts>.csv |
Non-Active profiles, used as input for the Move script |
FSLogix-Report-<ts>.html |
Interactive HTML report with filters and live size totals |
FSLogix-Move-<ts>.log.csv |
Audit log of one Move run (next to the Report CSV) |
These files contain real account names and SIDs, so they are excluded from version
control via .gitignore.
- Reverse FSLogix naming (
FlipFlopProfileDirectoryName=1) is not supported. - Only
S-1-5-21(regular user) SIDs are processed; well-known SIDs are ignored. - The
OtherDomainstatus is conservative on purpose — it does not treat the profile as deletable. Use-AssumeOtherDomainDeletedonly after confirming via the Domain SID inventory table at the bottom of the HTML report that the foreign SIDs actually come from a defunct migration source. Get-SmbOpenFileworks only on the local file server.
Most profiles end up as OtherDomain.
The script extracted the wrong domain SID. Pass -Server <fqdn-of-correct-DC>
and re-run. The HTML report's Domain SID inventory table shows which SID
prefixes appear in the profile folders so you can sanity-check the auto-detected
domain SID.
Most profiles end up as Deleted/ADError.
LDAP lookups are failing. Check:
- can the running account read the user objects in AD?
- is TCP 389 (or 636) reachable from this machine?
- is
-Serverpointing to a DC that actually replicates the user-containing OUs?
Join-Path : Argument cannot be bound to parameter "Path" because it is empty
in the Move script.
This happens if -ReportCsv was passed as a bare filename without a directory.
Use a full path or run from the directory containing the CSV. The current
script resolves relative paths internally.
MIT — see LICENSE.

