bug(updates): dirty install reports "up to date" — should detect local changes and offer to apply latest
#4,085 opened on Jun 13, 2026
Repository metrics
- Stars
- (16,904 stars)
- PR merge metrics
- (Avg merge 14h 31m) (314 merged PRs in 30d)
Description
Summary
When the hermes-webui install has a dirty working tree (uncommitted/modified tracked files), the update checker reports "Up to date" and offers no way to move to the latest version. A dirty install should instead be detected and offered the latest version (apply / reset-to-clean), since a dirty tree often means the install is in a modified or partially-updated state the user didn't intend.
Reporter
@b3nw, Hermes Discord #report-bugs, 2026-06-13:
if the hermes-webui install is dirty, it treats it as up to date, it should instead offer to apply the latest version
(Screenshot attached showing the settings update panel reporting up-to-date.)
Root cause
The update check path inspects only committed git refs and ignores the working-tree dirty state entirely:
_check_repo()(api/updates.py:~646) →_check_repo_release()(:~524) comparesHEAD's nearest release tag (git describe --tags --abbrev=0) against the latest fetched tag, and_check_repo_branch()(:~575) countsgit rev-list HEAD..<upstream>. Both operate on committed state.- A dirty working tree (
git diffagainst HEAD) is only consulted for the version display string via_dirty_suffix()(:~189, appends-dirty/-dirty-<hash>) — it is never fed into the behind/ahead comparison.
So when HEAD is at (or past) the latest release tag but the tree has local modifications, behind == 0 → the checker returns None → the UI shows "Up to date," silently ignoring that the install has been modified. The user is given no signal and no remediation affordance.
Why the fix is cheap
The apply side already has the primitive to fix a dirty install: apply_force_update(target) (api/updates.py:~1165) discards local modifications (git checkout .) and resets to origin/<branch> — exactly the "reset to a clean latest" action. The plain apply_update() (:~1227) does stash → pull --ff-only → pop and requires a clean tree. So the missing piece is purely on the check/surface side: detect dirty and expose the existing force/apply path.
Suggested fix shape
- In
_check_repo()(or the payload it returns), surface adirtyboolean (reuse the existing dirty detection in_dirty_suffix()/ agit diff-index --quiet HEADprobe — already used for the version string). - When
dirtyis true, don't report a bare "Up to date." Instead surface a distinct state in the Settings update panel — e.g. "Local changes detected" with an action to apply the latest version (wired toapply_force_update, which already discards local mods and resets to clean latest), ideally behind a confirm since it's destructive of local edits. - Keep the clean-tree behavior unchanged.
This is WebUI-only (api/updates.py + the Settings update-panel frontend in static/). Small surface; the destructive reset already exists and is tested.
Scope / considerations
- The "apply" must clearly warn it discards local modifications (it calls
git checkout .+ reset). Some users intentionally run patched installs — a confirm + clear copy avoids surprising them. A non-destructive option (stash + ff) isapply_update, but that can't fast-forward if HEAD already contains the tag; force-reset is the reliable "get me to clean latest" path. - Distinct from #3875 (message-display regression after update) — unrelated.
Reported by
@b3nw via Discord #report-bugs, 2026-06-13.