{"repo":"NousResearch/hermes-agent","public_id":"3474","display_label":"Skills install command soft-locks TUI via blocking prompt","summary":"A bug where the /skills install (and uninstall) commands soft-lock the TUI because of a blocking input() confirmation call. The fix skips the confirmation step in slash command contexts.","subsystem":"hermes_cli","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":3586,"merged_at":"2026-03-28T21:32:23+00:00"},"resolution":{"state":"resolved","by":"pr-3586"},"freshness":{"state":"stale","last_verified_at":"2026-03-28T21:32:23+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":3474,"pr_number":3586,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-28T21:32:23+00:00"}],"members":[{"kind":"issue","number":3474,"title":"[Bug]: Soft-locking when installing obsidian-cli skill","url":null,"state":"closed"},{"kind":"pr","number":3496,"title":"fix(skills): /skills install soft-locks in TUI due to blocking input() call  ","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#3586","number":3586}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":3586,"title":"fix(skills): skip confirmation in /skills install and uninstall slash commands","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/skills_hub.py","tests/hermes_cli/test_skills_skip_confirm.py"],"fix_hunks":[{"file":"hermes_cli/skills_hub.py","hunks":["@@ -304,7 +304,8 @@ def do_browse(page: int = 1, page_size: int = 20, source: str = \"all\",\n \n \n def do_install(identifier: str, category: str = \"\", force: bool = False,\n-               console: Optional[Console] = None, skip_confirm: bool = False) -> None:\n+               console: Optional[Console] = None, skip_confirm: bool = False,\n+               invalidate_cache: bool = True) -> None:\n     \"\"\"Fetch, quarantine, scan, confirm, and install a skill.\"\"\"\n     from tools.skills_hub import (\n         GitHubAuth, create_source_router, ensure_hub_dirs,","@@ -417,12 +418,16 @@ def do_install(identifier: str, category: str = \"\", force: bool = False,\n     c.print(f\"[bold green]Installed:[/] {install_dir.relative_to(SKILLS_DIR)}\")\n     c.print(f\"[dim]Files: {', '.join(bundle.files.keys())}[/]\\n\")\n \n-    # Invalidate the skills prompt cache so the new skill appears immediately\n-    try:\n-        from agent.prompt_builder import clear_skills_system_prompt_cache\n-        clear_skills_system_prompt_cache(clear_snapshot=True)\n-    except Exception:\n-        pass\n+    if invalidate_cache:\n+        # Invalidate the skills prompt cache so the new skill appears immediately\n+        try:\n+            from agent.prompt_builder import clear_skills_system_prompt_cache\n+            clear_skills_system_prompt_cache(clear_snapshot=True)\n+        except Exception:\n+            pass\n+    else:\n+        c.print(\"[dim]Skill will be available in your next session.[/]\")\n+        c.print(\"[dim]Use /reset to start a new session now, or --now to activate immediately (invalidates prompt cache).[/]\\n\")\n \n \n def do_inspect(identifier: str, console: Optional[Console] = None) -> None:","@@ -610,7 +615,8 @@ def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> N\n \n \n def do_uninstall(name: str, console: Optional[Console] = None,\n-                 skip_confirm: bool = False) -> None:\n+                 skip_confirm: bool = False,\n+                 invalidate_cache: bool = True) -> None:\n     \"\"\"Remove a hub-installed skill with confirmation.\"\"\"\n     from tools.skills_hub import uninstall_skill\n ","@@ -630,11 +636,15 @@ def do_uninstall(name: str, console: Optional[Console] = None,\n     success, msg = uninstall_skill(name)\n     if success:\n         c.print(f\"[bold green]{msg}[/]\\n\")\n-        try:\n-            from agent.prompt_builder import clear_skills_system_prompt_cache\n-            clear_skills_system_prompt_cache(clear_snapshot=True)\n-        except Exception:\n-            pass\n+        if invalidate_cache:\n+            try:\n+                from agent.prompt_builder import clear_skills_system_prompt_cache\n+                clear_skills_system_prompt_cache(clear_snapshot=True)\n+            except Exception:\n+                pass\n+        else:\n+            c.print(\"[dim]Change will take effect in your next session.[/]\")\n+            c.print(\"[dim]Use /reset to start a new session now, or --now to apply immediately (invalidates prompt cache).[/]\\n\")\n     else:\n         c.print(f\"[bold red]Error:[/] {msg}\\n\")\n ","@@ -1071,19 +1081,23 @@ 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|--yes]\\n\")\n+            c.print(\"[bold red]Usage:[/] /skills install <identifier> [--category <cat>] [--force] [--now]\\n\")\n             return\n         identifier = args[0]\n         category = \"\"\n-        # --yes / -y bypasses confirmation prompt (needed in TUI mode)\n-        # --force handles reinstall override\n-        skip_confirm = any(flag in args for flag in (\"--yes\", \"-y\"))\n+        # Slash commands run inside prompt_toolkit where input() hangs.\n+        # Always skip confirmation — the user typing the command is implicit consent.\n+        skip_confirm = True\n         force = \"--force\" in args\n+        # --now invalidates prompt cache immediately (costs more money).\n+        # Default: defer to next session to preserve cache.\n+        invalidate_cache = \"--now\" in args\n         for i, a in enumerate(args):\n             if a == \"--category\" and i + 1 < len(args):\n                 category = args[i + 1]\n         do_install(identifier, category=category, force=force,\n-                   skip_confirm=skip_confirm, console=c)\n+                   skip_confirm=skip_confirm, invalidate_cache=invalidate_cache,\n+                   console=c)\n \n     elif action == \"inspect\":\n         if not args:","@@ -1113,10 +1127,13 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:\n \n     elif action == \"uninstall\":\n         if not args:\n-            c.print(\"[bold red]Usage:[/] /skills uninstall <name> [--yes]\\n\")\n+            c.print(\"[bold red]Usage:[/] /skills uninstall <name> [--now]\\n\")\n             return\n-        skip_confirm = any(flag in args for flag in (\"--yes\", \"-y\"))\n-        do_uninstall(args[0], console=c, skip_confirm=skip_confirm)\n+        # Slash commands run inside prompt_toolkit where input() hangs.\n+        skip_confirm = True\n+        invalidate_cache = \"--now\" in args\n+        do_uninstall(args[0], console=c, skip_confirm=skip_confirm,\n+                     invalidate_cache=invalidate_cache)\n \n     elif action == \"publish\":\n         if not args:"]},{"file":"tests/hermes_cli/test_skills_skip_confirm.py","hunks":["@@ -1,10 +1,13 @@\n \"\"\"\n-Tests for skip_confirm behavior in /skills install and /skills uninstall.\n+Tests for skip_confirm and invalidate_cache behavior in /skills install\n+and /skills uninstall slash commands.\n \n-Verifies that --yes / -y bypasses the interactive confirmation prompt\n-that hangs inside prompt_toolkit's TUI.\n+Slash commands always skip confirmation (input() hangs in TUI).\n+Cache invalidation is deferred by default; --now opts into immediate\n+invalidation (at the cost of breaking prompt cache mid-session).\n \n Based on PR #1595 by 333Alden333 (salvaged).\n+Updated for PR #3586 (cache-aware install/uninstall).\n \"\"\"\n \n from unittest.mock import patch, MagicMock","@@ -32,23 +35,43 @@ def test_y_flag_sets_skip_confirm(self):\n             _, kwargs = mock_install.call_args\n             assert kwargs.get(\"skip_confirm\") is True\n \n-    def test_force_flag_sets_force_not_skip(self):\n+    def test_force_flag_sets_force(self):\n         from hermes_cli.skills_hub import handle_skills_slash\n         with patch(\"hermes_cli.skills_hub.do_install\") as mock_install:\n             handle_skills_slash(\"/skills install test/skill --force\")\n             mock_install.assert_called_once()\n             _, kwargs = mock_install.call_args\n             assert kwargs.get(\"force\") is True\n-            assert kwargs.get(\"skip_confirm\") is False\n+            # Slash commands always skip confirmation (input() hangs in TUI)\n+            assert kwargs.get(\"skip_confirm\") is True\n \n-    def test_no_flags(self):\n+    def test_no_flags_still_skips_confirm(self):\n+        \"\"\"Slash commands always skip confirmation — input() hangs in TUI.\"\"\"\n         from hermes_cli.skills_hub import handle_skills_slash\n         with patch(\"hermes_cli.skills_hub.do_install\") as mock_install:\n             handle_skills_slash(\"/skills install test/skill\")\n             mock_install.assert_called_once()\n             _, kwargs = mock_install.call_args\n             assert kwargs.get(\"force\") is False\n-            assert kwargs.get(\"skip_confirm\") is False\n+            assert kwargs.get(\"skip_confirm\") is True\n+\n+    def test_default_defers_cache_invalidation(self):\n+        \"\"\"Without --now, cache invalidation is deferred to next session.\"\"\"\n+        from hermes_cli.skills_hub import handle_skills_slash\n+        with patch(\"hermes_cli.skills_hub.do_install\") as mock_install:\n+            handle_skills_slash(\"/skills install test/skill\")\n+            mock_install.assert_called_once()\n+            _, kwargs = mock_install.call_args\n+            assert kwargs.get(\"invalidate_cache\") is False\n+\n+    def test_now_flag_invalidates_cache(self):\n+        \"\"\"--now opts into immediate cache invalidation.\"\"\"\n+        from hermes_cli.skills_hub import handle_skills_slash\n+        with patch(\"hermes_cli.skills_hub.do_install\") as mock_install:\n+            handle_skills_slash(\"/skills install test/skill --now\")\n+            mock_install.assert_called_once()\n+            _, kwargs = mock_install.call_args\n+            assert kwargs.get(\"invalidate_cache\") is True\n \n \n class TestHandleSkillsSlashUninstallFlags:","@@ -70,13 +93,32 @@ def test_y_flag_sets_skip_confirm(self):\n             _, kwargs = mock_uninstall.call_args\n             assert kwargs.get(\"skip_confirm\") is True\n \n-    def test_no_flags(self):\n+    def test_no_flags_still_skips_confirm(self):\n+        \"\"\"Slash commands always skip confirmation — input() hangs in TUI.\"\"\"\n         from hermes_cli.skills_hub import handle_skills_slash\n         with patch(\"hermes_cli.skills_hub.do_uninstall\") as mock_uninstall:\n             handle_skills_slash(\"/skills uninstall test-skill\")\n             mock_uninstall.assert_called_once()\n             _, kwargs = mock_uninstall.call_args\n-            assert kwargs.get(\"skip_confirm\", False) is False\n+            assert kwargs.get(\"skip_confirm\") is True\n+\n+    def test_default_defers_cache_invalidation(self):\n+        \"\"\"Without --now, cache invalidation is deferred to next session.\"\"\"\n+        from hermes_cli.skills_hub import handle_skills_slash\n+        with patch(\"hermes_cli.skills_hub.do_uninstall\") as mock_uninstall:\n+            handle_skills_slash(\"/skills uninstall test-skill\")\n+            mock_uninstall.assert_called_once()\n+            _, kwargs = mock_uninstall.call_args\n+            assert kwargs.get(\"invalidate_cache\") is False\n+\n+    def test_now_flag_invalidates_cache(self):\n+        \"\"\"--now opts into immediate cache invalidation.\"\"\"\n+        from hermes_cli.skills_hub import handle_skills_slash\n+        with patch(\"hermes_cli.skills_hub.do_uninstall\") as mock_uninstall:\n+            handle_skills_slash(\"/skills uninstall test-skill --now\")\n+            mock_uninstall.assert_called_once()\n+            _, kwargs = mock_uninstall.call_args\n+            assert kwargs.get(\"invalidate_cache\") is True\n \n \n class TestDoInstallSkipConfirm:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}