{"repo":"NousResearch/hermes-agent","public_id":"4256","display_label":"Ctrl+Shift+C keybinding crash and clipboard conflicts","summary":"A cluster of bugs, fixes, and related work around an invalid 'c-S-c' (Ctrl+Shift+C) keybinding that crashes prompt_toolkit on startup across platforms, plus adjacent issues with Ctrl+C copy/interrupt conflicts, macOS keybinding parity, and help text accuracy.","subsystem":"docs","counts":{"members":18,"open_issues":2,"open_issues_distinct":2,"open_prs":2,"closed_issues":6,"merged_prs":3,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19884},"resolution":{"state":"partially_resolved","by":"pr-19895"},"freshness":{"state":"verified","last_verified_at":"2026-05-04T21:49:38+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.11503267973856207},"fix_edges":[{"issue_number":16181,"pr_number":19884,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":19894,"pr_number":19895,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":19896,"pr_number":19895,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":19903,"pr_number":19919,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":4256,"title":"[UX] Support configurable keybindings via config.yaml","url":null,"state":"open"},{"kind":"issue","number":16181,"title":"[Bug]: Ctrl+C Copy/Interrupt Conflict in CLI","url":null,"state":"closed"},{"kind":"issue","number":19894,"title":"macOS: hermes crashes at startup — Invalid key c-S-c (Ctrl+Shift+C)","url":null,"state":"closed"},{"kind":"issue","number":19896,"title":"[Bug]: Linux: hermes crashes at startup — Invalid key c-S-c","url":null,"state":"closed"},{"kind":"issue","number":19903,"title":"CLI crash on startup: Invalid key 'c-S-c' — prompt_toolkit doesn't support Shift modifier","url":null,"state":"closed"},{"kind":"issue","number":20202,"title":"cli.py crashes on startup in WSL terminals that don't support c-S-c keybinding","url":null,"state":"closed"},{"kind":"issue","number":21564,"title":"/help command text incorrect","url":null,"state":"open"},{"kind":"issue","number":46448,"title":"feat(tui): pass Ctrl+C to VSCode terminal for native clipboard copy","url":null,"state":"closed"},{"kind":"pr","number":16183,"title":"fix(cli): fix shortcut config conflict in hermes_cli","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19884","number":19884}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19884,"title":"fix(cli): resolve Ctrl+C copy/interrupt conflict with Ctrl+Shift+C + Ctrl+Q aliases","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_cli/config.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -10483,7 +10483,98 @@ def handle_ctrl_c(event):\n                 else:\n                     self._should_exit = True\n                     event.app.exit()\n-        \n+\n+        @kb.add('c-S-c')  # Ctrl+Shift+C\n+        def handle_ctrl_shift_c(event):\n+            \"\"\"Copy text to clipboard (terminal-native).\n+\n+            This is a no-op at the application level. Terminal emulators\n+            handle the actual copy operation when Ctrl+Shift+C is pressed.\n+            This binding prevents Hermes from intercepting the keystroke\n+            as an interrupt signal.\n+\n+            On macOS the standard copy shortcut is Cmd+C (no Hermes binding\n+            needed). On Linux/Windows Ctrl+Shift+C is the conventional\n+            terminal copy shortcut.\n+            \"\"\"\n+            return  # No-op — let the terminal perform native copy\n+\n+        @kb.add('c-q')  # Ctrl+Q\n+        def handle_ctrl_q(event):\n+            \"\"\"Alternative interrupt/exit shortcut (Ctrl+Q).\n+\n+            Behaves like Ctrl+C: cancels active prompts, interrupts the\n+            running agent, or clears the input buffer. Does not support\n+            the double-press 'force exit' feature of Ctrl+C.\n+            \"\"\"\n+            # Cancel active voice recording.\n+            _should_cancel_voice = False\n+            _recorder_ref = None\n+            with cli_ref._voice_lock:\n+                if cli_ref._voice_recording and cli_ref._voice_recorder:\n+                    _recorder_ref = cli_ref._voice_recorder\n+                    cli_ref._voice_recording = False\n+                    cli_ref._voice_continuous = False\n+                    _should_cancel_voice = True\n+            if _should_cancel_voice:\n+                _cprint(f\"\\n{_DIM}Recording cancelled.{_RST}\")\n+                threading.Thread(\n+                    target=_recorder_ref.cancel, daemon=True\n+                ).start()\n+                event.app.invalidate()\n+                return\n+\n+            # Cancel sudo prompt\n+            if self._sudo_state:\n+                self._sudo_state[\"response_queue\"].put(\"\")\n+                self._sudo_state = None\n+                event.app.invalidate()\n+                return\n+\n+            # Cancel secret prompt\n+            if self._secret_state:\n+                self._cancel_secret_capture()\n+                event.app.current_buffer.reset()\n+                event.app.invalidate()\n+                return\n+\n+            # Cancel approval prompt (deny)\n+            if self._approval_state:\n+                self._approval_state[\"response_queue\"].put(\"deny\")\n+                self._approval_state = None\n+                event.app.invalidate()\n+                return\n+\n+            # Cancel /model picker\n+            if self._model_picker_state:\n+                self._close_model_picker()\n+                event.app.current_buffer.reset()\n+                event.app.invalidate()\n+                return\n+\n+            # Cancel clarify prompt\n+            if self._clarify_state:\n+                self._clarify_state[\"response_queue\"].put(\n+                    \"The user cancelled. Use your best judgement to proceed.\"\n+                )\n+                self._clarify_state = None\n+                self._clarify_freetext = False\n+                event.app.current_buffer.reset()\n+                event.app.invalidate()\n+                return\n+\n+            if self._agent_running and self.agent:\n+                print(\"\\n⚡ Interrupting agent...\")\n+                self.agent.interrupt()\n+            else:\n+                if event.app.current_buffer.text or self._attached_images:\n+                    event.app.current_buffer.reset()\n+                    self._attached_images.clear()\n+                    event.app.invalidate()\n+                else:\n+                    self._should_exit = True\n+                    event.app.exit()\n+\n         @kb.add('c-d')\n         def handle_ctrl_d(event):\n             \"\"\"Ctrl+D: delete char under cursor (standard readline behaviour)."]},{"file":"hermes_cli/config.py","hunks":["@@ -809,6 +809,7 @@ def _ensure_hermes_home_managed(home: Path):\n             \"enabled\": False,\n             \"fields\": [\"model\", \"context_pct\", \"cwd\"],  # Order shown; drop any to hide\n         },\n+        \"copy_shortcut\": \"auto\",  # \"auto\" (platform default) | \"ctrl_c\" | \"ctrl_shift_c\" | \"disabled\"\n     },\n \n     # Web dashboard settings\n"]}]},{"kind":"pr","number":19895,"title":"fix(cli): guard c-S-c key binding with try/except to prevent startup crash","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -10484,20 +10484,19 @@ def handle_ctrl_c(event):\n                     self._should_exit = True\n                     event.app.exit()\n \n-        @kb.add('c-S-c')  # Ctrl+Shift+C\n-        def handle_ctrl_shift_c(event):\n-            \"\"\"Copy text to clipboard (terminal-native).\n-\n-            This is a no-op at the application level. Terminal emulators\n-            handle the actual copy operation when Ctrl+Shift+C is pressed.\n-            This binding prevents Hermes from intercepting the keystroke\n-            as an interrupt signal.\n-\n-            On macOS the standard copy shortcut is Cmd+C (no Hermes binding\n-            needed). On Linux/Windows Ctrl+Shift+C is the conventional\n-            terminal copy shortcut.\n-            \"\"\"\n-            return  # No-op — let the terminal perform native copy\n+        try:\n+            @kb.add('c-S-c')  # Ctrl+Shift+C — no-op, let terminal handle native copy\n+            def handle_ctrl_shift_c(event):\n+                \"\"\"No-op: let the terminal handle Ctrl+Shift+C natively.\n+\n+                Wrapped in try/except because prompt_toolkit raises ValueError\n+                (\"Invalid key: c-S-c\") on platforms where this key spec is not\n+                recognised.  The binding is best-effort; if registration fails,\n+                startup continues normally.\n+                \"\"\"\n+                return\n+        except ValueError:\n+            pass  # prompt_toolkit on this platform/version doesn't support c-S-c\n \n         @kb.add('c-q')  # Ctrl+Q\n         def handle_ctrl_q(event):\n"]}]},{"kind":"pr","number":19907,"title":"fix(cli): remove invalid ctrl-shift-c keybinding","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19894","number":19894},{"kind":"pr","value":"#19902","number":19902},{"kind":"pr","value":"#19906","number":19906}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":19912,"title":"fix(cli): remove invalid 'c-S-c' keybinding that crashes prompt_toolkit","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19919","number":19919}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":19919,"title":"refactor(cli): drop dead c-S-c key binding (follow-up to #19895)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -10484,19 +10484,14 @@ def handle_ctrl_c(event):\n                     self._should_exit = True\n                     event.app.exit()\n \n-        try:\n-            @kb.add('c-S-c')  # Ctrl+Shift+C — no-op, let terminal handle native copy\n-            def handle_ctrl_shift_c(event):\n-                \"\"\"No-op: let the terminal handle Ctrl+Shift+C natively.\n-\n-                Wrapped in try/except because prompt_toolkit raises ValueError\n-                (\"Invalid key: c-S-c\") on platforms where this key spec is not\n-                recognised.  The binding is best-effort; if registration fails,\n-                startup continues normally.\n-                \"\"\"\n-                return\n-        except ValueError:\n-            pass  # prompt_toolkit on this platform/version doesn't support c-S-c\n+        # Ctrl+Shift+C: no binding needed. Terminal emulators (GNOME Terminal,\n+        # iTerm2, kitty, Windows Terminal, etc.) intercept Ctrl+Shift+C before\n+        # the keystroke reaches the application's stdin — prompt_toolkit never\n+        # sees it, and prompt_toolkit's key spec parser doesn't even recognise\n+        # 'c-S-c' anyway (the Shift modifier is meaningless on control-sequence\n+        # keys). #19884 added a handler for this; #19895 patched the resulting\n+        # startup crash with try/except. Both were based on a misreading of how\n+        # terminal key events propagate. Deleting the dead handler outright.\n \n         @kb.add('c-q')  # Ctrl+Q\n         def handle_ctrl_q(event):\n"]}]},{"kind":"pr","number":21753,"title":"fix(tui): avoid unsupported Cmd hotkeys in macOS help","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":23128,"title":"fix(tui): clarify copy shortcut help text","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":26318,"title":"fix(tui): document portable macOS help shortcuts","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21753","number":21753},{"kind":"pr","value":"#23128","number":23128},{"kind":"pr","value":"#21564","number":21564}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":51830,"title":"fix(tui): correct macOS hotkey help text to reflect actual TUI behavior","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#21564","number":21564},{"kind":"pr","value":"#21753","number":21753},{"kind":"pr","value":"#26318","number":26318}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}