Someone opens a ticket that reads like a five-minute job. "Please make Adobe Acrobat the default for PDF files on all machines." You have done harder things before breakfast. So you write six lines of PowerShell that set a registry value, you push it, and by the next morning every affected user has a toast in their Action Center that says An app default was reset. Every PDF still opens in Edge. Your script ran successfully. Windows just undid it.
This is not a bug and it is not a permissions problem. Windows deliberately guards the user's choice of default app with a per-user cryptographic hash, and it re-validates that hash every time a user signs in. If the hash does not match what Windows expects, Windows concludes that something tampered with the setting and reverts to its own default. That single design decision is why "set the default PDF handler" is one of the most-asked and least-understood questions in Windows endpoint management. This post walks the whole model from the ground up: what an association actually is, where every piece of it lives, why the hash exists, the supported way to set defaults at scale, and the difference between setting a default once and enforcing it forever.
Every default app choice is stored per user as a ProgId plus a Hash under the UserChoice key, and the shell validates that hash at sign-in. Writing the registry directly gives you a ProgId with no valid hash, so Windows detects the tamper and resets the association. The supported route is an XML association file exported with Dism /Online /Export-DefaultAppAssociations, then either imported into an image (seeds the first sign-on only) or applied through the Set a default associations configuration file policy (reapplies at every sign-in). A user can still change an enforced default, and it will hold until they next sign in.
The problem: the script runs, and Windows undoes it
Let us be precise about the failure, because the symptom is easy to misread.
You want .pdf files to open in Acrobat. You go looking for where that setting lives. You find it quickly, because it is not hidden. It sits in the current user's registry hive, one key per extension, and it holds the name of the program that handles that extension. You write that value. Nothing errors. You sign the user out and back in.
Three things then happen, in this order. First, the association reverts to whatever Windows thinks the default should be. Second, the user gets a notification saying an app default was reset. Third, an event is written to a log channel almost nobody looks at, recording precisely what happened and why.
The important detail is that nothing failed. Your write succeeded. Windows read the value back, ran a check on it, decided the value had not been set by the user through the proper user interface, and rolled it back. Microsoft documented this behaviour in support article KB4001770 as "Reset app default when a registry setting is deleted or corrupted and streamlined notification about the corruption."
That wording is worth reading twice. From Windows' point of view, your hand-written registry value is not a configuration change. It is corruption.
Context: why Microsoft did this. Before Windows 10, an application could set itself as the default handler for a file type by writing the registry, and plenty of installers did exactly that without asking. Microsoft's own guidance on the change is blunt about the design goal: "The main requirement for default file association is often forgotten: the end-user is in control." The hash is not there to make your life difficult. It is there so that a PDF reader you installed in 2019 cannot silently steal .html from your browser.
Why it happens: the association model and the hash that guards it
To fix this properly you need the whole model, not just the one key. There are four layers, and they are consulted in a specific order.
Layer 1: the ProgId, which is the name of a handler
A ProgId (programmatic identifier) is a short string that names a way of opening a file. AcroExch.Document.DC is a ProgId. So is ChromeHTML, and so is MSEdgeHTM. Modern Store-packaged apps get machine-generated ProgIds that look like AppX4ztfk9wxr86nxmzzq47px0nh0e58b8fw, which is why the Notepad handler in your registry looks like line noise.
ProgIds are registered under HKEY_CLASSES_ROOT. That subtree is not a real hive. Microsoft documents it as "a view formed by merging HKEY_CURRENT_USER\Software\Classes and HKEY_LOCAL_MACHINE\Software\Classes". Machine-wide registrations go in the HKLM half; per-user registrations go in the HKCU half. Microsoft also states plainly that "in general, HKEY_CLASSES_ROOT is intended to be read from but not written to".
Under the ProgId you find the friendly name in the key's default value, and the actual command line under shell\open\command. That command line is what finally launches when someone double-clicks a file.
Layer 2: the extension key, which is the machine's opinion
Each file extension gets its own key directly under HKEY_CLASSES_ROOT, including the leading period. Its default value holds a ProgId. Microsoft's file types documentation gives the shape as extension=ProgID and adds an important caveat: "Windows respects the Default value only if the ProgID found there is a registered ProgID. If the ProgID is unregistered, it is ignored."
Beside that default value sits a subkey called OpenWithProgids. Microsoft describes it as "a list of alternate ProgIDs for this file type. The programs for these ProgIDs appear in the Open with menu". Applications are supposed to add themselves to this list rather than seize the default. This is the polite, supported way for an app to say "I can also open these".
Layer 3: UserChoice, which is the user's opinion and it wins
Here is the layer that matters. When a user picks a default through Settings, Windows writes it to a per-user key, and that key overrides everything in layer 2.
For file extensions the parent key is:
For URL protocols such as http or mailto it is a different tree entirely, which is the single most common reason a "default browser" script only half works:
Both of those keys carry exactly two values that matter. ProgId names the handler. Hash is a short base64 string that proves the ProgId was written by the shell on behalf of the signed-in user, on this machine, for this extension.
Layer 4: the hash check at sign-in
At each sign-in the shell walks the UserChoice keys and recomputes the expected hash for every one of them. If a stored hash does not match the computed hash, the entry is treated as tampered. Windows resets that association and, depending on the situation, shows the user a notification.
The algorithm that produces the hash is not documented by Microsoft. There is no supported API, no cmdlet, and no published specification for generating a valid hash for an arbitrary ProgId. Community projects have reverse-engineered it, and they generally work, but you are then depending on undocumented behaviour that Microsoft is free to change in any monthly update. Do not build an enterprise standard on it.
The COM interface that used to do this job tells the same story in Microsoft's own words. IApplicationAssociationRegistration::SetAppAsDefault is documented as setting an application as default "provided that the application's publisher matches the current default's", returning E_ACCESSDENIED when it does not, and is flagged in the documentation as "Not intended for use in Windows 8." The programmatic door was closed a long time ago and clearly labelled.
Gotcha: Windows 11 made this per-extension, and that changed the shape of the request. In Windows 11 the Settings app exposes defaults primarily by file type. Microsoft's guidance is to "type the file extension or protocol you wish to change, such as .txt" and pick an app, or select an app and change individual file and link types from there. There is no single switch that makes an application the default for everything it claims. Even the browser Set default button covers a defined list, not everything. For devices in the European Economic Area, Microsoft documents that button as setting link types ftp, http, https and read, plus file types .htm, .html, .mht, .mhtml, .shtml, .svg, .xht, .xhtml and .xml. If your ticket says "make X the default", your first job is to turn that into a list of extensions and protocols.
The chain, end to end
Put together, a double-click resolves like this.
- Explorer takes the file's extension, for example
.pdf. - It looks for a
UserChoicekey for that extension in the current user's hive. - If one exists, the shell validates the
Hashvalue against theProgIdvalue. - If the hash is valid, the ProgId in
UserChoicewins and nothing else is consulted. - If the hash is invalid or missing, the entry is discarded, an event is logged, and the association falls back to the machine-level default.
- With no UserChoice at all, the default value of the
HKEY_CLASSES_ROOT\.pdfkey decides, provided that ProgId is registered. - The winning ProgId's
shell\open\commandis executed with the file path.
Registry reference for the whole feature area
These are the keys and values involved in file and protocol associations, machine and user. Paths are shown relative to the roots named in each row.
| Key or value | Type | What it does |
|---|---|---|
DefaultAssociationsConfiguration (in the policy parent key above) | REG_SZ | The Set a default associations configuration file policy. Holds the path to the association XML. Confirmed as the value name in the shipped WindowsExplorer.admx. |
HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer\NoNewAppAlert | REG_DWORD | Backs the Do not show the 'new application installed' notification policy. Suppresses the toast a newly installed handler triggers. |
HKCU\...\Explorer\FileExts\<.ext>\UserChoice, values ProgId and Hash | REG_SZ | The per-user default for one file extension, plus the hash that proves the shell wrote it. Overrides the machine registration. |
HKCU\...\Explorer\FileExts\<.ext>\OpenWithProgids | subkey | Per-user list of alternate handlers offered in Open with. |
HKCU\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\<protocol>\UserChoice | REG_SZ | The same mechanism for protocols such as http, https, mailto, ftp. A separate tree from file extensions. |
HKCR\.<ext> default value | REG_SZ | Machine or user-level ProgId registration for the extension. Ignored if the ProgId is not registered. |
HKCR\.<ext>\OpenWithProgids | subkey | Documented list of alternate ProgIDs shown in Open with. Where a well-behaved installer adds itself. |
HKCR\<ProgId> default value, and \shell\open\command | REG_SZ | Friendly name and the actual command line for the handler. |
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts | REG_DWORD per entry | Tracks which association toasts have been shown to this user. Observed on a live device and not documented by Microsoft; read it for context, never build detection on it. |
Do not write UserChoice by hand. Setting ProgId without a matching Hash, or copying a Hash from another machine or another user, produces exactly the state Windows is designed to reject. You get a reset at the next sign-in, a toast for every affected extension, and on a machine with a long extension list that means a notification storm. Microsoft's own guidance on this is one sentence long: "Stop using script or other pre-Windows 10 ways for configuring file association." Deleting UserChoice keys is equally risky, because that is indistinguishable from the corruption case and triggers the same reset path.
System files and binaries in the flow
| File | Role | Notes |
|---|---|---|
C:\Windows\System32\shell32.dll | The shell library that resolves an extension to a handler and launches it. | Version 10.0.26100.8972 on the Windows 11 25H2 device used for this article. |
C:\Windows\System32\windows.storage.dll | Carries much of the modern association and file-type plumbing that used to live in shell32. | Ships in lockstep with shell32.dll. |
C:\Windows\System32\shsvcs.dll | Registered message and resource file for the Microsoft-Windows-Shell-Core event provider. | This is where the AppDefaults event strings come from. |
C:\Windows\System32\Dism.exe | Exports, imports, lists and removes default app associations. | The only supported tool for producing the association XML. |
C:\Windows\System32\OEMDefaultAssociations.xml | The image-level association file. Dism /Import-DefaultAppAssociations writes here. | Present on stock Windows 11. Ships with OverwriteOnVersionMax and OverwriteIfProgIdIs attributes on inbox entries. |
shobjidl_core.h / IApplicationAssociationRegistration | The legacy COM surface for querying and setting defaults. | QueryCurrentDefault still reads. SetAppAsDefault is documented as "Not intended for use in Windows 8." |
assoc and ftype | Built-in commands for viewing extension-to-ProgId and ProgId-to-command mappings. | Internal to cmd.exe, not standalone executables. They read and write the machine registration, not UserChoice, so they cannot set a user's default. |
Context: there is no service and no scheduled task for this. Association resolution happens inside the shell process that needs it, and the hash validation runs as part of sign-in processing by Explorer. There is no dedicated Windows service to check, and there is no task under \Microsoft\Windows\... that drives file associations. If you are troubleshooting, do not go looking for a stopped service. The nearest thing to a moving part is the sign-in itself, which is why "sign out and back in" is a real diagnostic step here rather than a brush-off.
How to verify: read the four places that decide
Before changing anything, establish what the device currently believes. There are four sources of truth and they can disagree.
1. Read the live UserChoice values with PowerShell
This reads the per-user default for a handful of extensions and protocols, and reports whether a Hash value exists beside each ProgId. Run it as the signed-in user, not as an elevated different account, because UserChoice is per user.
Protocols live somewhere else, so check them separately. Forgetting this is why half-working default-browser scripts are so common.
2. Look at the keys in Registry Editor
Sometimes you just want to see it. Open Registry Editor and paste the path into the address bar.
3. Ask DISM what the image thinks
DISM reads and writes the image-level association list, which is a different thing from the user's live choice. Both commands below are read-only.
4. Read the AppDefaults event channel
There is a dedicated channel for exactly this feature, and almost nobody knows it exists. Microsoft's own guidance points at it: "You can check the Microsoft-Windows-Shell-Core/AppDefaults event log for clues about file associations reset."
The event identifiers below were read from the Microsoft-Windows-Shell-Core provider's own manifest on a Windows 11 25H2 device, using Get-WinEvent -ListProvider. The strings are Microsoft's, shipped in shsvcs.dll. Microsoft does not publish this table on learn.microsoft.com, so treat the numbers as version-specific and re-read the manifest on your own build before you write an alert rule against them.
| ID | Manifest string | What it tells you |
|---|---|---|
| 62440 | Hash mismatch detected for: %1. ProgId: %2. UserSid: %3. HashInRegistry: %4. ComputedHash: %5. | The smoking gun. Something wrote a UserChoice ProgId without a valid hash. The extension and the ProgId in the event name the culprit. |
| 62441 | User choice has been reset to prog id %1 for %2. CurrentDefaultProgId: %3. ShouldToast: %4 | Windows reverted the association. ShouldToast tells you whether the user was notified. |
| 62442 | Upgraded to prog id %1 from prog id %2 for %3 | A legitimate handler upgrade, for example an inbox app superseding an older one at OS upgrade. |
| 62443 | AppDefault Info: %1 | Informational. Carries the sign-in messages, including the hash version check. |
| 62444 | Missing Hash -- ProgId: %1 FileExtOrUriScheme: %2 | A ProgId with no hash at all. This is the exact shape a hand-written registry change leaves behind. |
| 62445 | Migration Info: %1 | Informational, emitted around profile and OS migration. |
Log files
Association handling itself is event-driven rather than file-logged, but DISM keeps a text log, and that is where an XML import failure shows up.
| Path | Search for | Healthy versus broken |
|---|---|---|
C:\Windows\Logs\DISM\dism.log | DefaultAppAssociations | Healthy: the operation is logged with a success result and the XML path you passed. Broken: an error immediately after the path, usually because the file is unreachable or is not valid XML. |
C:\Windows\Logs\DISM\dism.log | Import-DefaultAppAssociations | Confirms the import ran at all. Silence here after a task-sequence step means the step never executed. |
Gotcha: an empty /Get-DefaultAppAssociations is not a clean bill of health. That command reports the custom list applied to the image. Users get their defaults from their own UserChoice keys, seeded at first sign-on and possibly overridden by policy since. A device can show nothing from DISM and still have every user opening PDFs in Acrobat. Always read the user hive as well.
The fix: export the XML, then choose once or enforced
The supported route has three steps: build a reference device, export an XML from it, and then decide how that XML reaches your fleet. The third step is where most projects go wrong, because the two delivery mechanisms behave completely differently.
Step 1: set the defaults on a reference device, through the UI
Install the applications you care about on a clean device of the same Windows version as your fleet. Then set the defaults by hand, in Settings. This is the one place where clicking through the UI is not a shortcut but the actual requirement, because only the UI produces valid hashes.
Two traps here, both documented by Microsoft. First, on a brand-new reference machine the exported XML "may be truncated until the delay-install apps have fully installed", so wait 10 to 30 minutes and open the inbox apps before exporting. Second, if only one application on the device can handle a type, "that application will appear as the default for that type, even though there is no explicit choice in the registry", and "exporting the default applications to XML will not gather these implied settings". To capture an implied default you must select it explicitly, even though the UI already shows it.
Step 2: export the XML with DISM
Run this as the same user account that set the defaults in step 1. Microsoft is explicit that using a different account produces a malformed file.
The result is a flat list of Association elements. Each carries an Identifier (the extension or protocol), a ProgId, and a human-readable ApplicationName.
Step 3a: one-time defaults, by importing into the image
If you want to hand users a sensible starting point and then leave them alone, import the XML into the image. Microsoft's description of the switch is the whole story: "Imports a set of default application associations to a specified Windows image from an .xml file. The default application associations will be applied for each user during their first logon."
Step 3b: enforced defaults, by policy
If you need the association to come back after a user changes it, you need the policy. The setting is Set a default associations configuration file, it takes a path to the XML, and the shipped administrative template describes its behaviour precisely: "If this group policy is enabled and the client machine is domain-joined, the file will be processed and default associations will be applied at logon time." It then adds the sentence that answers the question everyone asks: "If the policy is enabled, users will still be able to override default file type and protocol associations, but on next logon the file will be reapplied."
| Question | DISM import into image | Policy XML |
|---|---|---|
| When does it apply? | At each user's first sign-on only. | At every sign-in, for every entry without Suggested="true". |
| Can the user change it afterwards? | Yes, permanently. | Yes, but only until they next sign in. |
| Effect on existing profiles | None. | Applies to every user who signs in while the policy is in force. |
| Can the XML be a partial list? | No. A missing entry triggers reset notifications. | Yes. This is the supported way to force only a subset. |
| Where the setting lives | C:\Windows\System32\OEMDefaultAssociations.xml | DefaultAssociationsConfiguration under the policy key. |
Tip: use two files, and let the policy carry the short one. Microsoft's own guidance is to keep one complete XML for the image import, containing every association the OS already had plus your changes, and a second, deliberately short XML containing only the handful of types you actually want to force. The complete file goes in via Dism /Import-DefaultAppAssociations and keeps the reset notifications quiet. The short file goes in the policy and enforces just your subset. At first sign-on the shell applies both. This is also the answer to "we want Acrobat for PDF but we do not care about anything else".
Group Policy walkthrough
- Copy your association XML to a location every device can reach. A read-only UNC path such as
\\contoso-fs01\NETLOGON\AppAssoc.xmlworks, and so does a local path you stage with the same package that installs the applications. - Open the Group Policy Management Editor on a GPO linked to the target computers. This is a Computer Configuration policy, so linking it to a user OU will do nothing.
- Navigate to Computer Configuration > Administrative Templates > Windows Components > File Explorer.
- Open Set a default associations configuration file and select Enabled.
- In Default Associations Configuration File, enter the full path to the XML. This is a path, not the XML content.
- Click OK, then confirm on a test device with
gpupdate /forcefollowed by a full sign-out and sign-in. Agpupdatealone will not reapply associations, because the work happens at logon. - While you are in the same node, consider enabling Do not show the 'new application installed' notification to suppress the toast that fires when an installer registers a new handler.
Intune walkthrough
There are two ways to deliver this from Intune, and they take different input.
- Go to Devices > Configuration > Create > New policy, platform Windows 10 and later, profile type Settings catalog.
- In the settings picker, search for
Default Associations Configuration. The setting sits under the Administrative Templates > Windows Components > File Explorer category, mirroring the GPO, and under Application Defaults for the CSP-backed form. - Enable the setting and supply its value. For the administrative-template form this is the path to the XML, exactly as in Group Policy.
- Assign to a device group. This node is device-scoped: Microsoft documents the CSP scope as Device, not User.
- For the CSP form, create a Custom profile instead and add an OMA-URI setting. The node is
./Device/Vendor/MSFT/Policy/Config/ApplicationDefaults/DefaultAssociationsConfiguration, data type String. - The CSP data is base64-encoded XML content, not a path. Microsoft's instruction is unambiguous: "The file then needs to be base64 encoded before being added to SyncML." Encode the whole file, paste the result as the value.
- Sync a pilot device, sign out and sign in, and verify with the companion script below.
Gotcha: the two delivery paths take different input, and Microsoft documents one applicability limit you must read carefully. The administrative template takes a path; the CSP node takes base64-encoded XML. Both map to the same registry value name, DefaultAssociationsConfiguration under Software\Policies\Microsoft\Windows\System. Microsoft does not document which form lands in that value when the setting arrives by MDM, so read the value on a pilot device and confirm before you assume. Separately, the ADMX text says the file is processed "if the client machine is domain-joined", while the CSP text says associations are applied "if policy is enabled and the client machine is Microsoft Entra joined". Neither sentence mentions Entra-registered or workgroup devices. Pilot on a device that matches your real join state rather than trusting either sentence to cover you.
What happens to a user who tries to change it
Nothing stops them. The Settings UI still works, the change writes a valid hash, and files open in their chosen app for the rest of the session and every session until they sign out. At the next sign-in the policy XML is processed again and their choice is replaced for any identifier the XML names. Identifiers the XML does not name are left alone permanently.
If you want a default that is a strong suggestion rather than a monthly argument, that is what the Suggested attribute is for. Microsoft documents it as: "The default value is false. If it's false, the Association is applied on every sign-in. If it's true, the Association is only applied once for the current DefaultAssociations Version." Increment the Version attribute on the root element and the suggested entries apply once more, on the next sign-in. Those two attributes arrived in Windows 11, version 22H2.
Context: Defender has nothing to do with this. There is no Defender, ASR, exploit protection, WDAC or firewall setting that controls file associations, and no Endpoint Security profile to check. The hash validation is a shell integrity mechanism, not a security product feature. The one genuine overlap is indirect: if you use WDAC or AppLocker to block an executable, its ProgId can still hold the association and users get a launch failure rather than a fallback handler. That is worth knowing, but it is not a Defender setting.
Proof it worked: real output from a live device
The companion script is Get-FileAssociationState.ps1. It reads the policy value, resolves and parses the XML if one is in force, reads every relevant UserChoice key, resolves each ProgId to its friendly name, counts recent AppDefaults events, and flags any identifier where the policy and the live value disagree. It writes nothing. It never sets a UserChoice value and never calls a DISM import or remove verb. It needs no PowerShell module and runs on both Windows PowerShell 5.1 and PowerShell 7.
The output below is a genuine run on a Windows 11 25H2 device with identifiers replaced. This device has no association policy applied, which is the baseline you should expect before you deploy anything.
Now the interesting run. Passing -PolicyXmlPath compares the live values against a candidate XML without deploying it, which is how you test an XML before it reaches users. This XML asks for Edge on .htm, .html, http and https, while the device is set to Chrome.
Read the verdicts like this. Match means the policy applied and the user has not fought it. Drift means either the user changed the default since their last sign-in, or the policy never reached them at all. NotInPolicy means you deliberately left that type to the user. NoUserChoice means nobody ever chose, which is normal and often means only one handler exists.
Two follow-ups turn a Drift into a diagnosis. Check the AppDefaults channel for events 62440 and 62444 on that device: if they are present, something is writing UserChoice values directly and you have an application problem, not a policy problem. If they are absent and the drift is on every identifier, the policy is not being processed, so check the join state, the XML path reachability from the user's context, and whether the device has actually had a fresh sign-in since the policy landed.
Tip: make the XML path a read-only, always-available location. A policy that points at an unreachable file does not error and does not warn. It simply does nothing at sign-in, and every association silently stays wherever it was. The companion script calls this out explicitly rather than reporting a clean result, because "no drift" and "no comparison possible" are very different answers. Stage the XML locally alongside the application package if any of your users are off the corporate network.
Community deep dives
| Author | Focus | Link |
|---|---|---|
| Martin Bengtsson | Deploying the association XML through the Intune settings catalog, including the base64 conversion step. | imab.dk |
| Donna Ryan (MSEndpointMgr) | Importing an exported default app association XML into an offline image as part of image build automation. | msendpointmgr.com |
References
- Export or Import Default Application Associations - Microsoft Learn. The export and import procedure, plus the four tips on truncated exports, implied defaults and upgrade behaviour.
- DISM Default Application Association Servicing Command-Line Options - Microsoft Learn. Exact syntax for the four switches, and the statement that imported associations apply at each user's first logon.
- ApplicationDefaults Policy CSP - Microsoft Learn. The OMA-URI, the base64 requirement, the Version and Suggested attributes, and the Group Policy mapping including the registry key name.
- File Types - Microsoft Learn. The HKEY_CLASSES_ROOT registration model, OpenWithProgids, and the merged-view behaviour of HKCR.
- IApplicationAssociationRegistration::SetAppAsDefault - Microsoft Learn. The publisher-match requirement, E_ACCESSDENIED, and "Not intended for use in Windows 8."
- Windows 10 - How to configure file associations for IT Pros? - Microsoft Learn archive. The tamper-detection rationale, the AppDefaults event channel, and the two-file technique for forcing a subset.
- Change default programs in Windows - Microsoft Support. The per-extension and per-app routes in the Windows 11 Settings app.
- Updates to Windows for the Digital Markets Act - Windows Insider Blog. The exact list of link and file types the browser Set default button covers in the EEA.
- Local sources verified on the device used for this article:
C:\Windows\PolicyDefinitions\WindowsExplorer.admxand its en-US ADML for the policy key, value name and behaviour text, andGet-WinEvent -ListProvider Microsoft-Windows-Shell-Corefor the AppDefaults event manifest.
Download it from Imran76Awan/Windows-11-Scripts — no sign-in required. It is read-only: it reports and never changes a device or anything in Intune. Validate it in your own environment before relying on the output.