NousResearch/hermes-agent

Skills install command soft-locks TUI via blocking prompt

Cluster 3474 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #3586 on 28 Mar 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #3586. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #3586. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #3586 fix(skills): skip confirmation in /skills install and uninstall slash commands

    merged to main

    files: hermes_cli/skills_hub.py, tests/hermes_cli/test_skills_skip_confirm.py

    Show the diff
    • hermes_cli/skills_hub.py
      @@ -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,
      @@ -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:
      @@ -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
       
      @@ -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")
       
      @@ -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:
      @@ -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
      @@ -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
      @@ -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:
      @@ -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:

1 PR closed without merging

From the close comments: merged via another change 1

  • #3496 fix(skills): /skills install soft-locks in TUI due to blocking input() call · merged via another change → #3586
Closed issues · 1
  • Closed#3474[Bug]: Soft-locking when installing obsidian-cli skill
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#3586
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
28 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.