Max Nardit
← Artikel
3 mindev

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:

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:

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

shell
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.

shell
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.

01 / Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

02 / Weitere Artikel