{"repo":"NousResearch/hermes-agent","public_id":"25839","display_label":"Security fixes for automated skill review role handling","summary":"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.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":53226,"merged_at":"2026-06-26T19:49:34+00:00"},"resolution":{"state":"resolved","by":"pr-53226"},"freshness":{"state":"verified","last_verified_at":"2026-06-26T19:49:34+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":25839,"pr_number":53226,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-26T19:49:34+00:00"}],"members":[{"kind":"issue","number":25839,"title":"System impersonates user role, tricking parallel agent instances into modifying skills without consent","url":null,"state":"closed"},{"kind":"pr","number":25934,"title":"fix(security): use system role for automated review prompts (#25839)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#25839","number":25839},{"kind":"pr","value":"#53226","number":53226},{"kind":"pr","value":"#17276","number":17276}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":53226,"title":"fix(skills): background review fork respects pinned skills","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_skill_manager_tool.py","tools/skill_manager_tool.py"],"fix_hunks":[{"file":"tests/tools/test_skill_manager_tool.py","hunks":["@@ -912,6 +912,67 @@ def test_background_review_refuses_to_patch_external_skill(self, tmp_path):\n         assert \"OLD_MARKER\" in (skill_dir / \"SKILL.md\").read_text()\n         assert \"NEW_MARKER\" not in (skill_dir / \"SKILL.md\").read_text()\n \n+    def test_background_review_refuses_to_patch_pinned_skill(self, tmp_path):\n+        \"\"\"#25839: the autonomous review fork respects pin like the curator\n+        does — a pinned skill is off-limits to background maintenance, even\n+        for patch/edit (which a foreground user-directed call is allowed to\n+        perform). Without a user in the loop there is no one to consent.\"\"\"\n+        from tools.skill_provenance import (\n+            BACKGROUND_REVIEW,\n+            reset_current_write_origin,\n+            set_current_write_origin,\n+        )\n+\n+        def _fake_get_record(skill_name):\n+            return {\"pinned\": True} if skill_name == \"my-skill\" else {\"pinned\": False}\n+\n+        with _skill_dir(tmp_path):\n+            _create_skill(\"my-skill\", VALID_SKILL_CONTENT)\n+            token = [redacted:secret-assignment]\n+            try:\n+                with patch(\"tools.skill_usage.get_record\", side_effect=_fake_get_record):\n+                    raw = skill_manage(\n+                        action=\"patch\",\n+                        name=\"my-skill\",\n+                        old_string=\"Do the thing.\",\n+                        new_string=\"Do the new thing.\",\n+                    )\n+            finally:\n+                reset_current_write_origin(token)\n+\n+        result = json.loads(raw)\n+        assert result[\"success\"] is False\n+        assert \"pinned\" in result[\"error\"].lower()\n+\n+    def test_background_review_unpinned_skill_not_blocked_by_pin_guard(self, tmp_path):\n+        \"\"\"The pin guard must not over-block: an unpinned agent-owned skill is\n+        still writable by the review fork.\"\"\"\n+        from tools.skill_provenance import (\n+            BACKGROUND_REVIEW,\n+            reset_current_write_origin,\n+            set_current_write_origin,\n+        )\n+\n+        with _skill_dir(tmp_path):\n+            _create_skill(\"my-skill\", VALID_SKILL_CONTENT)\n+            token = [redacted:secret-assignment]\n+            try:\n+                with patch(\n+                    \"tools.skill_usage.get_record\",\n+                    side_effect=lambda n: {\"pinned\": False},\n+                ):\n+                    raw = skill_manage(\n+                        action=\"patch\",\n+                        name=\"my-skill\",\n+                        old_string=\"Do the thing.\",\n+                        new_string=\"Do the new thing.\",\n+                    )\n+            finally:\n+                reset_current_write_origin(token)\n+\n+        result = json.loads(raw)\n+        assert result[\"success\"] is True\n+\n \n \n # ---------------------------------------------------------------------------"]},{"file":"tools/skill_manager_tool.py","hunks":["@@ -254,6 +254,27 @@ def _background_review_write_guard(\n     except Exception:\n         return None\n \n+    # Pin must be respected by autonomous maintenance. The curator already\n+    # skips pinned skills from every auto-transition; the background review\n+    # fork is the same kind of autonomous, no-user-present actor, so it must\n+    # not write to a pinned skill either (issue #25839). This is stricter than\n+    # the foreground ``_pinned_guard`` (which only blocks deletion) precisely\n+    # because there is no user in the loop to consent to an edit here.\n+    try:\n+        from tools import skill_usage\n+        if skill_usage.get_record(name).get(\"pinned\"):\n+            return {\n+                \"success\": False,\n+                \"error\": (\n+                    f\"Refusing background curator {action} for pinned skill \"\n+                    f\"'{name}': pinned skills are off-limits to autonomous \"\n+                    \"maintenance. Ask the user to run \"\n+                    f\"`hermes curator unpin {name}` if they want it changed.\"\n+                ),\n+            }\n+    except Exception:\n+        logger.debug(\"pinned skill guard lookup failed for %s\", name, exc_info=True)\n+\n     try:\n         from agent.skill_utils import is_external_skill_path\n         if is_external_skill_path(skill_dir):\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}