{"repo":"NousResearch/hermes-agent","public_id":"1006","display_label":"Skills Guard dangerous verdicts, --force override, and policy table handling","summary":"The members appear to concern Skills Guard behavior around dangerous scan verdicts, including an issue report that official/builtin skills are blocked and that --force does not override a DANGEROUS verdict. The related pull requests propose honoring the policy table and allowing --force or refining trust logic; two titles are identical.","subsystem":"optional-skills","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":1330,"merged_at":"2026-03-14T18:31:33+00:00"},"resolution":{"state":"resolved","by":"pr-1330"},"freshness":{"state":"stale","last_verified_at":"2026-03-14T18:31:33+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":1006,"pr_number":1330,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-14T18:31:33+00:00"}],"members":[{"kind":"issue","number":1006,"title":"Skills Guard: Official/builtin skills blocked by scan + --force flag doesn't override DANGEROUS verdict","url":null,"state":"closed"},{"kind":"pr","number":1007,"title":"fix(skills): honor policy table for dangerous verdicts","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1330","number":1330}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":1330,"title":"fix(skills): honor policy table for dangerous verdicts","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","hermes_cli/skills_hub.py","tests/hermes_cli/test_skills_hub.py","tests/hermes_cli/test_skills_install_flags.py","tests/tools/test_force_dangerous_override.py","tests/tools/test_skills_guard.py","tools/skills_guard.py"],"fix_hunks":[{"file":"hermes_cli/main.py","hunks":["@@ -2701,7 +2701,7 @@ def cmd_pairing(args):\n     skills_install = skills_subparsers.add_parser(\"install\", help=\"Install a skill\")\n     skills_install.add_argument(\"identifier\", help=\"Skill identifier (e.g. openai/skills/skill-creator)\")\n     skills_install.add_argument(\"--category\", default=\"\", help=\"Category folder to install into\")\n-    skills_install.add_argument(\"--force\", action=\"store_true\", help=\"Install despite caution verdict\")\n+    skills_install.add_argument(\"--force\", \"--yes\", \"-y\", dest=\"force\", action=\"store_true\", help=\"Install despite blocked scan verdict\")\n \n     skills_inspect = skills_subparsers.add_parser(\"inspect\", help=\"Preview a skill without installing\")\n     skills_inspect.add_argument(\"identifier\", help=\"Skill identifier\")"]},{"file":"hermes_cli/skills_hub.py","hunks":["@@ -1050,11 +1050,11 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:\n \n     elif action == \"install\":\n         if not args:\n-            c.print(\"[bold red]Usage:[/] /skills install <identifier> [--category <cat>] [--force]\\n\")\n+            c.print(\"[bold red]Usage:[/] /skills install <identifier> [--category <cat>] [--force|--yes]\\n\")\n             return\n         identifier = args[0]\n         category = \"\"\n-        force = \"--force\" in args\n+        force = any(flag in args for flag in (\"--force\", \"--yes\", \"-y\"))\n         for i, a in enumerate(args):\n             if a == \"--category\" and i + 1 < len(args):\n                 category = args[i + 1]"]},{"file":"tests/hermes_cli/test_skills_hub.py","hunks":["@@ -3,7 +3,7 @@\n import pytest\n from rich.console import Console\n \n-from hermes_cli.skills_hub import do_check, do_list, do_update\n+from hermes_cli.skills_hub import do_check, do_list, do_update, handle_skills_slash\n \n \n class _DummyLockFile:"]},{"file":"tests/hermes_cli/test_skills_install_flags.py","hunks":["@@ -0,0 +1,26 @@\n+import sys\n+from types import SimpleNamespace\n+\n+\n+def test_cli_skills_install_accepts_yes_alias(monkeypatch):\n+    from hermes_cli.main import main\n+\n+    captured = {}\n+\n+    def fake_skills_command(args):\n+        captured[\"identifier\"] = args.identifier\n+        captured[\"force\"] = args.force\n+\n+    monkeypatch.setattr(\"hermes_cli.skills_hub.skills_command\", fake_skills_command)\n+    monkeypatch.setattr(\n+        sys,\n+        \"argv\",\n+        [\"hermes\", \"skills\", \"install\", \"official/email/agentmail\", \"--yes\"],\n+    )\n+\n+    main()\n+\n+    assert captured == {\n+        \"identifier\": \"official/email/agentmail\",\n+        \"force\": True,\n+    }"]},{"file":"tests/tools/test_force_dangerous_override.py","hunks":["@@ -1,11 +1,8 @@\n-\"\"\"Tests for the --force flag dangerous verdict bypass fix in skills_guard.py.\n+\"\"\"Regression tests for skills guard policy precedence.\n \n-Regression test: the old code had `if result.verdict == \"dangerous\" and not force:`\n-which meant force=True would skip the early return, fall through the policy\n-lookup, and hit `if force: return True` - allowing installation of skills\n-flagged as dangerous (reverse shells, data exfiltration, etc).\n-\n-The docstring explicitly states: \"never overrides dangerous\".\n+Official/builtin skills should follow the INSTALL_POLICY table even when their\n+scan verdict is dangerous, and --force should override blocked verdicts for\n+non-builtin sources.\n \"\"\"\n \n ","@@ -44,10 +41,6 @@ def _new_should_allow(verdict, trust_level, force):\n     }\n     VERDICT_INDEX = {\"safe\": 0, \"caution\": 1, \"dangerous\": 2}\n \n-    # Fixed: no `and not force` - dangerous is always blocked\n-    if verdict == \"dangerous\":\n-        return False\n-\n     policy = INSTALL_POLICY.get(trust_level, INSTALL_POLICY[\"community\"])\n     vi = VERDICT_INDEX.get(verdict, 2)\n     decision = policy[vi]","@@ -61,35 +54,28 @@ def _new_should_allow(verdict, trust_level, force):\n     return False\n \n \n-class TestForceNeverOverridesDangerous:\n-    \"\"\"The core bug: --force bypassed the dangerous verdict block.\"\"\"\n+class TestPolicyPrecedenceForDangerousVerdicts:\n+    def test_builtin_dangerous_is_allowed_by_policy(self):\n+        assert _new_should_allow(\"dangerous\", \"builtin\", force=False) is True\n \n-    def test_old_code_allows_dangerous_with_force(self):\n-        \"\"\"Old code: force=True lets dangerous skills through.\"\"\"\n-        assert _old_should_allow(\"dangerous\", \"community\", force=True) is True\n+    def test_trusted_dangerous_is_blocked_without_force(self):\n+        assert _new_should_allow(\"dangerous\", \"trusted\", force=False) is False\n \n-    def test_new_code_blocks_dangerous_with_force(self):\n-        \"\"\"Fixed code: force=True still blocks dangerous skills.\"\"\"\n-        assert _new_should_allow(\"dangerous\", \"community\", force=True) is False\n+    def test_force_overrides_dangerous_for_community(self):\n+        assert _new_should_allow(\"dangerous\", \"community\", force=True) is True\n \n-    def test_new_code_blocks_dangerous_trusted_with_force(self):\n-        \"\"\"Fixed code: even trusted + force cannot install dangerous.\"\"\"\n-        assert _new_should_allow(\"dangerous\", \"trusted\", force=True) is False\n+    def test_force_overrides_dangerous_for_trusted(self):\n+        assert _new_should_allow(\"dangerous\", \"trusted\", force=True) is True\n \n     def test_force_still_overrides_caution(self):\n-        \"\"\"force=True should still work for caution verdicts.\"\"\"\n         assert _new_should_allow(\"caution\", \"community\", force=True) is True\n \n     def test_caution_community_blocked_without_force(self):\n-        \"\"\"Caution + community is blocked without force (unchanged).\"\"\"\n         assert _new_should_allow(\"caution\", \"community\", force=False) is False\n \n     def test_safe_always_allowed(self):\n-        \"\"\"Safe verdict is always allowed regardless of force.\"\"\"\n         assert _new_should_allow(\"safe\", \"community\", force=False) is True\n         assert _new_should_allow(\"safe\", \"community\", force=True) is True\n \n-    def test_dangerous_blocked_without_force(self):\n-        \"\"\"Dangerous is blocked without force (both old and new agree).\"\"\"\n-        assert _old_should_allow(\"dangerous\", \"community\", force=False) is False\n-        assert _new_should_allow(\"dangerous\", \"community\", force=False) is False\n+    def test_old_code_happened_to_allow_forced_dangerous_community(self):\n+        assert _old_should_allow(\"dangerous\", \"community\", force=True) is True"]},{"file":"tests/tools/test_skills_guard.py","hunks":["@@ -46,9 +46,9 @@ def _can_symlink():\n \n \n class TestResolveTrustLevel:\n-    def test_builtin_not_exposed(self):\n-        # builtin is only used internally, not resolved from source string\n-        assert _resolve_trust_level(\"openai/skills\") == \"trusted\"\n+    def test_official_sources_resolve_to_builtin(self):\n+        assert _resolve_trust_level(\"official\") == \"builtin\"\n+        assert _resolve_trust_level(\"official/email/agentmail\") == \"builtin\"\n \n     def test_trusted_repos(self):\n         assert _resolve_trust_level(\"openai/skills\") == \"trusted\"","@@ -116,11 +116,17 @@ def test_caution_trusted_allowed(self):\n         allowed, _ = should_allow_install(self._result(\"trusted\", \"caution\", f))\n         assert allowed is True\n \n-    def test_dangerous_blocked_even_trusted(self):\n+    def test_trusted_dangerous_blocked_without_force(self):\n         f = [Finding(\"x\", \"critical\", \"c\", \"f\", 1, \"m\", \"d\")]\n         allowed, _ = should_allow_install(self._result(\"trusted\", \"dangerous\", f))\n         assert allowed is False\n \n+    def test_builtin_dangerous_allowed_without_force(self):\n+        f = [Finding(\"x\", \"critical\", \"c\", \"f\", 1, \"m\", \"d\")]\n+        allowed, reason = should_allow_install(self._result(\"builtin\", \"dangerous\", f))\n+        assert allowed is True\n+        assert \"builtin source\" in reason\n+\n     def test_force_overrides_caution(self):\n         f = [Finding(\"x\", \"high\", \"c\", \"f\", 1, \"m\", \"d\")]\n         allowed, reason = should_allow_install(self._result(\"community\", \"caution\", f), force=True)","@@ -132,22 +138,21 @@ def test_dangerous_blocked_without_force(self):\n         allowed, _ = should_allow_install(self._result(\"community\", \"dangerous\", f), force=False)\n         assert allowed is False\n \n-    def test_force_never_overrides_dangerous(self):\n-        \"\"\"--force must not bypass dangerous verdict (regression test).\"\"\"\n+    def test_force_overrides_dangerous_for_community(self):\n         f = [Finding(\"x\", \"critical\", \"c\", \"f\", 1, \"m\", \"d\")]\n         allowed, reason = should_allow_install(\n             self._result(\"community\", \"dangerous\", f), force=True\n         )\n-        assert allowed is False\n-        assert \"DANGEROUS\" in reason\n+        assert allowed is True\n+        assert \"Force-installed\" in reason\n \n-    def test_force_never_overrides_dangerous_trusted(self):\n-        \"\"\"--force must not bypass dangerous even for trusted sources.\"\"\"\n+    def test_force_overrides_dangerous_for_trusted(self):\n         f = [Finding(\"x\", \"critical\", \"c\", \"f\", 1, \"m\", \"d\")]\n-        allowed, _ = should_allow_install(\n+        allowed, reason = should_allow_install(\n             self._result(\"trusted\", \"dangerous\", f), force=True\n         )\n-        assert allowed is False\n+        assert allowed is True\n+        assert \"Force-installed\" in reason\n \n \n # ---------------------------------------------------------------------------"]},{"file":"tools/skills_guard.py","hunks":["@@ -645,14 +645,11 @@ def should_allow_install(result: ScanResult, force: bool = False) -> Tuple[bool,\n \n     Args:\n         result: Scan result from scan_skill()\n-        force: If True, override blocks for caution verdicts (never overrides dangerous)\n+        force: If True, override blocked policy decisions for this scan result\n \n     Returns:\n         (allowed, reason) tuple\n     \"\"\"\n-    if result.verdict == \"dangerous\":\n-        return False, f\"Scan verdict is DANGEROUS ({len(result.findings)} findings). Blocked.\"\n-\n     policy = INSTALL_POLICY.get(result.trust_level, INSTALL_POLICY[\"community\"])\n     vi = VERDICT_INDEX.get(result.verdict, 2)\n     decision = policy[vi]","@@ -661,7 +658,10 @@ def should_allow_install(result: ScanResult, force: bool = False) -> Tuple[bool,\n         return True, f\"Allowed ({result.trust_level} source, {result.verdict} verdict)\"\n \n     if force:\n-        return True, f\"Force-installed despite {result.verdict} verdict ({len(result.findings)} findings)\"\n+        return True, (\n+            f\"Force-installed despite blocked {result.verdict} verdict \"\n+            f\"({len(result.findings)} findings)\"\n+        )\n \n     return False, (\n         f\"Blocked ({result.trust_level} source + {result.verdict} verdict, \"\n"]}]},{"kind":"pr","number":1507,"title":"fix: allow --force override and refine trust logic in Skills Guard (#1006)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1330","number":1330}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}