|

Efficient way to manipulate HKU registry entries

Reading Time: 6 minutes

In this post, we’ll explore how to reliably deploy registry changes under the HKEY_USERS (HKU) and HKEY_CURRENT_USER (HKCU) hive using Microsoft Intune. While Group Policy Preferences have long enabled administrators to target HKCU with ease, Intune’s lack of a built-in HKCU registry deployment feature forces us to adopt alternative methods. We’ll first review Intune’s proactive remediation framework and its prerequisites and then introduce a PowerShell-based solution to ensure performant registry updates for both current and future users.

change HKU registry using powershell and intune

Registry Changes via Group Policy Preferences

Group Policy Preferences (GPP) let you define registry updates under User Configuration > Preferences > Windows Settings > Registry, where you can create or update HKCU entries in the context of the logged-on user. This built-in mechanism handles both deployment and rollback gracefully, but only applies in Active Directory–joined environments under on-premises Group Policy.

Intune’s Proactive Remediation Framework

To replicate GPP-style registry updates in Intune, Microsoft offers Remediations (apart from configuration profiles that change the user’s registry hive) which consist of paired detection and remediation scripts. The detection script examines the device state—returning 0 if compliant or 1 if not—and, on a non-zero exit code, triggers the remediation script to correct the configuration. Remediations run under the Intune Management Extension on a fixed defined schedule and can also be kicked off on-demand.

Note: Remediation scripts must be UTF-8 encoded to run correctly in Intune.

Note: It is not mandatory to run a script having both a Detection and Remediation script. You can also create a Detection only Remediations script (e.g. to return some info or perform an action that you don’t want to have a remediation).

Limitations and Workarounds for HKU registry paths

While Intune remediations excel at simple file or service checks, targeting HKU poses two main challenges:

  1. Context: Remediations default to the system context, where HKCU points to the system account, not end users. We can run it as user, but we will not be able to change the registry keys for other users/new users connected to the targeted machine.
  2. New Users: Even if you impersonate users to update their hives, new users created after script deployment won’t inherit those changes.

Common community workarounds include:

  • Deploying .reg files via a Win32 app and launching them under user context on logon (fragile and lacks scalability).
  • Scheduled tasks that load each user’s hive (NTUSER.DAT) at logon to import settings (requires precise timing and error-handling).

Instead, we propose leveraging a PowerShell script to safely load and modify each user’s hive—both for existing and future profiles—under the system account, then clean up.

You can also find the code in my GitHub page.

I am always open to suggestions or improvements.

Detection Script

<#
.SYNOPSIS
    Detection script for HKCU remediation: checks whether all non-system users have the desired registry values.
.DESCRIPTION
    Enumerates each local user profile (excluding built-in service accounts), loads their NTUSER.DAT hive under HKEY_USERS,<SID>,
    verifies each registry value under Software\SysTuNation\Settings matches the expected data and type,
    then unloads the hive. Exits with code 0 if fully compliant, or 1 if any mismatch is found.
    Logs all actions with Start-Transcript and captures output in $intuneOutput.
#>

param (
    [string]$LogFile = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\SysTuNation_HKCUDetect.log'
)

try {
    Start-Transcript -Path $LogFile -Append -NoClobber -ErrorAction Stop | Out-Null
    Write-Host "Transcript started: $LogFile"
} catch {
    Write-Host "ERROR: Failed to start transcript - $_"
    exit 1
}

# Initialize Intune output accumulator
$script:intuneOutput = ""

function LogMessage {
    param([string]$Msg)
    $ts   = (Get-Date).ToString('o')
    $line = "$ts`t$Msg"
    Write-Host $line
    $script:intuneOutput += $line + "|"
}

# Define detection settings
$RegKey   = 'Software\SysTuNation\Settings'
$Values   = @{
    'EnableFeatureX' = @{ Data = 1;       Type = 'DWord' }
    'DefaultRegion'  = @{ Data = 'Europe'; Type = 'String' }
}
$TypeMap = @{ 
    'String' = [Microsoft.Win32.RegistryValueKind]::String;
    'DWord'  = [Microsoft.Win32.RegistryValueKind]::DWord;
    'QWord'  = [Microsoft.Win32.RegistryValueKind]::QWord
}

LogMessage 'Starting detection of HKCU settings for all users (excluding system/service accounts).'

try {
    # Get all profile SIDs, then exclude service accounts (S-1-5-18,19,20)
    $profiles = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ErrorAction Stop |
        Where-Object { $_.PSChildName -notmatch '^S-1-5-(18|19|20)$' } |
        ForEach-Object {
            [PSCustomObject]@{
                SID      = $_.PSChildName
                HivePath = Join-Path -Path $_.GetValue('ProfileImagePath') -ChildPath 'NTUSER.DAT'
            }
        }
    LogMessage "Profiles to check: $($profiles.Count) (system/service accounts excluded)."
} catch {
    LogMessage "ERROR: Failed to enumerate profiles - $_"
    Stop-Transcript | Out-Null
    exit 1
}

$nonCompliant = $false

foreach ($p in $profiles) {
    $sid     = $p.SID
    $hive    = $p.HivePath
    $mounted = $false

    LogMessage "Processing SID: $sid"

    if (-not (Test-Path $hive -PathType Leaf)) {
        LogMessage "SKIP [$sid]: Hive not found at $hive"
        continue
    }

    try {
        # Load hive if not already loaded
        if (-not (Test-Path "Registry::HKEY_USERS\$sid")) {
            Write-Host "Loading hive for SID $sid"
            & reg.exe LOAD "HKEY_USERS\$sid" $hive
            if ($LASTEXITCODE -ne 0) { throw "reg.exe LOAD failed with code $LASTEXITCODE" }
            $mounted = $true
            LogMessage "Loaded hive: HKEY_USERS\$sid"
        } else {
            LogMessage "Hive already loaded: HKEY_USERS\$sid"
        }

        # Verify each expected value
        foreach ($name in $Values.Keys) {
            $fullPath = "Registry::HKEY_USERS\$sid\$RegKey"
            if (-not (Test-Path $fullPath)) {
                LogMessage "[$sid] Key missing: $RegKey"
                $nonCompliant = $true; break
            }

            Write-Host "Checking value '$name' for SID $sid"
            $item = Get-ItemProperty -Path $fullPath -Name $name -ErrorAction Stop
            $actualData = $item.$name
            $actualType = (Get-Item $fullPath).GetValueKind($name)

            if ($actualData -ne $Values[$name].Data -or $actualType -ne $TypeMap[$Values[$name].Type]) {
                LogMessage "[$sid] Mismatch on $name : expected '$($Values[$name].Data)' ($($Values[$name].Type)), got '$actualData' ($actualType)"
                $nonCompliant = $true; break
            } else {
                LogMessage "[$sid] $name is compliant"
            }
        }

    } catch {
        LogMessage "ERROR [$sid]: $_"
        $nonCompliant = $true
    } finally {
        if ($mounted) {
            Write-Host "Unloading hive for SID $sid"
            & reg.exe UNLOAD "HKEY_USERS\$sid"
            if ($LASTEXITCODE -ne 0) {
                LogMessage "WARNING [$sid]: reg.exe UNLOAD exit code $LASTEXITCODE"
            } else {
                LogMessage "Unloaded hive: HKEY_USERS\$sid"
            }
        }
    }

    if ($nonCompliant) { break }
}

if ($nonCompliant) {
    LogMessage 'Detection result: NON-COMPLIANT'
    $exitCode = 1
} else {
    LogMessage 'Detection result: COMPLIANT'
    $exitCode = 0
}

LogMessage 'Detection complete.'

# Stop transcript
Stop-Transcript | Out-Null

# Output accumulator for Intune
Write-Host "Detection Output: $intuneOutput"

exit $exitCode

Remediation Script

<#
.SYNOPSIS
    Deploy HKCU registry values for all users via Intune (no caching).
.DESCRIPTION
    Loads each user NTUSER.DAT hive, writes desired HKCU values, then unloads the hive.
    Uses Start-Transcript and Write-Host for logging, with -ErrorAction Stop on critical cmdlets.
    Captures all Write-Host output in $intuneOutput for later inspection.
#>

param (
    [string]$LogFile = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\SysTuNation_HKCUDeploy.log'
)

try {
    Start-Transcript -Path $LogFile -Append -NoClobber -ErrorAction Stop | Out-Null
    Write-Host "Transcript started: $LogFile"
} catch {
    Write-Host "ERROR: Failed to start transcript - $_"
    exit 1
}

# Initialize Intune output accumulator
$script:intuneOutput = ""

function LogMessage {
    param([string]$Msg)
    $ts   = (Get-Date).ToString('o')
    $line = "$ts`t$Msg"
    Write-Host $line
    $script:intuneOutput += $line + "|"
}

# Define registry settings
$RegKey = 'Software\SysTuNation\Settings'
$Values = @{
    'EnableFeatureX' = @{ Data = 1;       Type = 'DWord' }
    'DefaultRegion'  = @{ Data = 'Europe'; Type = 'String' }
}

LogMessage 'Starting deployment of HKCU settings for all users (excluding system/service accounts).'

try {
    # Enumerate all profile SIDs, excluding system/service accounts (S-1-5-18,19,20)
    $profiles = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ErrorAction Stop |
        Where-Object { $_.PSChildName -notmatch '^S-1-5-(18|19|20)$' } |
        ForEach-Object {
            [PSCustomObject]@{
                SID      = $_.PSChildName
                HivePath = Join-Path -Path $_.GetValue('ProfileImagePath') -ChildPath 'NTUSER.DAT'
            }
        }
    LogMessage "Profiles to process: $($profiles.Count) (system/service accounts excluded)."
} catch {
    LogMessage "ERROR: Failed to enumerate profiles - $_"
    Stop-Transcript | Out-Null
    exit 1
}

foreach ($p in $profiles) {
    $sid     = $p.SID
    $hive    = $p.HivePath
    $mounted = $false

    LogMessage "Processing SID: $sid"

    if (-not (Test-Path $hive -PathType Leaf)) {
        LogMessage "SKIP [$sid]: Hive not found at $hive"
        continue
    }

    try {
        # Load user hive if not already loaded
        if (-not (Test-Path "Registry::HKEY_USERS\$sid")) {
            Write-Host "Loading hive for SID $sid"
            & reg.exe LOAD "HKEY_USERS\$sid" $hive
            if ($LASTEXITCODE -ne 0) { throw "reg.exe LOAD failed with exit code $LASTEXITCODE" }
            $mounted = $true
            LogMessage "Loaded hive: HKEY_USERS\$sid"
        } else {
            LogMessage "Hive already loaded: HKEY_USERS\$sid"
        }

        # Apply each registry setting
        foreach ($name in $Values.Keys) {
            $exp      = $Values[$name]
            $fullPath = "Registry::HKEY_USERS\$sid\$RegKey"

            if (-not (Test-Path $fullPath)) {
                New-Item -Path $fullPath -Force -ErrorAction Stop | Out-Null
                LogMessage "[$sid] Created key: $RegKey"
            }

            New-ItemProperty -Path $fullPath -Name $name `
                -Value $exp.Data -PropertyType $exp.Type -Force -ErrorAction Stop | Out-Null
            LogMessage "[$sid] Set $name = $($exp.Data) ($($exp.Type))"
        }

    } catch {
        LogMessage "ERROR [$sid]: $_"
    } finally {
        # Unload hive if loaded
        if ($mounted) {
            Write-Host "Unloading hive for SID $sid"
            & reg.exe UNLOAD "HKEY_USERS\$sid"
            if ($LASTEXITCODE -ne 0) {
                LogMessage "WARNING [$sid]: reg.exe UNLOAD exit code $LASTEXITCODE"
            } else {
                LogMessage "Unloaded hive: HKEY_USERS\$sid"
            }
        }
    }
}

LogMessage 'HKCU deployment finished.'

# Stop transcript
Stop-Transcript | Out-Null

# Output accumulator for Intune
Write-Host $script:intuneOutput

As ALWAYS: Test before deploying

References and Documentation

Other Useful Posts

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *