# NousResearch/hermes-agent · Skills bundle hash crashes on bytes content

## Open now

- Nothing open on GitHub.

A bug where bundle_content_hash raises an AttributeError when encountering bytes values during skills check/update operations. Multiple fix attempts address handling bytes alongside string content in the hashing logic.

## TL;DR

- 17 member(s): 0 open issue(s), 4 closed issue(s), 0 open PR(s), 1 merged PR(s), 12 PR(s) closed without merging.
- A fix was merged to main in PR #19575. Verify it addresses your case.
- **Recommended:** read PR #19575 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-19575`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.14`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #2739: \[Bug\]: \`hermes skills update\` fails
- #13408: fix: bundle_content_hash crashes with bytes values (AttributeError)
- #19073: AttributeError in bundle_content_hash when checking skill updates
- #19090: Bug: hermes skills check/update crashes with AttributeError in bundle_content_hash

## Prior & competing PRs

- **#2740: fix(skills): handle bytes content in bundle_content_hash**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3072e5543`, `3072e5543ba01c23358772db5fe1a2e770ee108a`, `91ea3ae4b`, `91ea3ae4b2e5214390f0865329648f9cffbbc8bc`, `#9925`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#13531: fix(skills): handle bytes in bundle content hash**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#3751`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#19575: fix(skills): hash byte-backed bundle files (closes #13408)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `scripts/release.py`, `tests/tools/test_skills_hub.py`, `tools/skills_hub.py`
  - fix hunks:
  - `scripts/release.py`

    ```diff
    @@ -633,6 +633,8 @@
         "[redacted:email]": "CIRWEL",
         "[redacted:email]": "molvikar",
         "[redacted:email]": "nftpoetrist",
    +    "[redacted:email]": "colorcross",
    +    "[redacted:email]": "zhao0112",
         "[redacted:email]": "leozeli",
         "[redacted:email]": "LehaoLin",
         "[redacted:email]": "I3eg1nner",
    ```
  - `tests/tools/test_skills_hub.py`

    ```diff
    @@ -901,6 +901,69 @@ def test_bundle_content_hash_matches_installed_content_hash(self, tmp_path):
     
             assert bundle_content_hash(bundle) == content_hash(skill_dir)
     
    +    def test_bundle_content_hash_accepts_binary_files(self):
    +        bundle = SkillBundle(
    +            name="demo-binary-skill",
    +            files={
    +                "SKILL.md": "# Demo\n",
    +                "assets/logo.png": b"\x89PNG\r\n\x1a\nbinary",
    +            },
    +            source="github",
    +            identifier="owner/repo/demo-binary-skill",
    +            trust_level="community",
    +        )
    +
    +        digest = bundle_content_hash(bundle)
    +
    +        assert digest.startswith("sha256:")
    +
    +    def test_bundle_content_hash_bytes_matches_str_equivalent(self):
    +        """Bytes content must hash identically to its str-decoded form."""
    +        text_bundle = SkillBundle(
    +            name="demo-skill",
    +            files={
    +                "SKILL.md": "same content",
    +                "references/checklist.md": "- [ ] security\n",
    +            },
    +            source="github",
    +            identifier="owner/repo/demo-skill",
    +            trust_level="community",
    +        )
    +        bytes_bundle = SkillBundle(
    +            name="demo-skill",
    +            files={
    +                "SKILL.md": b"same content",
    +                "references/checklist.md": b"- [ ] security\n",
    +            },
    +            source="github",
    +            identifier="owner/repo/demo-skill",
    +            trust_level="community",
    +        )
    +
    +        assert bundle_content_hash(bytes_bundle) == bundle_content_hash(text_bundle)
    +
    +    def test_bundle_content_hash_mixed_matches_on_disk(self, tmp_path):
    +        """In-memory bundle hash must equal on-disk content_hash for mixed bytes+str."""
    +        from tools.skills_guard import content_hash
    +
    +        bundle = SkillBundle(
    +            name="demo-skill",
    +            files={
    +                "SKILL.md": b"# Demo Skill\n",
    +                "references/checklist.md": "- [ ] security\n",
    +            },
    +            source="github",
    +            identifier="owner/repo/demo-skill",
    +            trust_level="community",
    +        )
    +        skill_dir = tmp_path / "demo-skill"
    +        skill_dir.mkdir()
    +        (skill_dir / "SKILL.md").write_bytes(b"# Demo Skill\n")
    +        (skill_dir / "references").mkdir()
    +        (skill_dir / "references" / "checklist.md").write_text("- [ ] security\n")
    +
    +        assert bundle_content_hash(bundle) == content_hash(skill_dir)
    +
         def test_reports_update_when_remote_hash_differs(self):
             lock = MagicMock()
             lock.list_installed.return_value = [{
    ```
  - `tools/skills_hub.py`

    ```diff
    @@ -2801,7 +2801,11 @@ def bundle_content_hash(bundle: SkillBundle) -> str:
         """Compute a deterministic hash for an in-memory skill bundle."""
         h = hashlib.sha256()
         for rel_path in sorted(bundle.files):
    -        h.update(bundle.files[rel_path].encode("utf-8"))
    +        content = bundle.files[rel_path]
    +        if isinstance(content, bytes):
    +            h.update(content)
    +        else:
    +            h.update(content.encode("utf-8"))
         return f"sha256:{h.hexdigest()[:16]}"
     
     
    ```
- **#3751: Handle byte-backed skill bundle hashes**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3072e5543ba01c23358772db5fe1a2e770ee108a`
- **#12518: fix(skills_hub): handle bytes content in bundle_content_hash**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#2740`
- **#13424: fix(skills_hub): handle bytes content in bundle_content_hash**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#2740`
- **#14108: fix(skills): hash byte-backed bundle files**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#13531`
- **#19079: fix: handle bytes values in bundle_content_hash()**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#19575`, `#19328`
- **#19081: fix(skills): handle bytes values in bundle_content_hash**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#19575`, `#19328`
- **#19116: fix(skills): hash bytes bundle contents**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#9925`, `#9925`, `#14108`, `#13531`, `#13408`
- **#19120: fix(skills): handle bytes values in bundle_content_hash — fixes AttributeError on skills check/update (#19090)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#19575`, `#19328`
- **#19145: fix(skills): handle bytes in bundle update hash**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#19575`, `#19328`
- **#19328: fix(skills): hash byte-backed bundle files (closes #13408)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#19575`

## Fix status

- Merged to main in PR #19575 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-04`.
- Subsystem: `hermes_cli`
