Bug: hermes skills check reports permanent false-positive update_available on Windows
Summary
After a successful hermes skills update (e.g. "Updated 32 skill(s)"), hermes skills check continues listing ALL those skills as update_available — forever. The hashes never match on Windows.
Root cause
Two content-hash functions — documented in-code as "MUST stay symmetric" — diverge on Windows due to path-separator and sort-order differences:
tools/skills_hub.py::bundle_content_hash (in-memory bundle, L3573):
- Iterates
sorted(bundle.files) — keys retain backslash separators on Windows (e.g. references\methods\x.md)
sorted() on strings is case-sensitive (SKILL.md sorts before references/)
tools/skills_guard.py::content_hash (on-disk, L766):
- Uses
f.relative_to(skill_path).as_posix() — keys always have forward slash (e.g. references/methods/x.md)
sorted(skill_path.rglob("*")) sorts Path objects, which is case-insensitive on Windows (references/ sorts before SKILL.md)
The relative path is mixed into the SHA-256, so for any skill with files in subdirectories, the two digests can never match on Windows. On POSIX both functions agree, so the bug is invisible on Linux/macOS.
Proof
Recomputed the correct symmetric hash for all 32 flagged skills → 32/32 byte-identical to the registry, 0 genuinely outdated. After applying the fix and re-recording lock hashes: hermes skills check reports "0 update(s) available across 55 skills". Tamper test confirms genuine content changes are still detected.
Fix
In BOTH bundle_content_hash and content_hash, normalize the rel-path key to / and sort by that normalized string key:
# bundle_content_hash (skills_hub.py):
for rel_path in sorted(bundle.files, key=lambda p: p.replace("\\", "/")):
normalized = rel_path.replace("\\", "/")
h.update(normalized.encode("utf-8"))
h.update(b"\x00")
...
# content_hash (skills_guard.py):
for f in sorted(skill_path.rglob("*"), key=lambda p: p.relative_to(skill_path).as_posix().replace("\\", "/")):
if f.is_file():
rel = f.relative_to(skill_path).as_posix()
h.update(rel.encode("utf-8"))
...
Add a sys.platform-guarded test with a mixed-separator + mixed-case fixture asserting bundle_content_hash(bundle) == content_hash(disk).
Environment
- Windows 10/11 (any install with hub skills containing subdirectory files)
- Verified against current
main
Attribution
Diego Kolling + Hermes Agent (Nous subscription)
Bug:
hermes skills checkreports permanent false-positiveupdate_availableon WindowsSummary
After a successful
hermes skills update(e.g. "Updated 32 skill(s)"),hermes skills checkcontinues listing ALL those skills asupdate_available— forever. The hashes never match on Windows.Root cause
Two content-hash functions — documented in-code as "MUST stay symmetric" — diverge on Windows due to path-separator and sort-order differences:
tools/skills_hub.py::bundle_content_hash(in-memory bundle, L3573):sorted(bundle.files)— keys retain backslash separators on Windows (e.g.references\methods\x.md)sorted()on strings is case-sensitive (SKILL.mdsorts beforereferences/)tools/skills_guard.py::content_hash(on-disk, L766):f.relative_to(skill_path).as_posix()— keys always have forward slash (e.g.references/methods/x.md)sorted(skill_path.rglob("*"))sortsPathobjects, which is case-insensitive on Windows (references/sorts beforeSKILL.md)The relative path is mixed into the SHA-256, so for any skill with files in subdirectories, the two digests can never match on Windows. On POSIX both functions agree, so the bug is invisible on Linux/macOS.
Proof
Recomputed the correct symmetric hash for all 32 flagged skills → 32/32 byte-identical to the registry, 0 genuinely outdated. After applying the fix and re-recording lock hashes:
hermes skills checkreports "0 update(s) available across 55 skills". Tamper test confirms genuine content changes are still detected.Fix
In BOTH
bundle_content_hashandcontent_hash, normalize the rel-path key to/and sort by that normalized string key:Add a
sys.platform-guarded test with a mixed-separator + mixed-case fixture assertingbundle_content_hash(bundle) == content_hash(disk).Environment
mainAttribution
Diego Kolling + Hermes Agent (Nous subscription)