Installer hash mismatch on Chrome and friends
winget aborts with a hash mismatch on a package that installed fine last week.
A package installs fine for weeks. Then it starts failing with an installer hash mismatch. Chrome is the usual suspect — its winget entries have collected hash-mismatch reports for years — but any vendor shipping quickly enough will do it.
What is actually happening
A winget manifest pins the installer’s SHA-256. Before running anything, winget downloads the installer, hashes it, and compares. If the bytes at that URL are no longer the bytes the manifest describes, the hash does not match and winget refuses to proceed.
The most commonly reported cause is timing rather than anything sinister: many vendors publish to a stable download URL and update the file in place, so the installer changes as soon as they ship, while the manifest describing it is updated separately and lands later. For the window in between, the URL serves something the manifest does not describe. This is a long-standing and well-documented friction in the winget ecosystem, not a Chrome-specific quirk.
We also see the harder variant on our own catalog: the bytes behind a URL changing while the version string stays put. That one is worth calling out separately because it is the case our automation deliberately will not resolve on its own — see below.
The refusal is correct, and it is the single most valuable safety property winget has. The bytes at a URL are not the bytes you approved. Winget cannot distinguish a benign vendor rebuild from a compromised CDN, so it stops.
Do not override it
The obvious workaround is --ignore-security-hash. Resist it, and ideally make it impossible.
It is worth knowing how strong this check is in a managed context. We tested it directly, serving bytes whose hash deliberately did not match the manifest:
- normal install: winget downloads, hashes, and aborts — “Installer hash does not match; this cannot be overridden when running as admin.”
- with the override flag and the override disabled: refused
- with the override flag and the override enabled: still refused, with the same message
The reason is visible in winget’s own download flow: the elevation check is evaluated first, and unlike the other branches it has no escape — it falls straight through to terminating the install. So for any elevated caller, which is what a managed install is, a hash mismatch cannot be overridden at all. Not by the flag, not by turning the override on.
One precision worth carrying, because it is easy to state this wrongly: the trigger is elevation, not machine scope. An elevated user-scope install is blocked exactly the same way, and a non-elevated machine-scope attempt is not. Note also that the Group Policy does not itself permit overriding — it controls whether the winget admin setting can be enabled, and the flag then invokes it. Three layers, and the elevation check sits above all of them. Setting EnableHashOverride=0 remains worth doing for the non-elevated edge and for defence in depth.
Our own shim never composes --ignore-security-hash or --force at all; both are on a permanent denylist.
The fix that actually removes the problem
Everything above is about surviving the mismatch gracefully. The way to stop having it is to take the vendor’s CDN out of the install path.
When a package is admitted, the installer bytes that were verified and detonated are retained, content-addressed. The catalog then flips that coordinate to serve from your own feed, and the manifest handed to endpoints points at your server instead of the vendor — with the same pinned SHA-256, so the endpoint still verifies exactly what it verified before.
After that flip, a vendor republishing at the same version is a non-event. Your endpoints are installing bytes you hold, that were detonated and attested, and that cannot change underneath you.
Noticing when upstream changes
There is also a detector that periodically re-fetches the upstream installer URL for versions you are actively serving and re-hashes it against what was attested. Distinct URLs are fetched once per sweep, which matters for a vendor like Chrome where several versions share one large download.
When the bytes differ, the true version is resolved from the live bytes themselves rather than from any label, and the outcome routes three ways. Newer content is re-ingested and re-detonated. Older content is flagged as a would-be downgrade. Same version with new bytes — the Chrome case — is surfaced for manual handling, and that block is keyed to the digest, so it re-fires only if the bytes change again rather than nagging.
What the endpoint does meanwhile
If an endpoint does hit a hash mismatch, the outcome is recorded as skipped with an upstream-drift reason rather than as a failure. It does not count toward the install-failure rate and cannot trip automatic rollback, because the package is not defective — your copy of the truth is stale. The coordinate then cools down for six hours so a doomed install is not re-attempted every cycle.