PowerShell script to migrate user profiles to new Windows 10 devices
Moving staff to replacement Windows 10 computers is routine, but copying the Desktop does not migrate a complete profile. The transfer must include profile data, registry settings and identity checks.
Windows separates the account from the profile. The account controls sign-in, while the profile stores Documents, AppData and preferences; reusing a domain identity is safer than creating an unrelated local account.
This PowerShell workflow stages files with robocopy, then merges core HKCU\Software values on the replacement. It uses tools included with Windows 10 and Windows PowerShell 5.1.
Run it after sign-out and backup. The package may contain personal records and credentials, so protect it and handle it under the Privacy Act 1988 and Australian Privacy Principles.
Know what must travel
Profile folders sit under C:\Users\<name>. AppData holds application state, while NTUSER.DAT and sometimes UsrClass.dat contain per-user registry data.
Local accounts receive a new security identifier on the replacement, so permissions can reference the old identity. Reuse domain accounts and re-establish Windows-encrypted credentials.
Prepare the source and replacement devices
Use the same Windows edition and a destination build equal to or newer than the source. Patch both devices, close applications that lock files, and have users sign out before capture.
On the destination, create each account and complete one sign-in so Windows generates its profile. Let OneDrive and Outlook synchronise first, and download cloud-only placeholders.
Use BitLocker or approved encrypted storage, keeping recovery keys separately. Australian teams may still run Windows 10 for scanners, point-of-sale or engineering software, although support ended on 14 October 2025.
Create a reusable PowerShell script
Save the code as Win10ProfileMigration.ps1 and run it elevated. Use .\Win10ProfileMigration.ps1 -Mode Capture -Root E:\ProfileMigration -Account 'jsmith' on the source, then change Capture to Restore on the destination.
param(
[ValidateSet('Capture', 'Restore')][string]$Mode,
[Parameter(Mandatory)][string]$Root,
[string]$Account = '*'
)
$ErrorActionPreference = 'Stop'
function Copy-Tree {
param($Source, $Target, [switch]$SkipRegistryFiles)
New-Item -ItemType Directory -Force -Path $Target | Out-Null
$arguments = @($Source, $Target, '/E', '/XJ', '/COPY:DAT', '/DCOPY:DAT',
'/R:2', '/W:2', '/NFL', '/NDL', '/NP')
if ($SkipRegistryFiles) {
$arguments += @('/XF', 'NTUSER.DAT', 'NTUSER.DAT.LOG1',
'NTUSER.DAT.LOG2', 'UsrClass.dat', '*.ost')
}
& robocopy.exe @arguments
if ($LASTEXITCODE -ge 8) {
throw "Robocopy failed with exit code $LASTEXITCODE."
}
}
if ($Mode -eq 'Capture') {
$profiles = Get-CimInstance Win32_UserProfile | Where-Object {
-not $_.Special -and
$_.LocalPath -like "$env:SystemDrive\Users\*"
}
foreach ($profile in $profiles) {
$name = Split-Path $profile.LocalPath -Leaf
if ($name -notlike $Account) { continue }
if ($profile.Loaded) { throw "Sign out $name before capture." }
$package = Join-Path $Root "Profiles\$name"
Copy-Tree $profile.LocalPath (Join-Path $package 'Data')
[pscustomobject]@{
User = $name
SID = $profile.SID
Source = $profile.LocalPath
Captured = (Get-Date).ToString('o')
} | ConvertTo-Json | Set-Content (Join-Path $package 'manifest.json')
}
}
else {
foreach ($package in Get-ChildItem (Join-Path $Root 'Profiles') -Directory) {
$manifest = Get-Content (Join-Path $package.FullName 'manifest.json') |
ConvertFrom-Json
if ($manifest.User -notlike $Account) { continue }
$destination = Join-Path $env:SystemDrive "Users\$($manifest.User)"
$targetHive = Join-Path $destination 'NTUSER.DAT'
if (!(Test-Path $targetHive)) {
throw 'Sign in once on the destination, then sign out.'
}
Copy-Tree (Join-Path $package.FullName 'Data') $destination -SkipRegistryFiles
$suffix = [guid]::NewGuid().ToString('N')
$oldMount = "ITD_Old_$suffix"
$newMount = "ITD_New_$suffix"
$export = Join-Path $env:TEMP 'profile.reg'
try {
& reg.exe load "HKU\$oldMount" (Join-Path $package.FullName 'Data\NTUSER.DAT')
if ($LASTEXITCODE) { throw 'Could not load the captured hive.' }
& reg.exe load "HKU\$newMount" $targetHive
if ($LASTEXITCODE) { throw 'Could not load the destination hive.' }
Remove-Item "Registry::HKEY_USERS\$oldMount\Software\Classes\UsrClass.dat" `
-Recurse -Force -ErrorAction SilentlyContinue
& reg.exe export "HKU\$oldMount\Software" $export /y
if ($LASTEXITCODE) { throw 'Could not export the registry.' }
$text = (Get-Content $export -Raw).
Replace("HKEY_USERS\$oldMount", "HKEY_USERS\$newMount")
Set-Content $export $text -Encoding Unicode
& reg.exe import $export
if ($LASTEXITCODE) { throw 'Could not import the registry.' }
}
finally {
& reg.exe unload "HKU\$newMount"
& reg.exe unload "HKU\$oldMount"
Remove-Item $export -Force -ErrorAction SilentlyContinue
}
}
}
/COPY:DAT preserves data, attributes and timestamps without replacing destination permissions. Robocopy codes below 8 are acceptable; higher codes stop the script. Registry hives unload even when an import fails.
Capture signed-out profiles
Each package sits under Profiles\<name>\Data, with a manifest recording username, SID and capture time. The script rejects a selected profile that remains loaded.
Review copy summaries and package sizes. Repeated failures usually mean an application or security product still holds a file; use a separate administrator session and disable automatic sign-in.
Restore and verify the profile
On the destination, sign in once, sign out and run Restore. The script checks that the profile exists, copies data over it and leaves the new profile permissions in place.
It merges HKCU\Software, carrying common Explorer, application and environment preferences. It does not replace the whole hive or migrate every licence, credential, scheduled task or user class; use USMT when those are required.
Test a non-critical account first. Check applications, mapped drives, printers, browser favourites and OneDrive synchronisation, including any application tied to the old local SID.
Run a controlled Australian rollout
Publish the change window in local time for Sydney, Melbourne, Brisbane, Perth and Adelaide, whose working hours and time zones differ. Align access, patching and audit logging with the ASD Essential Eight.
Before the change window
- Confirm a system-image restore point and separate BitLocker recovery details.
- Let OneDrive synchronisation finish and identify cloud-only files.
- Reuse the domain identity; document any local-SID change.
- Test scanners, printers, VPN clients and line-of-business software.
During the change window
- Move one canary account before the wider batch.
- Suspend synchronisation agents and keep the user signed out.
- Keep the old device isolated but intact until acceptance.
- Record package hashes, timestamps and the responsible operator.
Keep packages within approved Australian storage, restrict operator access, and dispose of copies under the organisation's privacy and records-retention policy.
Compare methods and complete the cutover
This script suits small, controlled refresh batches, not complete identity migration. Compare coverage and effort before a fleet rollout, especially where applications depend on registration or Windows user classes.
| Method | Best fit | Coverage | Main limitation |
|---|---|---|---|
| Scripted robocopy package | Small refresh batches | Profile files and core HKCU\Software |
Not full identity or user-class migration |
| Microsoft USMT | Larger Windows migrations | Broader file and registry coverage | Requires the Windows ADK and identity planning |
| New profile with selective copy | Clean rebuilds | Deliberately selected folders | Legacy application settings remain behind |
| OneDrive Known Folder Backup | Office-heavy staff | Redirected Documents and Desktop | Does not replace AppData or a full profile |
Finish only after application, synchronisation and security checks pass. Securely erase the staging package, retain the old device temporarily, and document exceptions for local accounts and unsupported peripherals.