Decoding the Intune Win32 App GRS Hash

When a Win32 app fails to install, the Intune Management Extension records the failure under a GRS key on the device and won’t re-evaluate that app for roughly 24 hours. The advice you’ll find in every forum thread is to delete the registry key, unassign and reassign the app, or reboot the device.

None of that is necessary. Open the app in Intune, click Save without changing anything, and the app is re-evaluated at the next check-in.

The GRS subkey name is a SHA-256 hash, and one of the fields going into it is MetadataVersion. Any save increments that value. The input changes, so the hash changes, and the resulting subkey has no failure recorded against it. IME finds no cooldown and tries again.

The install command line is not in that hash. Neither is the uninstall command, the detection rule, the requirement rules, the return codes, or the display name. So the popular explanation (the hash covers your app settings, so fixing a broken setting makes it retry) describes a mechanism that doesn’t exist. Fixing the setting does clear the cooldown, but only as a side effect of having saved the app on your way out.

Note: No decompiled source is reproduced here. This post describes observed behavior and the field composition of a hash. The IME assemblies are plain .NET and readable with any decompiler, which is how the field list was established, but the interesting part is that all of it reproduces from data already sitting on an enrolled device.


Where the information lives

IME keeps retry and backoff state for Win32 apps here:

HKLM\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps\{user}\GRS\{hash}

The subkey name is 44 base64 characters, which decodes to 32 bytes. That’s a SHA-256 digest, so the question is what goes into it.

That question is relevant because the standard remedies only make sense if the hash covers something a portal edit doesn’t touch. If it did cover the install command, editing the install command would produce a new subkey, and nobody would need to delete registry keys.

How the subkey is built

The hash isn’t computed over a single app. It covers a subgraph: an app together with its flattened dependency and supersedence closure. Connected components of the app relation graph are computed first, and each component is hashed as a unit. That’s why one app can end up owning several GRS subkeys.

For a given subgraph:

  1. Each policy in it is reduced to a comma-joined list of fields.
  2. Those strings are sorted, then joined with commas. App ordering inside a subgraph can’t affect the result.
  3. SHA-256 over the UTF-16LE bytes of that string.
  4. Base64 of the digest.

The per-policy field list, in order:

#FieldNotes
1IdApp GUID
2Version
3MetadataVersionThe one that moves on any save
4AvailableAppEnforcementNormalized: 1→0 and 3→2, so those pairs are indistinguishable
5Intent
6Targeted
7DOPrioritySerialized as the enum name, e.g. NotConfigured
8ToastStateOnly if set
9RebootExOnly if set. JSON, properties ordinally sorted
10StartDeadlineExOnly if set. JSON, properties ordinally sorted
11AppApplicabilityStateDueToAssginmentFiltersOnly if set (Microsoft’s spelling, not mine)
12RelationVersion
13newFlatDependenciesTrue/False
14FlatDependenciesOnly if set. Sorted by AppId, ChildId, Type, Action, Level
15ScriptsOnly if set
16Evergreen installer VersionIdEvergreen apps only

Not present anywhere in that list: install command, uninstall command, detection rules, requirement rules, return codes, display name, setup file path. The user isn’t in there either, even though the user GUID sits in the registry path above GRS.

IME receives all of those fields on the policy object and excludes them from the hash anyway.


Reproducing it from a live device

The algorithm reproduces from data already present on one enrolled workstation, with no writes to anything.

Recomputing the preimage from the policy in the IME logs produced the exact registry subkey name for seven apps. An eighth key appears only in a log line (Did not find any previous subgraph reevaluation time at key …) and not in the registry at all, and that one matched too.

Two things fell out of the reproduction before the field list was read:

The same two GRS keys appear under two different user-context GUIDs for the same app, which is what you’d expect if the user isn’t part of the input.

One user context held three GRS keys for a single app that was still assigned and healthy. Something had changed the hash input three times without the app ever being reassigned.


Test 1: Editing the Install Command Line

Reproducing a hash from existing data is one thing. Predicting a subkey that doesn’t exist yet is a better test, so each expected value was computed and written down before the change was made.

The test app writes a file and calls exit /b 1, and that’s the whole app. Assigned Required to one device, left to fail once.

Baseline before the edit:

InstallCommandLine  install.cmd
Version 1   MetadataVersion 1   RelationVersion 0
GRS key   5Nhyy9sV9nLHHCfYAgLksr+hQcuj2kFV2IGbNYi48u4=   (computed AND in registry)
State     EnforcementState 5000, 0x80070001
Attempt   16:33:55

Predictions, recorded first:

HypothesisExpected subkey
MetadataVersion 1→2 (predicted)fVusV8U6ofxRGODnwtYrsB2ItwllcbPN7dxq/YwooDE=
Version 1→253ec9BRWU4wQs1HQRREIH8CVDzYO1VhvOOtuZA3jPto=
Neither movesUnchanged

Then the install command was changed to install.cmd -x and nothing else.

ClaimPredictedObserved
VersionUnchanged1
MetadataVersion1 → 22
RelationVersionUnchanged0
New subkeyfVusV8U6…fVusV8U6…
Old subkeyOrphaned, retainedRetained, frozen at its old evaluation time
Any unexpected subkeyNoneNone
Retry inside the cooldownYesYes

So the cooldown clears, but not because the install command is in the hash. It clears because MetadataVersion moved.


Test 2: A Field the device never sees

The practical follow-up question was whether there’s something harmless an admin can edit to clear a stuck cooldown, without touching install or detection logic.

The client policy only contains device-relevant fields. Description, notes, owner, developer, category and logo aren’t in it at all. That sets up a clean fork: if MetadataVersion tracks the client-facing policy, then editing the Description changes nothing the device receives, and the cooldown should survive.

Prediction, recorded before the edit: MetadataVersion stays at 2 and the subkey stays fVusV8U6…. My hopes weren’t high, but the test was cheap. A version field named for metadata delivered to the client has no obvious reason to move when the client-facing content is identical.

That prediction was wrong. Changing only the Description bumped MetadataVersion from 2 to 3, and the subkey became Ll8rBNa9mH3XNRsd+qM7Nc2ijEqYd5q2YMAxW2UzKQg=, which is exactly the value precomputed for MetadataVersion=3 before the edit. The app retried at the next check-in.

The hash model held. The model of what drives the version field did not. MetadataVersion tracks saves of the app, not changes to the part of it the device receives.

Diagnostic Note: A Description change leaves no trace in the client policy, unlike the install command edit. If the version hadn’t moved, “no change observed” would have been indistinguishable from “IME hasn’t re-fetched yet”, and the negative result would have been worthless. The bump proved the policy was fresh and removed the need for a control. Any further no-op test here needs a positive control built in from the start.


Test 3: Saving with no changes at all

That left one question: does the bump need an actual value change, or does any save do it? A portal could reasonably skip the write when nothing differs.

Prediction, recorded before the save: no bump. I expected to be disappointed here too, on the reasoning that portals generally don’t issue writes for identical values.

Wrong again. Opening the app and clicking Save without touching a single field bumped MetadataVersion from 3 to 4, produced the precomputed subkey eO/aFPZEcLRC8hLJXusEVrXFy2wl9dLeud2VIog5/nI=, and triggered a fourth install attempt.

Results

EditMetadataVersionSubkeyRetried
Install command line1 → 2fVusV8U6… as predictedYes
Description (never sent to the device)2 → 3Ll8rBNa9… as predictedYes
Nothing at all, just Save3 → 4eO/aFPZE… as predictedYes

Four subkeys now exist for one app, one per save. None of the superseded ones got cleaned up, at least not within the days I was watching, so a device accumulates one dead GRS entry per app, per save. That’s a few registry keys and not worth worrying about, but it does mean a device can hold far more GRS keys than it has assigned apps, and only one of them per app is live.


What did the tests actually prove

The hash model went three for three. Every subkey was computed before the change and matched exactly, and no unexpected subkey ever appeared. Combined with the eight reproductions from existing data, I’m comfortable calling that verified.

My model of what drives MetadataVersion went zero for two. I predicted a non-client-facing field wouldn’t bump it (wrong), then that a no-op save wouldn’t bump it (wrong). The field is a lot more eager than its name suggests.

The conclusions here rest on the first model. MetadataVersion being part of the hash input is confirmed by reproduction. When the service decides to increment it is server-side behavior that’s been sampled, not derived.


What to do with a stuck app

Open the app in the Intune portal and click Save. Nothing needs changing, so there’s nothing to revert and no configuration drift.

If some part of the UI insists on a modification before it will let you save, edit the Description. It never reaches the device, so it can’t affect installation, detection, requirements or reporting. It’s the safest field in the app.

Deleting registry keys, unassigning and reassigning, and rebooting the device all work, in the sense that eventually the app retries. They’re just more work than clicking Save.

Caveats

The 23-minute retry figure from Test 1 is a result of forcing. The IME service was restarted to force check-ins, which accelerates polling and not the cooldown. The defense is that the app retried at the first check-in after the policy changed rather than waiting out 24 hours. Unforced, the observed interval was 61 minutes.

Version was never exercised. It stayed at 1 through a property edit, which fits the idea that it tracks content rather than metadata, but that hasn’t been tested by uploading a new installer. Don’t quote me on what Version tracks until I get around to testing that too.

Subgraphs with dependencies weren’t reproduced. All eight reproductions were single-app subgraphs, so field 14 (FlatDependencies) is described from the implementation and not confirmed against a real key.

One tenant, one device, one IME build (1.104.102.0). Nothing here says anything about behavior across versions.

Every save I tried bumped MetadataVersion, including the one where I changed nothing at all. That’s still just three saves on one app, and all of them went through the portal. A save issued through Graph might behave differently, and I haven’t tested that.

Incidental findings

The {user} path segment is the Entra object ID. Published sources calling it a SID are also correct: an Entra-backed S-1-12-1-a-b-c-d SID is a re-encoding of the same GUID. Decoding the machine’s SID produced a match against a Win32Apps subkey. Two descriptions of the same bytes, and the literature has been arguing about it for years.

MDM sync scheduled tasks don’t trigger a Win32 app policy poll. IME runs its own check-in timer independently of OMA-DM sync, so “just hit sync in Company Portal” is unreliable advice for anything Win32 related. Only a service restart forced a poll here.

Install context and GRS context are different axes. An app configured with System install behavior was recorded under the user-context GUID, not the device context.

What would disprove it

A subkey the field list above can’t reproduce. Everything so far has reproduced to the byte, and the first one that doesn’t means the input model is incomplete.

Has anyone tested whether a Graph PATCH against a Win32 app bumps MetadataVersion the same way the portal does? Let me know if you have other views or feedback on this 🙂