# NousResearch/hermes-agent · Security fixes for automated skill review role handling

## Open now

- Nothing open on GitHub.

These members relate to role assignment issues in automated/background skill review processes, where incorrect role usage could allow unintended skill modifications. Fixes address using the appropriate system role and respecting pinned skills during background review forks.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #53226. Verify it addresses your case.
- **Recommended:** read PR #53226 (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: `resolved` · deciding PR(s): `pr-53226`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: 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

- #25839: System impersonates user role, tricking parallel agent instances into modifying skills without consent

## Prior & competing PRs

- **#53226: fix(skills): background review fork respects pinned skills**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `tests/tools/test_skill_manager_tool.py`, `tools/skill_manager_tool.py`
  - fix hunks:
  - `tests/tools/test_skill_manager_tool.py`

    ```diff
    @@ -912,6 +912,67 @@ def test_background_review_refuses_to_patch_external_skill(self, tmp_path):
             assert "OLD_MARKER" in (skill_dir / "SKILL.md").read_text()
             assert "NEW_MARKER" not in (skill_dir / "SKILL.md").read_text()
     
    +    def test_background_review_refuses_to_patch_pinned_skill(self, tmp_path):
    +        """#25839: the autonomous review fork respects pin like the curator
    +        does — a pinned skill is off-limits to background maintenance, even
    +        for patch/edit (which a foreground user-directed call is allowed to
    +        perform). Without a user in the loop there is no one to consent."""
    +        from tools.skill_provenance import (
    +            BACKGROUND_REVIEW,
    +            reset_current_write_origin,
    +            set_current_write_origin,
    +        )
    +
    +        def _fake_get_record(skill_name):
    +            return {"pinned": True} if skill_name == "my-skill" else {"pinned": False}
    +
    +        with _skill_dir(tmp_path):
    +            _create_skill("my-skill", VALID_SKILL_CONTENT)
    +            token = [redacted:secret-assignment]
    +            try:
    +                with patch("tools.skill_usage.get_record", side_effect=_fake_get_record):
    +                    raw = skill_manage(
    +                        action="patch",
    +                        name="my-skill",
    +                        old_string="Do the thing.",
    +                        new_string="Do the new thing.",
    +                    )
    +            finally:
    +                reset_current_write_origin(token)
    +
    +        result = json.loads(raw)
    +        assert result["success"] is False
    +        assert "pinned" in result["error"].lower()
    +
    +    def test_background_review_unpinned_skill_not_blocked_by_pin_guard(self, tmp_path):
    +        """The pin guard must not over-block: an unpinned agent-owned skill is
    +        still writable by the review fork."""
    +        from tools.skill_provenance import (
    +            BACKGROUND_REVIEW,
    +            reset_current_write_origin,
    +            set_current_write_origin,
    +        )
    +
    +        with _skill_dir(tmp_path):
    +            _create_skill("my-skill", VALID_SKILL_CONTENT)
    +            token = [redacted:secret-assignment]
    +            try:
    +                with patch(
    +                    "tools.skill_usage.get_record",
    +                    side_effect=lambda n: {"pinned": False},
    +                ):
    +                    raw = skill_manage(
    +                        action="patch",
    +                        name="my-skill",
    +                        old_string="Do the thing.",
    +                        new_string="Do the new thing.",
    +                    )
    +            finally:
    +                reset_current_write_origin(token)
    +
    +        result = json.loads(raw)
    +        assert result["success"] is True
    +
     
     
     # ---------------------------------------------------------------------------
    ```
  - `tools/skill_manager_tool.py`

    ```diff
    @@ -254,6 +254,27 @@ def _background_review_write_guard(
         except Exception:
             return None
     
    +    # Pin must be respected by autonomous maintenance. The curator already
    +    # skips pinned skills from every auto-transition; the background review
    +    # fork is the same kind of autonomous, no-user-present actor, so it must
    +    # not write to a pinned skill either (issue #25839). This is stricter than
    +    # the foreground ``_pinned_guard`` (which only blocks deletion) precisely
    +    # because there is no user in the loop to consent to an edit here.
    +    try:
    +        from tools import skill_usage
    +        if skill_usage.get_record(name).get("pinned"):
    +            return {
    +                "success": False,
    +                "error": (
    +                    f"Refusing background curator {action} for pinned skill "
    +                    f"'{name}': pinned skills are off-limits to autonomous "
    +                    "maintenance. Ask the user to run "
    +                    f"`hermes curator unpin {name}` if they want it changed."
    +                ),
    +            }
    +    except Exception:
    +        logger.debug("pinned skill guard lookup failed for %s", name, exc_info=True)
    +
         try:
             from agent.skill_utils import is_external_skill_path
             if is_external_skill_path(skill_dir):
    ```
- **#25934: fix(security): use system role for automated review prompts (#25839)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#25839`, `#53226`, `#17276`

## Fix status

- Merged to main in PR #53226 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-26`.
- Subsystem: `agent`
