@@ -351,12 +351,12 @@ func (m *Model) handleEvent(ev client.Event) (tea.Model, tea.Cmd) {
351351 m .sandbox = ev .Sandbox
352352
353353 case "thinking" :
354- m .thinking .WriteString (ev .Content )
354+ m .thinking .WriteString (sanitize ( ev .Content ) )
355355 m .status = "thinking"
356356
357357 case "token" :
358358 if i := m .cur (); i >= 0 {
359- m .msgs [i ].content += ev .Content
359+ m .msgs [i ].content += sanitize ( ev .Content )
360360 m .msgs [i ].streaming = true
361361 }
362362 m .status = "responding"
@@ -509,7 +509,7 @@ func (m *Model) finalize() {
509509}
510510
511511func (m * Model ) addNote (s string ) {
512- m .notices = append (m .notices , s )
512+ m .notices = append (m .notices , sanitize ( s ) )
513513 if len (m .notices ) > 6 {
514514 m .notices = m .notices [len (m .notices )- 6 :]
515515 }
@@ -695,7 +695,37 @@ func linePreview(data string) string {
695695}
696696
697697func collapse (s string ) string {
698- return strings .Join (strings .Fields (s ), " " )
698+ return strings .Join (strings .Fields (sanitize (s )), " " )
699+ }
700+
701+ // sanitize strips terminal control sequences from untrusted content before it
702+ // is rendered. Agent output — streamed tokens, tool results, file contents,
703+ // resumed transcripts — is attacker-influenced; raw C0 control bytes (notably
704+ // ESC, 0x1b) could drive ANSI/OSC escapes that move the cursor, clear the
705+ // screen, or exfiltrate via OSC 52. We keep newlines and tabs and drop every
706+ // other control byte (and DEL), which defangs escape sequences by removing
707+ // their introducer while leaving readable text intact.
708+ func sanitize (s string ) string {
709+ if ! strings .ContainsFunc (s , isControl ) {
710+ return s
711+ }
712+ var b strings.Builder
713+ b .Grow (len (s ))
714+ for _ , r := range s {
715+ if ! isControl (r ) {
716+ b .WriteRune (r )
717+ }
718+ }
719+ return b .String ()
720+ }
721+
722+ // isControl reports whether r is a control character we strip from untrusted
723+ // text (C0 controls and DEL, except newline and tab).
724+ func isControl (r rune ) bool {
725+ if r == '\n' || r == '\t' {
726+ return false
727+ }
728+ return r < 0x20 || r == 0x7f
699729}
700730
701731// formatDuration renders a short, friendly elapsed time.
0 commit comments