Ctrl+Shift+C keybinding crash and clipboard conflicts

NousResearch/hermes-agent · cluster 4256 · docs

Data as of

4 open items. Fix merged in #19884. Check it covers your case.

The short answer

2open issues
2open PRs
18items grouped
Known?
Yes. 18 related items grouped here, 6 issues already closed.
Fix event
A fix was merged to main in PR #19884. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #19895. That is separate from the fix event.
Still open
2 issues and 2 PRs.

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.

Open issues2

  • Issue#4256[UX] Support configurable keybindings via config.yaml
  • Issue#21564/help command text incorrect

Open PRs2

  • PR#23128fix(tui): clarify copy shortcut help text
  • PR#51830fix(tui): correct macOS hotkey help text to reflect actual TUI behavior

Fixes on record

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

  • #19884 fix(cli): resolve Ctrl+C copy/interrupt conflict with Ctrl+Shift+C + Ctrl+Q aliases

    merged to main

    files: cli.py, hermes_cli/config.py

    Show the diff
    • cli.py
      @@ -10483,7 +10483,98 @@ def handle_ctrl_c(event):
                       else:
                           self._should_exit = True
                           event.app.exit()
      -        
      +
      +        @kb.add('c-S-c')  # Ctrl+Shift+C
      +        def handle_ctrl_shift_c(event):
      +            """Copy text to clipboard (terminal-native).
      +
      +            This is a no-op at the application level. Terminal emulators
      +            handle the actual copy operation when Ctrl+Shift+C is pressed.
      +            This binding prevents Hermes from intercepting the keystroke
      +            as an interrupt signal.
      +
      +            On macOS the standard copy shortcut is Cmd+C (no Hermes binding
      +            needed). On Linux/Windows Ctrl+Shift+C is the conventional
      +            terminal copy shortcut.
      +            """
      +            return  # No-op — let the terminal perform native copy
      +
      +        @kb.add('c-q')  # Ctrl+Q
      +        def handle_ctrl_q(event):
      +            """Alternative interrupt/exit shortcut (Ctrl+Q).
      +
      +            Behaves like Ctrl+C: cancels active prompts, interrupts the
      +            running agent, or clears the input buffer. Does not support
      +            the double-press 'force exit' feature of Ctrl+C.
      +            """
      +            # Cancel active voice recording.
      +            _should_cancel_voice = False
      +            _recorder_ref = None
      +            with cli_ref._voice_lock:
      +                if cli_ref._voice_recording and cli_ref._voice_recorder:
      +                    _recorder_ref = cli_ref._voice_recorder
      +                    cli_ref._voice_recording = False
      +                    cli_ref._voice_continuous = False
      +                    _should_cancel_voice = True
      +            if _should_cancel_voice:
      +                _cprint(f"\n{_DIM}Recording cancelled.{_RST}")
      +                threading.Thread(
      +                    target=_recorder_ref.cancel, daemon=True
      +                ).start()
      +                event.app.invalidate()
      +                return
      +
      +            # Cancel sudo prompt
      +            if self._sudo_state:
      +                self._sudo_state["response_queue"].put("")
      +                self._sudo_state = None
      +                event.app.invalidate()
      +                return
      +
      +            # Cancel secret prompt
      +            if self._secret_state:
      +                self._cancel_secret_capture()
      +                event.app.current_buffer.reset()
      +                event.app.invalidate()
      +                return
      +
      +            # Cancel approval prompt (deny)
      +            if self._approval_state:
      +                self._approval_state["response_queue"].put("deny")
      +                self._approval_state = None
      +                event.app.invalidate()
      +                return
      +
      +            # Cancel /model picker
      +            if self._model_picker_state:
      +                self._close_model_picker()
      +                event.app.current_buffer.reset()
      +                event.app.invalidate()
      +                return
      +
      +            # Cancel clarify prompt
      +            if self._clarify_state:
      +                self._clarify_state["response_queue"].put(
      +                    "The user cancelled. Use your best judgement to proceed."
      +                )
      +                self._clarify_state = None
      +                self._clarify_freetext = False
      +                event.app.current_buffer.reset()
      +                event.app.invalidate()
      +                return
      +
      +            if self._agent_running and self.agent:
      +                print("\n⚡ Interrupting agent...")
      +                self.agent.interrupt()
      +            else:
      +                if event.app.current_buffer.text or self._attached_images:
      +                    event.app.current_buffer.reset()
      +                    self._attached_images.clear()
      +                    event.app.invalidate()
      +                else:
      +                    self._should_exit = True
      +                    event.app.exit()
      +
               @kb.add('c-d')
               def handle_ctrl_d(event):
                   """Ctrl+D: delete char under cursor (standard readline behaviour).
    • hermes_cli/config.py
      @@ -809,6 +809,7 @@ def _ensure_hermes_home_managed(home: Path):
                   "enabled": False,
                   "fields": ["model", "context_pct", "cwd"],  # Order shown; drop any to hide
               },
      +        "copy_shortcut": "auto",  # "auto" (platform default) | "ctrl_c" | "ctrl_shift_c" | "disabled"
           },
       
           # Web dashboard settings
  • #19895 fix(cli): guard c-S-c key binding with try/except to prevent startup crash

    merged to main

    files: cli.py

    Show the diff
    • cli.py
      @@ -10484,20 +10484,19 @@ def handle_ctrl_c(event):
                           self._should_exit = True
                           event.app.exit()
       
      -        @kb.add('c-S-c')  # Ctrl+Shift+C
      -        def handle_ctrl_shift_c(event):
      -            """Copy text to clipboard (terminal-native).
      -
      -            This is a no-op at the application level. Terminal emulators
      -            handle the actual copy operation when Ctrl+Shift+C is pressed.
      -            This binding prevents Hermes from intercepting the keystroke
      -            as an interrupt signal.
      -
      -            On macOS the standard copy shortcut is Cmd+C (no Hermes binding
      -            needed). On Linux/Windows Ctrl+Shift+C is the conventional
      -            terminal copy shortcut.
      -            """
      -            return  # No-op — let the terminal perform native copy
      +        try:
      +            @kb.add('c-S-c')  # Ctrl+Shift+C — no-op, let terminal handle native copy
      +            def handle_ctrl_shift_c(event):
      +                """No-op: let the terminal handle Ctrl+Shift+C natively.
      +
      +                Wrapped in try/except because prompt_toolkit raises ValueError
      +                ("Invalid key: c-S-c") on platforms where this key spec is not
      +                recognised.  The binding is best-effort; if registration fails,
      +                startup continues normally.
      +                """
      +                return
      +        except ValueError:
      +            pass  # prompt_toolkit on this platform/version doesn't support c-S-c
       
               @kb.add('c-q')  # Ctrl+Q
               def handle_ctrl_q(event):
  • #19919 refactor(cli): drop dead c-S-c key binding (follow-up to #19895)

    merged to main

    files: cli.py

    Show the diff
    • cli.py
      @@ -10484,19 +10484,14 @@ def handle_ctrl_c(event):
                           self._should_exit = True
                           event.app.exit()
       
      -        try:
      -            @kb.add('c-S-c')  # Ctrl+Shift+C — no-op, let terminal handle native copy
      -            def handle_ctrl_shift_c(event):
      -                """No-op: let the terminal handle Ctrl+Shift+C natively.
      -
      -                Wrapped in try/except because prompt_toolkit raises ValueError
      -                ("Invalid key: c-S-c") on platforms where this key spec is not
      -                recognised.  The binding is best-effort; if registration fails,
      -                startup continues normally.
      -                """
      -                return
      -        except ValueError:
      -            pass  # prompt_toolkit on this platform/version doesn't support c-S-c
      +        # Ctrl+Shift+C: no binding needed. Terminal emulators (GNOME Terminal,
      +        # iTerm2, kitty, Windows Terminal, etc.) intercept Ctrl+Shift+C before
      +        # the keystroke reaches the application's stdin — prompt_toolkit never
      +        # sees it, and prompt_toolkit's key spec parser doesn't even recognise
      +        # 'c-S-c' anyway (the Shift modifier is meaningless on control-sequence
      +        # keys). #19884 added a handler for this; #19895 patched the resulting
      +        # startup crash with try/except. Both were based on a misreading of how
      +        # terminal key events propagate. Deleting the dead handler outright.
       
               @kb.add('c-q')  # Ctrl+Q
               def handle_ctrl_q(event):

5 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1 · stale or abandoned 1 · superseded by another PR 1 · other 1

  • #16183 fix(cli): fix shortcut config conflict in hermes_cli · merged via another change → #19884
  • #19907 fix(cli): remove invalid ctrl-shift-c keybinding · superseded by another PR → #19894, #19902, #19906
  • #19912 fix(cli): remove invalid 'c-S-c' keybinding that crashes prompt_toolkit · done elsewhere → #19919
  • #21753 fix(tui): avoid unsupported Cmd hotkeys in macOS help · stale or abandoned
  • #26318 fix(tui): document portable macOS help shortcuts · other → #21753, #23128, #21564
Closed issues · 6
  • Closed#16181[Bug]: Ctrl+C Copy/Interrupt Conflict in CLI
  • Closed#19894macOS: hermes crashes at startup — Invalid key c-S-c (Ctrl+Shift+C)
  • Closed#19896[Bug]: Linux: hermes crashes at startup — Invalid key c-S-c
  • Closed#19903CLI crash on startup: Invalid key 'c-S-c' — prompt_toolkit doesn't support Shift modifier
  • Closed#20202cli.py crashes on startup in WSL terminals that don't support c-S-c keybinding
  • Closed#46448feat(tui): pass Ctrl+C to VSCode terminal for native clipboard copy
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.12: 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
docs
Merged PRs
#19884, #19895, #19919
Closed issues
6
Attempts
5 PRs closed without merging
Fix link checked
4 May 2026

For your agent

The same facts, typed.