NousResearch/hermes-agent
Ctrl+C during slash commands exits session unexpectedly
Cluster 5142 · hermes cli. Data as of
Nothing open on GitHub. Fix merged in #32017 on 25 May 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 #32017. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- 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.
Open issues0
Open PRs0
Fixes on record
-
#32017 fix(cli): catch KeyboardInterrupt during slash commands to prevent session exit
files:
cli.py,tests/cli/test_slash_command_interrupt.pyShow the diff
-
cli.py@@ -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@@ -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")
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#5189 fix(cli): catch KeyboardInterrupt during slash commands to prevent session exit · merged via another change →
#32017
Closed issues · 1
- Closed#5142[Bug] Ctrl+C during slow slash commands exits session instead of interrupting
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.00means 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 LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #32017
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 25 May 2026