Skip to content

Commit 44f004a

Browse files
chapterjasonclaude
andcommitted
Sanitize history replay and ship tmux config
Every page reload used to append a stray "1; 2c0; 276; 0c" to the shell: xterm processed a Device Attributes query from tmux's scrollback, replied, and tmux forwarded the reply to the inner shell. - sanitizeForReplay strips CSI terminal-query/status sequences (final byte c/n) and OSC color queries (10/11;?) from the history frame. Colors and formatting are preserved. - server/tmux.conf centralises tmux options (default-terminal, escape-time 0, focus-events on, status off, history-limit 10000, set-clipboard on, set-titles off, aggressive-resize on). tmux now starts with `-f tmux.conf` and each session re-sources it so a long-running daemon picks up changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7b7ee03 commit 44f004a

4 files changed

Lines changed: 31 additions & 3 deletions

File tree

server/src/session/replay.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Strip escape sequences that would trigger the terminal to emit a
2+
// response (Device Attributes, cursor-position / status reports, OSC
3+
// color queries). Replaying them lets xterm re-answer, and those
4+
// answers leak onto the shell.
5+
const CSI_QUERY = /\x1b\[[?>=]?[0-9;]*[cn]/g;
6+
const OSC_COLOR_QUERY = /\x1b\][0-9]+;\?(?:\x07|\x1b\\)/g;
7+
8+
export function sanitizeForReplay(data: string): string {
9+
return data.replace(CSI_QUERY, "").replace(OSC_COLOR_QUERY, "");
10+
}

server/src/session/session.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { spawn, type IPty } from "node-pty";
22
import { randomUUID } from "node:crypto";
3+
import { dirname, resolve } from "node:path";
4+
import { fileURLToPath } from "node:url";
35
import { SCROLLBACK_BYTES } from "../config.js";
46
import type { SessionInfo } from "../types/session.js";
7+
import { sanitizeForReplay } from "./replay.js";
58
import { Scrollback } from "./scrollback.js";
69
import * as tmux from "./tmux.js";
710

11+
const TMUX_CONF = resolve(dirname(fileURLToPath(import.meta.url)), "../../tmux.conf");
12+
813
export type OutputListener = (chunk: string) => void;
914
export type ExitListener = (code: number, signal?: number) => void;
1015

@@ -47,6 +52,8 @@ export class Session {
4752
this.pty = spawn(
4853
"tmux",
4954
[
55+
"-f",
56+
TMUX_CONF,
5057
"new-session",
5158
"-A",
5259
"-s",
@@ -79,8 +86,7 @@ export class Session {
7986
}
8087

8188
private async configureTmux(): Promise<void> {
82-
await tmux.setOption(this.tmuxName, "status", "off");
83-
await tmux.setOption(this.tmuxName, "history-limit", "10000");
89+
await tmux.sourceFile(TMUX_CONF);
8490
await tmux.setTitle(this.tmuxName, this._title);
8591
}
8692

@@ -116,7 +122,7 @@ export class Session {
116122
}
117123

118124
history(): string {
119-
return this.scrollback.snapshot();
125+
return sanitizeForReplay(this.scrollback.snapshot());
120126
}
121127

122128
subscribe(onData: OutputListener, onExit: ExitListener): () => void {

server/src/session/tmux.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,7 @@ export async function setOption(name: string, option: string, value: string): Pr
8484
export async function kill(name: string): Promise<void> {
8585
await run(["kill-session", "-t", name]).catch(() => {});
8686
}
87+
88+
export async function sourceFile(path: string): Promise<void> {
89+
await run(["source-file", path]).catch(() => {});
90+
}

server/tmux.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
set-option -g default-terminal "xterm-256color"
2+
set-option -g escape-time 0
3+
set-option -g focus-events on
4+
set-option -g set-titles off
5+
set-option -g aggressive-resize on
6+
set-option -g set-clipboard on
7+
set-option -g status off
8+
set-option -g history-limit 10000

0 commit comments

Comments
 (0)