# NousResearch/hermes-agent · Skills install command soft-locks TUI via blocking prompt

## Open now

- Nothing open on GitHub.

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.

## 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 #3586. Verify it addresses your case.
- **Recommended:** read PR #3586 (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-3586`. 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

- #3474: \[Bug\]: Soft-locking when installing obsidian-cli skill

## Prior & competing PRs

- **#3586: fix(skills): skip confirmation in /skills install and uninstall slash commands**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/skills_hub.py`, `tests/hermes_cli/test_skills_skip_confirm.py`
  - fix hunks:
  - `hermes_cli/skills_hub.py`

    ```diff
    @@ -304,7 +304,8 @@ def do_browse(page: int = 1, page_size: int = 20, source: str = "all",
     
     
     def do_install(identifier: str, category: str = "", force: bool = False,
    -               console: Optional[Console] = None, skip_confirm: bool = False) -> None:
    +               console: Optional[Console] = None, skip_confirm: bool = False,
    +               invalidate_cache: bool = True) -> None:
         """Fetch, quarantine, scan, confirm, and install a skill."""
         from tools.skills_hub import (
             GitHubAuth, create_source_router, ensure_hub_dirs,
    ```

    ```diff
    @@ -417,12 +418,16 @@ def do_install(identifier: str, category: str = "", force: bool = False,
         c.print(f"[bold green]Installed:[/] {install_dir.relative_to(SKILLS_DIR)}")
         c.print(f"[dim]Files: {', '.join(bundle.files.keys())}[/]\n")
     
    -    # Invalidate the skills prompt cache so the new skill appears immediately
    -    try:
    -        from agent.prompt_builder import clear_skills_system_prompt_cache
    -        clear_skills_system_prompt_cache(clear_snapshot=True)
    -    except Exception:
    -        pass
    +    if invalidate_cache:
    +        # Invalidate the skills prompt cache so the new skill appears immediately
    +        try:
    +            from agent.prompt_builder import clear_skills_system_prompt_cache
    +            clear_skills_system_prompt_cache(clear_snapshot=True)
    +        except Exception:
    +            pass
    +    else:
    +        c.print("[dim]Skill will be available in your next session.[/]")
    +        c.print("[dim]Use /reset to start a new session now, or --now to activate immediately (invalidates prompt cache).[/]\n")
     
     
     def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
    ```

    ```diff
    @@ -610,7 +615,8 @@ def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> N
     
     
     def do_uninstall(name: str, console: Optional[Console] = None,
    -                 skip_confirm: bool = False) -> None:
    +                 skip_confirm: bool = False,
    +                 invalidate_cache: bool = True) -> None:
         """Remove a hub-installed skill with confirmation."""
         from tools.skills_hub import uninstall_skill
     
    ```

    ```diff
    @@ -630,11 +636,15 @@ def do_uninstall(name: str, console: Optional[Console] = None,
         success, msg = uninstall_skill(name)
         if success:
             c.print(f"[bold green]{msg}[/]\n")
    -        try:
    -            from agent.prompt_builder import clear_skills_system_prompt_cache
    -            clear_skills_system_prompt_cache(clear_snapshot=True)
    -        except Exception:
    -            pass
    +        if invalidate_cache:
    +            try:
    +                from agent.prompt_builder import clear_skills_system_prompt_cache
    +                clear_skills_system_prompt_cache(clear_snapshot=True)
    +            except Exception:
    +                pass
    +        else:
    +            c.print("[dim]Change will take effect in your next session.[/]")
    +            c.print("[dim]Use /reset to start a new session now, or --now to apply immediately (invalidates prompt cache).[/]\n")
         else:
             c.print(f"[bold red]Error:[/] {msg}\n")
     
    ```

    ```diff
    @@ -1071,19 +1081,23 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
     
         elif action == "install":
             if not args:
    -            c.print("[bold red]Usage:[/] /skills install <identifier> [--category <cat>] [--force|--yes]\n")
    +            c.print("[bold red]Usage:[/] /skills install <identifier> [--category <cat>] [--force] [--now]\n")
                 return
             identifier = args[0]
             category = ""
    -        # --yes / -y bypasses confirmation prompt (needed in TUI mode)
    -        # --force handles reinstall override
    -        skip_confirm = any(flag in args for flag in ("--yes", "-y"))
    +        # Slash commands run inside prompt_toolkit where input() hangs.
    +        # Always skip confirmation — the user typing the command is implicit consent.
    +        skip_confirm = True
             force = "--force" in args
    +        # --now invalidates prompt cache immediately (costs more money).
    +        # Default: defer to next session to preserve cache.
    +        invalidate_cache = "--now" in args
             for i, a in enumerate(args):
                 if a == "--category" and i + 1 < len(args):
                     category = args[i + 1]
             do_install(identifier, category=category, force=force,
    -                   skip_confirm=skip_confirm, console=c)
    +                   skip_confirm=skip_confirm, invalidate_cache=invalidate_cache,
    +                   console=c)
     
         elif action == "inspect":
             if not args:
    ```

    ```diff
    @@ -1113,10 +1127,13 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None:
     
         elif action == "uninstall":
             if not args:
    -            c.print("[bold red]Usage:[/] /skills uninstall <name> [--yes]\n")
    +            c.print("[bold red]Usage:[/] /skills uninstall <name> [--now]\n")
                 return
    -        skip_confirm = any(flag in args for flag in ("--yes", "-y"))
    -        do_uninstall(args[0], console=c, skip_confirm=skip_confirm)
    +        # Slash commands run inside prompt_toolkit where input() hangs.
    +        skip_confirm = True
    +        invalidate_cache = "--now" in args
    +        do_uninstall(args[0], console=c, skip_confirm=skip_confirm,
    +                     invalidate_cache=invalidate_cache)
     
         elif action == "publish":
             if not args:
    ```
  - `tests/hermes_cli/test_skills_skip_confirm.py`

    ```diff
    @@ -1,10 +1,13 @@
     """
    -Tests for skip_confirm behavior in /skills install and /skills uninstall.
    +Tests for skip_confirm and invalidate_cache behavior in /skills install
    +and /skills uninstall slash commands.
     
    -Verifies that --yes / -y bypasses the interactive confirmation prompt
    -that hangs inside prompt_toolkit's TUI.
    +Slash commands always skip confirmation (input() hangs in TUI).
    +Cache invalidation is deferred by default; --now opts into immediate
    +invalidation (at the cost of breaking prompt cache mid-session).
     
     Based on PR #1595 by 333Alden333 (salvaged).
    +Updated for PR #3586 (cache-aware install/uninstall).
     """
     
     from unittest.mock import patch, MagicMock
    ```

    ```diff
    @@ -32,23 +35,43 @@ def test_y_flag_sets_skip_confirm(self):
                 _, kwargs = mock_install.call_args
                 assert kwargs.get("skip_confirm") is True
     
    -    def test_force_flag_sets_force_not_skip(self):
    +    def test_force_flag_sets_force(self):
             from hermes_cli.skills_hub import handle_skills_slash
             with patch("hermes_cli.skills_hub.do_install") as mock_install:
                 handle_skills_slash("/skills install test/skill --force")
                 mock_install.assert_called_once()
                 _, kwargs = mock_install.call_args
                 assert kwargs.get("force") is True
    -            assert kwargs.get("skip_confirm") is False
    +            # Slash commands always skip confirmation (input() hangs in TUI)
    +            assert kwargs.get("skip_confirm") is True
     
    -    def test_no_flags(self):
    +    def test_no_flags_still_skips_confirm(self):
    +        """Slash commands always skip confirmation — input() hangs in TUI."""
             from hermes_cli.skills_hub import handle_skills_slash
             with patch("hermes_cli.skills_hub.do_install") as mock_install:
                 handle_skills_slash("/skills install test/skill")
                 mock_install.assert_called_once()
                 _, kwargs = mock_install.call_args
                 assert kwargs.get("force") is False
    -            assert kwargs.get("skip_confirm") is False
    +            assert kwargs.get("skip_confirm") is True
    +
    +    def test_default_defers_cache_invalidation(self):
    +        """Without --now, cache invalidation is deferred to next session."""
    +        from hermes_cli.skills_hub import handle_skills_slash
    +        with patch("hermes_cli.skills_hub.do_install") as mock_install:
    +            handle_skills_slash("/skills install test/skill")
    +            mock_install.assert_called_once()
    +            _, kwargs = mock_install.call_args
    +            assert kwargs.get("invalidate_cache") is False
    +
    +    def test_now_flag_invalidates_cache(self):
    +        """--now opts into immediate cache invalidation."""
    +        from hermes_cli.skills_hub import handle_skills_slash
    +        with patch("hermes_cli.skills_hub.do_install") as mock_install:
    +            handle_skills_slash("/skills install test/skill --now")
    +            mock_install.assert_called_once()
    +            _, kwargs = mock_install.call_args
    +            assert kwargs.get("invalidate_cache") is True
     
     
     class TestHandleSkillsSlashUninstallFlags:
    ```

    ```diff
    @@ -70,13 +93,32 @@ def test_y_flag_sets_skip_confirm(self):
                 _, kwargs = mock_uninstall.call_args
                 assert kwargs.get("skip_confirm") is True
     
    -    def test_no_flags(self):
    +    def test_no_flags_still_skips_confirm(self):
    +        """Slash commands always skip confirmation — input() hangs in TUI."""
             from hermes_cli.skills_hub import handle_skills_slash
             with patch("hermes_cli.skills_hub.do_uninstall") as mock_uninstall:
                 handle_skills_slash("/skills uninstall test-skill")
                 mock_uninstall.assert_called_once()
                 _, kwargs = mock_uninstall.call_args
    -            assert kwargs.get("skip_confirm", False) is False
    +            assert kwargs.get("skip_confirm") is True
    +
    +    def test_default_defers_cache_invalidation(self):
    +        """Without --now, cache invalidation is deferred to next session."""
    +        from hermes_cli.skills_hub import handle_skills_slash
    +        with patch("hermes_cli.skills_hub.do_uninstall") as mock_uninstall:
    +            handle_skills_slash("/skills uninstall test-skill")
    +            mock_uninstall.assert_called_once()
    +            _, kwargs = mock_uninstall.call_args
    +            assert kwargs.get("invalidate_cache") is False
    +
    +    def test_now_flag_invalidates_cache(self):
    +        """--now opts into immediate cache invalidation."""
    +        from hermes_cli.skills_hub import handle_skills_slash
    +        with patch("hermes_cli.skills_hub.do_uninstall") as mock_uninstall:
    +            handle_skills_slash("/skills uninstall test-skill --now")
    +            mock_uninstall.assert_called_once()
    +            _, kwargs = mock_uninstall.call_args
    +            assert kwargs.get("invalidate_cache") is True
     
     
     class TestDoInstallSkipConfirm:
    ```
- **#3496: fix(skills): /skills install soft-locks in TUI due to blocking input() call  **. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#3586`

## Fix status

- Merged to main in PR #3586 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-28`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `hermes_cli`
