After the third failed login script rollout in a single quarter, a financial services client brought us in to clean up their configuration management. The previous MSP had been pushing registry edits through a mix of legacy .reg files, GPO preferences, and ad-hoc remote sessions; nobody could tell which change had broken what. The fix was not exotic — it was teaching the team to manage the PowerShell Windows Registry interface as a first-class infrastructure surface, with the same discipline we apply to DNS zones or Active Directory schema changes.
That engagement is a good lens for this article. Registry editing looks trivial in a lab; in a managed environment with hundreds of endpoints, the difference between a working script and a Sunday-night rollback comes down to understanding how PowerShell maps the registry, why the provider model matters, and which cmdlets behave the way you actually expect.
Why the Registry Still Matters in 2026
Despite years of predictions that configuration would move entirely to JSON, YAML, or cloud-managed policy stores, the Windows Registry remains the canonical configuration database for Windows endpoints and servers. Group Policy writes to it. Intune configuration profiles eventually land in it. Most third-party Windows software still reads from it at startup. If you architect Windows infrastructure, you cannot avoid touching the registry — and if you touch the registry at scale, you need to do it through PowerShell rather than through regedit.exe.
Think of the registry as the foundation slab of a building. You rarely interact with it directly once construction is complete, but every wall, pipe, and electrical run depends on its integrity. A misplaced value in HKLM\SYSTEM is the equivalent of a hairline crack that nobody notices until the structure above it starts shifting. PowerShell gives you the equivalent of an engineer’s level and tape measure — precise, scriptable, and auditable.
Understanding the PowerShell Registry Provider
PowerShell does not treat the registry as a separate API surface. Instead, it exposes the registry through its provider model — the same abstraction that lets you navigate the filesystem, certificate stores, or environment variables using a unified vocabulary. The provider responsible here is the Registry provider, and it surfaces the hives as PowerShell drives.
Two drives are mounted by default in every PowerShell session: HKLM: for HKEY_LOCAL_MACHINE and HKCU: for HKEY_CURRENT_USER. Other hives — HKCR, HKU, and HKCC — are not mounted automatically, which trips up engineers who expect parity with regedit. You can add them yourself with New-PSDrive when you need them; I will show that in a later section.
The practical consequence of the provider model is that everything you already know about navigating files works against the registry. Set-Location, Get-ChildItem, Get-Item, and the wildcard expansion rules all apply. This is why I tell new engineers on the team to stop thinking about “registry cmdlets” as a separate skill — it is the same vocabulary, pointed at a different data store.
Navigating Hives Like a Filesystem
Start by changing into a hive the same way you would change into a directory:
# Move into the SOFTWARE branch of HKEY_LOCAL_MACHINE.
# Set-Location accepts the provider-qualified path; the trailing
# colon after HKLM tells PowerShell to use the Registry drive.
Set-Location -Path "HKLM:\SOFTWARE"
# Confirm where you are. Useful in scripts where the location
# may have been changed by a previous function call.
Get-Location
# List the subkeys directly beneath the current location.
# Get-ChildItem returns RegistryKey objects, not strings.
Get-ChildItem
The output of Get-ChildItem against a registry path looks similar to a directory listing, but each row represents a registry key, not a file. Keys are containers; values live inside them. This distinction matters because the cmdlets for reading keys differ from those for reading values, and beginners often conflate the two.
Reading Registry Keys and Values
Once you are pointed at the right hive, retrieval splits into two cmdlets. Get-Item fetches a key object — useful when you want metadata about the key itself, such as the number of subkeys it contains or its security descriptor. Get-ItemProperty retrieves the named values stored inside a key, which is almost always what you actually want.
# Retrieve the key object itself. Returns a RegistryKey,
# not the values inside it. Use this when you need metadata
# such as SubKeyCount or the underlying handle.
Get-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion"
# Retrieve a specific named value within the key. The -Name
# parameter filters to a single property; omitting it returns
# every value stored under that key as a PSCustomObject.
Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion" -Name "ProgramFilesDir"
That second command returns a PowerShell object with the property ProgramFilesDir set to the path of the Program Files directory. The return type is a PSCustomObject — not a string — so if you need the raw value for downstream logic, dereference the property explicitly:
# Pull just the string value, not the wrapping object.
# Without this dereference, comparisons and string formatting
# behave unexpectedly because you are working with an object.
$programFiles = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion" -Name "ProgramFilesDir").ProgramFilesDir
This is the single most common mistake I see in inherited scripts. Engineers assume Get-ItemProperty returns a string, write a comparison against a literal, and the script silently fails because the comparison is between an object and a string. A few extra characters now saves an hour of confused debugging later.
Creating and Removing Registry Keys
Creating a new key follows the same vocabulary as creating a directory. New-Item creates the container; Remove-Item deletes it. Because keys are containers, removing one removes everything beneath it — which is exactly the kind of operation that should make you pause before running it on a production host.
# Create a new key under HKEY_CURRENT_USER.
# New-Item returns the created RegistryKey object; pipe to
# Out-Null if you do not want it surfacing in script output.
New-Item -Path "HKCU:\Software\ExampleKey"
# Remove a key. The -Recurse switch is required if the key
# contains subkeys; without it, you will get a confirmation
# prompt or an error in non-interactive sessions.
Remove-Item -Path "HKCU:\Software\ExampleKey" -Recurse
I make every script that calls Remove-Item against a registry path go through a -WhatIf dry run during code review. The cmdlet is too quiet about destructive operations, and the registry has no recycle bin. If you delete the wrong key, your recovery path is restoring from a registry export or a system restore point — neither of which is fast in an incident.
Managing Registry Values
Values are the individual settings inside a key. PowerShell distinguishes between creating a new value and updating an existing one, but most engineers — myself included — use Set-ItemProperty for both because it creates the value if it does not exist and updates it if it does. The strict-purist alternative is New-ItemProperty for creation and Set-ItemProperty for updates, which is what the official documentation prefers.
# Set or create a string value under the key.
# -Type is critical: it controls the underlying registry data
# type (REG_SZ, REG_DWORD, REG_BINARY, etc.). Omitting it
# defaults to String, which is rarely what you want for
# numeric flags or binary blobs.
Set-ItemProperty -Path "HKCU:\Software\ExampleKey" `
-Name "ExampleValue" `
-Value "PowerShell" `
-Type String
# Remove an individual value without touching the key.
# Useful when rolling back a single configuration change.
Remove-ItemProperty -Path "HKCU:\Software\ExampleKey" -Name "ExampleValue"
The -Type parameter is the one I see misused most often. Windows reads many registry values as a specific data type, and PowerShell will happily write a string into a slot that the consuming service expects to be a DWORD. The service then either ignores the value or fails to start with a cryptic error. Always specify -Type explicitly when writing values that drive Windows behavior — String, ExpandString, Binary, DWord, MultiString, or QWord, matching whatever the documentation for that key calls for.
A More Realistic Example
The toy example above is fine for learning the syntax. In practice, you will compose these cmdlets into something more deliberate, like writing a build timestamp into a custom software branch as part of a deployment pipeline:
# Define the registry location and value metadata up front.
# Keeping these as variables makes the script easier to
# adapt for different environments or value names.
$regPath = "HKCU:\Software\MyCustomSoftware"
$regName = "BuildTime"
$regValue = "Build started at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
# Ensure the parent key exists before writing to it.
# Without this guard, New-ItemProperty fails if the path
# does not already exist.
if (-not (Test-Path -Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}
# Create the named value as a string. Use New-ItemProperty
# for explicit creation; this throws if the value already
# exists, which is sometimes what you want for auditing.
New-ItemProperty -Path $regPath `
-Name $regName `
-PropertyType String `
-Value $regValue | Out-Null
# Verify by reading the value back. Always verify writes
# in production scripts; the registry does not raise an
# exception if a value silently fails to persist.
$verify = Get-ItemProperty -Path $regPath -Name $regName
Write-Host "$regName is now set to: $($verify.$regName)"
That Test-Path guard is the difference between a script that works on a fresh image and a script that works everywhere. Treat the registry the way you would treat a database migration — never assume the target state, always check first, and verify after.
Exporting and Importing for Backup or Migration
For backup or migration, you can export a key tree to a .reg file. Recent PowerShell modules expose Export-RegistryFile and Import-RegistryFile for this; older environments may rely on shelling out to reg.exe export and reg.exe import, which is still entirely valid and works on every supported Windows version.
# Modern syntax — exports the key and all values beneath it
# into a .reg file that can be re-imported or hand-edited.
Export-RegistryFile -Path "HKCU:\Software\ExampleKey" `
-Destination "C:\Backups\ExampleKey.reg"
# Import the same file back onto another host. Useful for
# replicating a known-good configuration across a fleet.
Import-RegistryFile -Path "C:\Backups\ExampleKey.reg"
# Legacy fallback using reg.exe. Works everywhere, scripts
# can capture stdout for audit logs, and the .reg format
# is identical to what Export-RegistryFile produces.
reg.exe export "HKCU\Software\ExampleKey" "C:\Backups\ExampleKey.reg" /y
I export the relevant subtree before every registry change in a managed environment. It costs nothing, it takes under a second, and it has saved more than one incident response from escalating into a full rebuild. You can read more about how we approach orchestrated rollback in our piece on Veeam failover plans for DR scenarios — the same instinct of “capture state before you change state” applies here, just at a much smaller blast radius.
Why You Should Not Roll Your Own Hardening Script
Now for the opinionated part. Every few months a client asks us whether we can write “a single PowerShell script” that hardens the registry to CIS or DISA STIG benchmarks. The honest answer is that you can — but you almost certainly should not. A serious hardening script touches several hundred registry keys, each with conditional logic for OS version, server role, and existing policy state. The community scripts on GitHub that attempt this are routinely over a thousand lines, and the maintenance burden compounds with every Windows release.
A better architecture is to model each setting as a small, idempotent function — read the current value, compare against the desired state, write only if they differ, log every action. Compose those functions into modules grouped by domain (account policies, network settings, audit configuration). This is the approach we took for a hospital network we manage; it took longer to build initially, but four years on, the same modules survive Windows Server upgrades with minor adjustments rather than rewrites.
The trade-off here is build time versus run-time clarity. A monolithic script is faster to author and shorter to read; a modular library demands more discipline but produces an auditable trail when a compliance auditor asks why a specific setting changed. For regulated industries, that trail is non-negotiable. The NIST Cybersecurity Framework implicitly assumes you can demonstrate control state on demand — and you cannot demonstrate it from a 1,200-line script that nobody on the team fully understands.
Mounting the Other Hives
Earlier I mentioned that only HKLM: and HKCU: are mounted by default. When you need HKEY_USERS or HKEY_CLASSES_ROOT — common in user-profile migration work or COM registration troubleshooting — mount them yourself:
# Mount HKEY_USERS as a PowerShell drive for the session.
# -Persist is not supported for the Registry provider, so
# this only lasts until the session ends; add it to your
# profile script if you want it available by default.
New-PSDrive -Name HKU `
-PSProvider Registry `
-Root "HKEY_USERS"
# Now you can navigate it like any other drive.
Get-ChildItem -Path "HKU:\"
I keep these New-PSDrive calls in a shared profile script that is deployed to every administrative jump host. Engineers should not have to remember the incantation every time — the infrastructure should make the right thing easy. This is the same philosophy we follow when configuring policy enforcement on devices; see our write-up on Intune compliance policies for post-deployment enforcement for how the same principle applies further up the stack.
Working Against Remote Registries
Most of the cmdlets I have shown work against the local host. To target a remote machine, wrap the call in Invoke-Command against a PowerShell remoting session — which assumes WinRM is configured and you have the appropriate credentials.
# Read a value from a remote host. The script block runs
# on the target machine, so the HKLM: drive refers to the
# remote registry, not the local one.
Invoke-Command -ComputerName "FILE01.corp.local" `
-ScriptBlock {
Get-ItemProperty `
-Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" `
-Name "ProductName"
}
There is an older approach using the [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey .NET method, which does not require WinRM but does require the Remote Registry service running on the target. For modern fleets I default to PowerShell remoting; the legacy method is mostly useful when you inherit a network where WinRM has not been enabled and you cannot enable it quickly. The official guidance on PowerShell remoting from Microsoft Learn is the right starting point if you are building this out for the first time.
Caveats Worth Knowing
A few honest limitations to keep in mind:
First, registry redirection. On 64-bit Windows, 32-bit processes see a redirected view of HKLM\Software under HKLM\Software\WOW6432Node. If you launch the 32-bit PowerShell host on a 64-bit machine, your registry writes will land in the redirected location, which is rarely what you want. Stick to the 64-bit PowerShell host unless you have a specific reason not to, and verify your writes from a separate session.
Second, permissions. Set-ItemProperty does not elevate. If the key requires administrative rights to modify, you need to be running PowerShell as administrator — or, for keys under HKLM\SYSTEM, sometimes as SYSTEM itself via a scheduled task or PsExec. PowerShell will return an access denied error rather than silently failing, but the error message is not always specific enough to make the cause obvious.
Third, transactional semantics. The registry supports transactions through the .NET API, but PowerShell’s standard cmdlets do not expose them cleanly. If you need atomic multi-key updates, you either accept the risk of partial writes and design idempotent retry logic, or you drop down to the .NET API directly. For most operational work, idempotent retry is the more pragmatic choice — easier to test and easier for a teammate to maintain.
Finally, performance at scale. Reading a few values is instant; reading every key under a deep tree across hundreds of machines is not. If you are doing fleet-wide registry queries, batch them through a single remoting invocation per host rather than one cmdlet call per value, and consider whether a configuration management tool — Intune, DSC, Ansible — should own that workload instead of an ad-hoc script.
Practical Takeaway
Treat the registry the same way you treat any other production data store: read with intent, write with verification, back up before you mutate, and log everything. The PowerShell vocabulary is small — six or seven cmdlets cover ninety percent of real-world work — but the discipline around when and how you use them is what separates a working script from one that survives its second year in production.
If your team is inheriting an environment with undocumented registry-driven configuration, or you are trying to bring registry changes under the same change-control rigor as the rest of your infrastructure, we can help architect that. Most engagements start with an audit of what is currently being written, by whom, and on what cadence — and the answers are almost always more interesting than the client expected.


