# NousResearch/hermes-agent · Ctrl+C during slash commands exits session unexpectedly

## Open now

- Nothing open on GitHub.

A bug where pressing Ctrl+C during slow slash commands exits the entire session rather than just interrupting the command. Two proposed fixes and one bug report address catching KeyboardInterrupt to prevent unintended session termination.

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

- #5142: \[Bug\] Ctrl+C during slow slash commands exits session instead of interrupting

## Prior & competing PRs

- **#32017: fix(cli): catch KeyboardInterrupt during slash commands to prevent session exit**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `tests/cli/test_slash_command_interrupt.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -14200,11 +14200,19 @@ def process_loop():
     
                         if not _file_drop and isinstance(user_input, str) and _looks_like_slash_command(user_input):
                             _cprint(f"\n⚙️  {user_input}")
    -                        if not self.process_command(user_input):
    -                            self._should_exit = True
    -                            # Schedule app exit
    -                            if app.is_running:
    -                                app.exit()
    +                        try:
    +                            if not self.process_command(user_input):
    +                                self._should_exit = True
    +                                # Schedule app exit
    +                                if app.is_running:
    +                                    app.exit()
    +                        except KeyboardInterrupt:
    +                            # Ctrl+C during a slow slash command (e.g. /skills browse,
    +                            # /sessions list with a large DB) should interrupt the
    +                            # command and return to the prompt, NOT exit the entire
    +                            # session. Without this guard a KeyboardInterrupt unwinds
    +                            # to the outer prompt_toolkit loop and the session dies.
    +                            _cprint("\n[dim]Command interrupted.[/dim]")
                             continue
                         
                         # Expand paste references back to full content
    ```
  - `tests/cli/test_slash_command_interrupt.py`

    ```diff
    @@ -0,0 +1,113 @@
    +"""Tests for the KeyboardInterrupt guard around slash command dispatch.
    +
    +A Ctrl+C during a slow slash command (e.g. /skills browse on a large
    +skill tree, or /sessions list against a multi-GB SQLite DB) used to
    +unwind to the outer prompt_toolkit loop and kill the entire session.
    +The fix wraps `self.process_command(user_input)` in a try/except
    +KeyboardInterrupt so the command aborts but the session survives.
    +
    +These tests verify the contract without spinning up the full
    +prompt_toolkit input loop. We exercise the same try/except by calling
    +through a thin wrapper that mirrors the real dispatch shape.
    +"""
    +
    +from unittest.mock import MagicMock, patch
    +
    +from cli import HermesCLI
    +
    +
    +def _make_cli():
    +    cli = HermesCLI.__new__(HermesCLI)
    +    cli._should_exit = False
    +    cli.conversation_history = []
    +    cli.agent = None
    +    cli._session_db = None
    +    return cli
    +
    +
    +def _dispatch(cli, user_input: str, process_command_side_effect=None):
    +    """Mirror the production dispatch shape from cli.py around line 14236.
    +
    +    Real call site:
    +        if not _file_drop and isinstance(user_input, str) and _looks_like_slash_command(user_input):
    +            _cprint(f"\\n⚙️  {user_input}")
    +            try:
    +                if not self.process_command(user_input):
    +                    self._should_exit = True
    +                    if app.is_running:
    +                        app.exit()
    +            except KeyboardInterrupt:
    +                _cprint("\\n[dim]Command interrupted.[/dim]")
    +            continue
    +    """
    +    if process_command_side_effect is not None:
    +        with patch.object(cli, "process_command", side_effect=process_command_side_effect) as mock_pc:
    +            try:
    +                if not cli.process_command(user_input):
    +                    cli._should_exit = True
    +            except KeyboardInterrupt:
    +                # Mirror production: swallow, do NOT raise.
    +                pass
    +            return mock_pc
    +
    +
    +class TestSlashCommandKeyboardInterrupt:
    +    def test_keyboardinterrupt_in_slash_command_does_not_set_exit(self):
    +        """Ctrl+C in the middle of /skills browse must NOT set _should_exit.
    +
    +        Before the fix: KeyboardInterrupt unwinds past the dispatch,
    +        the outer event loop catches it, session dies.
    +        After the fix: KeyboardInterrupt is caught locally, _should_exit
    +        stays False, the prompt loop continues.
    +        """
    +        cli = _make_cli()
    +
    +        def raises_keyboard_interrupt(_cmd):
    +            raise KeyboardInterrupt("user pressed Ctrl+C during slow command")
    +
    +        _dispatch(cli, "/skills browse", process_command_side_effect=raises_keyboard_interrupt)
    +
    +        assert cli._should_exit is False, (
    +            "KeyboardInterrupt during slash command must not flag exit"
    +        )
    +
    +    def test_normal_slash_command_returns_truthy_keeps_session_alive(self):
    +        """A successful slash command (returns truthy) must NOT set _should_exit."""
    +        cli = _make_cli()
    +
    +        _dispatch(cli, "/help", process_command_side_effect=[True])
    +
    +        assert cli._should_exit is False
    +
    +    def test_slash_command_returning_false_sets_exit(self):
    +        """The legitimate exit signal — process_command() returning False —
    +        still sets _should_exit. This is the path /exit / /quit use."""
    +        cli = _make_cli()
    +
    +        _dispatch(cli, "/exit", process_command_side_effect=[False])
    +
    +        assert cli._should_exit is True
    +
    +    def test_other_exceptions_propagate(self):
    +        """Only KeyboardInterrupt is caught locally. Other exceptions must
    +        propagate so they show up in logs and the global handler can deal
    +        with them — silently swallowing all exceptions would mask bugs."""
    +        cli = _make_cli()
    +
    +        class CustomError(Exception):
    +            pass
    +
    +        def raises_custom(_cmd):
    +            raise CustomError("real bug")
    +
    +        try:
    +            with patch.object(cli, "process_command", side_effect=raises_custom):
    +                try:
    +                    if not cli.process_command("/something"):
    +                        cli._should_exit = True
    +                except KeyboardInterrupt:
    +                    pass  # would NOT catch CustomError
    +        except CustomError:
    +            return  # expected — non-KBI exceptions propagate
    +
    +        raise AssertionError("CustomError should have propagated")
    ```
- **#5189: fix(cli): catch KeyboardInterrupt during slash commands to prevent session exit**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#32017`

## Fix status

- Merged to main in PR #32017 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-25`.
- Subsystem: `hermes_cli`
