Wie ich OSC 52 Clipboard in xterm.js durch tmux zum Laufen gebracht habe
OSC 52 Clipboard funktioniert in xterm.js durch tmux nicht. Der Fix ist nicht set-clipboard oder terminal-features, sondern allow-passthrough.
Inhalt
Ich baue ein Web-Dashboard, das browserbasierten Terminal-Zugriff auf tmux-Sessions über xterm.js + WebSocket + PTY bietet. Claude Code hat einen /copy-Befehl, der über OSC 52 Text in die System-Zwischenablage kopiert. Funktioniert tadellos in iTerm2, Windows Terminal, Ghostty. Durch mein Web-Terminal: nichts.
Hat mich zwei Stunden und drei Sackgassen gekostet, um herauszufinden warum.
xterm.js verarbeitet OSC 52 nicht
Erstes Problem: xterm.js ignoriert OSC 52 standardmäßig. Die Escape-Sequenz kommt an, der Parser sieht sie, nichts passiert. Ich musste einen Handler manuell registrieren:
term.parser.registerOscHandler(52, (data) => {
const idx = data.indexOf(';')
if (idx === -1) return false
const b64 = data.substring(idx + 1)
if (!b64) return false
try {
const decoded = atob(b64)
const bytes = new Uint8Array(decoded.length)
for (let i = 0; i < decoded.length; i++) {
bytes[i] = decoded.charCodeAt(i)
}
const text = new TextDecoder().decode(bytes)
navigator.clipboard.writeText(text)
.catch(() => {
const el = document.createElement('textarea')
el.value = text
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
})
} catch (e) {}
return true
})Das Format ist \x1b]52;c;<base64>\x07. c ist die Clipboard-Selection, der Payload ist Base64-codierter Text. Eine Stolperfalle: atob gibt einen Binär-String zurück, kein UTF-8. Du brauchst TextDecoder, damit Nicht-ASCII-Zeichen korrekt landen.
Hätte der ganze Fix sein sollen. War es nicht.
tmux set-clipboard, der falsche Hinweis
Nachdem der Handler installiert war, funktionierte /copy in Claude Code immer noch nicht. Ich bin in den tmux-Konfigurations-Kaninchenbau gestiegen.
tmux hat set-clipboard, das die OSC-52-Behandlung steuert:
set -g set-clipboard external # OSC 52 ans äußere Terminal weiterreichenHat nicht geholfen. Dann habe ich gefunden, dass tmux die Terminfo-Capability Ms prüft, um zu entscheiden, ob das äußere Terminal OSC 52 unterstützt. Mein xterm-256color hatte sie nicht. tmux 3.2+ hat einen Workaround:
set -g terminal-features "xterm-256color:clipboard"Trotzdem nichts. Ich habe tmux list-buffers nach /copy geprüft: leer. tmux hat OSC 52 überhaupt nicht gesehen. Nichts davon war relevant.
Hier ist der Grund: set-clipboard steuert, wie tmux rohes OSC 52 behandelt. Aber Claude Code schickt innerhalb von tmux kein rohes OSC 52, sondern wickelt es in DCS-Passthrough ein (nächster Abschnitt). DCS-Passthrough ist ein komplett anderer Code-Pfad, der das Clipboard-Handling von tmux vollständig umgeht. set-clipboard external und terminal-features clipboard haben also das falsche Problem gelöst.
Der Durchbruch kam aus einem anderen Test: Ich habe printf '\ direkt in tmux ausgeführt. Hat funktioniert. Mein xterm.js-Handler hat gefeuert, „test“ landete in der Zwischenablage. Rohes OSC 52 läuft sauber durch tmux durch.
Warum funktioniert dann /copy von Claude Code nicht?
DCS-Passthrough, das eigentliche Problem
Claude Code erkennt $TMUX und wickelt OSC 52 in eine DCS-Passthrough-Hülle:
\ePtmux;\e\e]52;c;<base64>\a\e\\
Das sagt tmux: „reich diese Escape-Sequenz direkt ans äußere Terminal weiter“. Genau das machen Neovim und tmux-yank auch, der Standard-Ansatz für Tools, die wissen, dass sie in tmux laufen.
Der Haken: tmux 3.3+ blockiert DCS-Passthrough standardmäßig. allow-passthrough ist auf off. Die DCS-Hülle wird stillschweigend verworfen. Mein xterm.js-Handler sieht nie etwas.
tmux set-option -t <session> allow-passthrough onEine Option. Zwei Stunden.
Die vollständige Lösung
Im Backend (Python/FastAPI) Passthrough vor dem Attach aktivieren und beim Disconnect wieder zurücksetzen:
async def attach_session(session_name: str):
subprocess.run([
'tmux', 'set-option', '-t', session_name,
'allow-passthrough', 'on'
])
# ... attach via PTY ...
async def detach_session(session_name: str):
subprocess.run([
'tmux', 'set-option', '-t', session_name,
'-u', 'allow-passthrough' # restore default
])Pro Session, nicht global. Beim Disconnect zurücksetzen, um die Sicherheits-Angriffsfläche klein zu halten. DCS-Passthrough lässt die innere Shell beliebige Escape-Sequenzen an dein Terminal schicken, also ist „global an“ mehr Angriffsfläche, als du willst.
Im Frontend (xterm.js) den Handler auf jeder Terminal-Instanz registrieren:
function setupClipboard(term) {
term.parser.registerOscHandler(52, (data) => {
const idx = data.indexOf(';')
if (idx === -1) return false
const b64 = data.substring(idx + 1)
if (!b64) return false
try {
const decoded = atob(b64)
const bytes = new Uint8Array(decoded.length)
for (let i = 0; i < decoded.length; i++)
bytes[i] = decoded.charCodeAt(i)
navigator.clipboard.writeText(new TextDecoder().decode(bytes))
} catch (e) {}
return true
})
}Bei Split-Panes braucht jede Terminal-Instanz ihren eigenen Handler.
Debugging-Timeline
| Schritt | Was ich versucht habe | Ergebnis |
|---|---|---|
| 1 | OSC-52-Handler in xterm.js | /copy immer noch kaputt |
| 2 | set-clipboard external | Nichts (falscher Code-Pfad) |
| 3 | terminal-features clipboard | Nichts (gleicher Grund) |
| 4 | tmux list-buffers | Leer, tmux sieht die Sequenz nie |
| 5 | Rohes printf '\ | Funktioniert. Handler feuert |
| 6 | console.log + serverseitiges PTY-Log auf /copy | Keiner feuert |
| 7 | Erkannt: Claude Code wickelt in DCS-Passthrough ein | Sequenz von tmux blockiert |
| 8 | allow-passthrough on | Gefixt |
Schritt 5 war der Schlüssel: rohes OSC 52 hat funktioniert, das von Claude Code nicht. Gleiche Sequenz, andere Hülle.
Wenn du das selbst baust
Zwei Dinge brauchst du, und nur diese: einen OSC-52-Handler in xterm.js (xterm.js liefert keinen mit) und allow-passthrough on in tmux, damit DCS-eingewickelte Sequenzen von Claude Code oder Neovim deinen Handler erreichen.
Du brauchst weder set-clipboard external noch terminal-features clipboard. Die steuern rohes OSC 52, aber moderne Tools wickeln ihren Output in DCS-Passthrough, was diese ganze Pipeline umgeht.
Vergiss eines davon und das Clipboard scheitert lautlos. Kein Fehler, keine Warnung.
01 / Diskussion
Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.
02 / Weitere Artikel
OpenAI API-Guthaben für ChatGPT Pro vs. Claude Max API-Guthaben: gleiche Beträge, anderer Zaun um den Coding-Agenten
Ein headless Claude-Skript darf Anthropics Guthaben verbrauchen, ein Codex-Lauf mit API-Key darf das von OpenAI nicht. Außerdem schreibt jedes Lab eine Abrechnungsregel auf, die beim anderen fehlt, und genau diese Regeln entscheiden, welches Guthaben zu welcher Aufgabe passt.
GPT-6.1 Terra nach drei Tagen: die veröffentlichten Berichte, und warum keiner Terras Computer-Use-Tool testet
Terra wurde als Modell für Bildschirme und Klicks vermarktet. In den ersten drei Tagen erschienen ein Pelikan, eine Tabellenprüfung, ein Latenzvergleich, ein Quiz zum Bildschirmlesen und GitHubs Einschätzung zu C#. Am nächsten an Belegen dafür, wie Terra Sperren umgeht, kommt OpenAIs eigene System Card, und die Zahl, auf die es dort ankommt, ist, wie oft das Modell einen anderen Weg fand.
Claude Code Memory in Codex 0.165 importieren: memories vor dem ersten Import einschalten, sonst bleibt das Profil auf v1
Die Notizen zu kopieren ist der einfache Teil. Jede Claude-Code-Datei kommt unverändert an, und was Codex beim Sessionstart lädt, ist ein vom Modell geschriebener Verweis darauf, in einer Zusammenfassung mit harter Größengrenze. Zwei standardmäßig ausgeschaltete Schalter und ein überzähliges Verzeichnis entscheiden, in welcher Memory-Pipeline diese Dateien landen.