Max Nardit
Dev

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.

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:

javascript
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:

bash
set -g set-clipboard external  # OSC 52 ans äußere Terminal weiterreichen

Hat 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:

bash
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 '\e]52;c;dGVzdA==\a' 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.

bash
tmux set-option -t <session> allow-passthrough on

Eine Option. Zwei Stunden.

Die vollständige Lösung

Im Backend (Python/FastAPI) Passthrough vor dem Attach aktivieren und beim Disconnect wieder zurücksetzen:

python
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:

javascript
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

SchrittWas ich versucht habeErgebnis
1OSC-52-Handler in xterm.js/copy immer noch kaputt
2set-clipboard externalNichts (falscher Code-Pfad)
3terminal-features clipboardNichts (gleicher Grund)
4tmux list-buffersLeer, tmux sieht die Sequenz nie
5Rohes printf '\e]52;c;dGVzdA==\a'Funktioniert. Handler feuert
6console.log + serverseitiges PTY-Log auf /copyKeiner feuert
7Erkannt: Claude Code wickelt in DCS-Passthrough einSequenz von tmux blockiert
8allow-passthrough onGefixt

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.

Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Weitere Artikel

OSC 52 Clipboard-Fix für xterm.js + tmux (allow-passthrough)