# NousResearch/hermes-agent · SGR mouse escape sequences leaking into input/composer

## Open now

- Nothing open on GitHub.

Multiple issues and proposed fixes centered on SGR mouse tracking codes leaking into the terminal input buffer or composer, covering detection, recovery, and prevention strategies.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #35512. Verify it addresses your case.
- **Recommended:** read PR #35512 (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_PARTIAL`): 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.34`: 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

- #18658: fix(tui): SGR mouse sequence leakage into composer under heavy render in tmux
- #29557: \[Bug\]: Mouse tracking escape sequences leak into input buffer (raw SGR mouse codes appear in prompt input)

## Prior & competing PRs

- **#35512: fix(tui): swallow degraded mouse-burst noise so a stalled loop can't lock the composer**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts`, `ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts`
  - fix hunks:
  - `ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts`

    ```diff
    @@ -133,4 +133,35 @@ describe('fragmented SGR mouse recovery', () => {
     
         expect(key).toMatchObject({ kind: 'key', sequence: '1234;56;78M9;10;11M' })
       })
    +
    +  it('swallows a fully degraded mouse-burst noise blob without leaking prompt text', () => {
    +    // Captured from Windows Terminal during a heavy tool-call render: the event
    +    // loop blocked past App's 50ms flush timer, so a long burst of SGR mouse
    +    // reports (mode 1003 any-motion) arrived as text with prefixes AND
    +    // too degraded for SGR_MOUSE_FRAGMENT_RE (1- and 2-param remnants, a
    +    // stray focus-in `[I`), so without the whole-text noise fast path the entire
    +    // blob types into the composer and locks the user out.
    +    const blob =
    +      'M6M35;220;56M6M35;218;56M169;48M;157;47M;44M20;43M79;40M78;40M0M7M35;49;41M48;41M;47;40M9;15;32M[I;31M5;211;26M35;211;25M7M;220;1MM0M09;25M24M23M3;22MM18M99;26M32MM38M63;44M47MM1;51M M4M54M'
    +    const [events] = parseMultipleKeypresses(INITIAL_STATE, blob)
    +
    +    expect(events).toEqual([])
    +  })
    +
    +  it('keeps plain prose that only contains scattered M and m letters', () => {
    +    const [[key]] = parseMultipleKeypresses(INITIAL_STATE, 'Mmm MMM mmm yummy')
    +
    +    expect(key).toMatchObject({ kind: 'key', sequence: 'Mmm MMM mmm yummy' })
    +  })
    +
    +  it('swallows noise wholesale even when it contains intact recoverable fragments', () => {
    +    // A noise blob can carry a few intact `<b;c;r M` fragments amid the chewed
    +    // shards. The whole-text noise check must run BEFORE fragment recovery —
    +    // otherwise parseTextWithSgrMouseFragments returns non-null and emits a
    +    // pile of recovered mouse events instead of dropping the blob wholesale.
    +    const blob = '<35;159;11M;44M20;43M0M7M<35;124;26M;47;40M9;15;32M5M2M'
    +    const [events] = parseMultipleKeypresses(INITIAL_STATE, blob)
    +
    +    expect(events).toEqual([])
    +  })
     })
    ```
  - `ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts`

    ```diff
    @@ -65,6 +65,34 @@ const XTVERSION_RE = /^\x1bP>\|(.*?)(?:\x07|\x1b\\)$/s
     const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/
     const SGR_MOUSE_FRAGMENT_RE = /(?<!\d)(?:\[<|<)?(?:[0-9]|[1-9][0-9]|1\d{2}|2[0-4]\d|25[0-5]);\d+;\d+[Mm]/g
     
    +// Whole-text mouse-burst noise fast path. When a heavy render blocks the event
    +// loop past App's 50ms flush watchdog, a long burst of SGR mouse reports (mode
    +// 1003 any-motion / 1006 SGR) can arrive as a single text token with prefixes
    +// AND coordinate digits chewed off across many partial reads. The surviving
    +// shards (1- and 2-param remnants, stray focus-in `[I`, lone `M`/`m`
    +// terminators) are too degraded for SGR_MOUSE_FRAGMENT_RE, so the leftover
    +// tail leaks into the composer and locks the user out (they can't type or exit).
    +//
    +// If the ENTIRE text token is drawn only from the mouse-leak alphabet
    +// (`[ ] < ; I M m`, digits, and the stray spaces a burst can carry) AND it
    +// carries the structural signature of mouse coordinates — ≥3 `M`/`m`
    +// terminators, at least one digit, and at least one `;` separator — swallow it
    +// wholesale. All three constraints together preserve real prose: `Mmm MMM mmm`
    +// has no digit and no `;`, `see 1;2;3M for details` contains disqualifying
    +// letters, and `1234;56;78M9;10;11M` has only two terminators.
    +// eslint-disable-next-line no-control-regex
    +const MOUSE_BURST_NOISE_RE = /^(?=[\s\S]*\d)(?=[\s\S]*;)(?=(?:[^Mm]*[Mm]){3})[\d;<\[\]IMm \x1b]+$/
    +
    +// Residual-shard variant for the gaps BETWEEN / AFTER recovered fragments
    +// inside parseTextWithSgrMouseFragments. A real recovery run leaves degraded
    +// remnants (e.g. `M6M`, `7M;220;1MM0M`, lone `;157;47M`) that are pure
    +// mouse-leak alphabet but too short to satisfy the ≥3-terminator whole-text
    +// rule. Swallow such a residue only when it is pure alphabet AND carries a
    +// digit AND at least one `M`/`m` — a prose gap like ` for details ` contains
    +// disqualifying letters and never matches.
    +// eslint-disable-next-line no-control-regex
    +const MOUSE_BURST_RESIDUE_RE = /^(?=[^\d]*\d)(?=[^Mm]*[Mm])[\d;<\[\]IMm \x1b]+$/
    +
     function createPasteKey(content: string): ParsedKey {
       return {
         kind: 'key',
    ```

    ```diff
    @@ -268,6 +296,16 @@ export function parseMultipleKeypresses(
         } else if (token.type === 'text') {
           if (inPaste) {
             pasteBuffer += token.value
    +      } else if (MOUSE_BURST_NOISE_RE.test(token.value)) {
    +        // Fully degraded mouse-burst noise — a heavy render (e.g. a sudo /
    +        // secret prompt repaint) blocked the event loop past App's 50ms flush
    +        // watchdog, so a long burst of SGR mouse reports arrived as text with
    +        // prefixes AND coordinate digits chewed off. Checked BEFORE fragment
    +        // recovery: a noise blob can still contain a few intact `<b;c;r M`
    +        // fragments, and parseTextWithSgrMouseFragments would then return
    +        // non-null and emit a pile of recovered mouse events instead of
    +        // dropping the blob wholesale. Swallow it here so it never leaks into
    +        // the composer (and we skip the extra fragment-recovery work mid-stall).
           } else {
             const mouseFragments = parseTextWithSgrMouseFragments(token.value)
     
    ```

    ```diff
    @@ -674,7 +712,12 @@ function parseTextWithSgrMouseFragments(text: string): ParsedInput[] | null {
         }
     
         if (first.index! > cursor) {
    -      parsed.push(parseKeypress(text.slice(cursor, first.index!)))
    +      const gap = text.slice(cursor, first.index!)
    +      // Skip pure mouse-leak residue between recovered fragments; only emit
    +      // real text gaps as keypresses.
    +      if (!MOUSE_BURST_RESIDUE_RE.test(gap)) {
    +        parsed.push(parseKeypress(gap))
    +      }
         }
     
         for (const match of run) {
    ```

    ```diff
    @@ -690,7 +733,12 @@ function parseTextWithSgrMouseFragments(text: string): ParsedInput[] | null {
       }
     
       if (cursor < text.length) {
    -    parsed.push(parseKeypress(text.slice(cursor)))
    +    const tail = text.slice(cursor)
    +    // Swallow a pure mouse-leak residue tail (the head fragments recovered, but
    +    // the burst trailed off into chewed-up shards). Emit only real trailing text.
    +    if (!MOUSE_BURST_RESIDUE_RE.test(tail)) {
    +      parsed.push(parseKeypress(tail))
    +    }
       }
     
       return parsed
    ```
- **#18695: fix(tui): drop corrupted SGR mouse fragments that escape existing recovery**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#34317: fix(tui): drop corrupted SGR mouse garbage that leaked into composer**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#35512`, `#36864`

## Fix status

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