Claude Code Sprachmodus über SSH

Wenn der Server kein Mikrofon hat

08.04.2026 | 20 Shawwal 1447
10 min read

بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ

Das fehlende Mikrofon

Der Sprachmodus von Claude Code braucht ein Mikrofon. Auf einem per SSH erreichten Remote-Linux-Server ist keins vorhanden. Schaltet man den Sprachmodus dort ein, wirft der Audio-Stack:

cannot find card '0'

Das Mikrofon ist nicht defekt. Server werden üblicherweise ohne Audio-Hardware ausgeliefert. Die Lösung: das lokale Mikrofon per SSH weiterleiten, damit der Remote-Prozess es wie ein eigenes Gerät nutzt.

Warum es fehlschlägt

Der Fehler trägt das Format von ALSA: card '0' ist das Audio-Gerät auf Kernel-Ebene, das die Schicht erwartet. Egal auf welchem Weg der Sprachmodus das Mikrofon auslesen will, er landet bei ALSA. Dort wird nach einem physischen Gerät gesucht, keins gefunden, und der obige Fehler zurückgegeben.

Die Userspace-Daemons, die normalerweise zwischen App und Hardware sitzen, PulseAudio und PipeWire, laufen auf einem typischen Server-Setup ebenfalls nicht. Selbst wenn sie liefen, hätten sie nichts zu verwalten.

Das Problem ist also keine Fehlkonfiguration. Auf der Maschine existiert schlicht keine Audio-Ebene.

Warum es trotzdem lösbar ist

Die Annahme, dass Sprachaufnahme hardware-lokal ist, lässt das Ganze unlösbar wirken. Unter Linux trifft sie nicht zu: Der Daemon ist ein Userspace-Dienst, und die Hardware sitzt dahinter. PipeWire und PulseAudio stellen dasselbe Protokoll über einen Netzwerk-Socket bereit, nicht nur über den lokalen Unix-Socket. Mit der richtigen Konfiguration kann eine App auf einem Rechner das Mikrofon eines anderen aufnehmen.

Das ist der ganze Ansatz: TCP beim lokalen Daemon aktivieren, den Port per SSH weiterleiten, den Remote-Prozess darauf richten.

Was sonst noch infrage käme

Audio über ein Netzwerk hat unter Linux mehrere Varianten. Die Nachbarn dieses Ansatzes:

Der hier gewählte Weg, TCP über SSH, fügt auf beiden Seiten nichts Neues hinzu (PipeWire ist auf dem Laptop bereits vorhanden, OpenSSH auf beiden), und er ist durch den SSH-Tunnel verschlüsselt.

Das Setup

Drei bewegliche Teile:

  1. Der lokale Audio-Daemon lauscht auf einem TCP-Port, zusätzlich zum üblichen Unix-Socket.
  2. SSH trägt diesen Port über einen Reverse-Tunnel zum Remote-Rechner.
  3. Der Audio-Stack auf der Remote-Seite (sowohl PulseAudio-fähige Apps als auch reine ALSA-Apps) wird auf den Tunnel gerichtet.

Schritt 1: den lokalen Audio-Server für TCP öffnen

Auf dem lokalen Rechner die Standard-PipeWire-PulseAudio-Konfiguration ins eigene Benutzerverzeichnis kopieren:

Terminal window
cp /usr/share/pipewire/pipewire-pulse.conf ~/.config/pipewire/pipewire-pulse.conf

~/.config/pipewire/pipewire-pulse.conf öffnen, den server.address-Block suchen und die TCP-Adresse ergänzen:

server.address = [
"unix:native"
"tcp:127.0.0.1:4713" # nur localhost, für SSH-Audio-Weiterleitung
]

PipeWire neu starten, damit die Änderung greift:

Terminal window
systemctl --user restart pipewire pipewire-pulse

PipeWire lauscht nun auf TCP-Port 4713, aber nur auf localhost. Aus dem Netzwerk ist er nicht erreichbar. Weil die Änderung in der Konfigurationsdatei steht, überlebt sie Neustarts.

Externer Link docs.pipewire.org/page_module_protocol_pulse.html

Schritt 2: per SSH mit Reverse-Tunnel einloggen

Statt eines einfachen ssh user@server kommt das -R-Flag hinzu:

Terminal window
ssh -R 4713:127.0.0.1:4713 user@your-server

Das -R-Flag sagt SSH: alles, was sich auf dem Remote-Rechner mit Port 4713 verbindet, wird zu Port 4713 auf dem lokalen Rechner weitergeleitet. Der Audio-Verkehr läuft innerhalb der verschlüsselten SSH-Verbindung.

Externer Link man.openbsd.org/ssh_config.5

Schritt 3: den Remote auf den Audio-Server ausrichten

Einmal auf dem Remote eingeloggt, diese Umgebungsvariable setzen:

Terminal window
export PULSE_SERVER="tcp:127.0.0.1:4713"

Jede App, die PulseAudio (oder PipeWires Pulse-Kompatibilitätsschicht) nutzt, verbindet sich nun mit diesem Port, und der tunnelt direkt zurück zum lokalen Mikrofon.

Schritt 4: ALSA über PulseAudio leiten

Einige Apps greifen direkt auf ALSA zu, statt über die PulseAudio-Umgebungsvariablen zu gehen. Der Sprachmodus von Claude Code ist eine davon, zu urteilen nach dem ursprünglichen Fehlerformat (cannot find card '0' ist eine ALSA-Meldung). Für solche Apps braucht ALSA eine eigene Routing-Regel. ~/.asoundrc auf dem Remote-Rechner anlegen:

pcm.!default { type pulse }
ctl.!default { type pulse }

Das sagt ALSA: wenn eine App nach dem Standard-Audiogerät fragt, leite sie über PulseAudio, statt nach Hardware zu suchen. Die benötigten Pakete installieren:

Terminal window
sudo apt install libasound2-plugins pulseaudio-utils alsa-utils

alsa-utils stellt arecord und die aufnahmeseitigen ALSA-Binaries bereit, die der Sprachaufnahme-Pfad braucht. Ohne dieses Paket kann der Audio-Pfad vollständig funktionieren (Tunnel steht, Mikrofon ist mit pactl info sichtbar) und der Sprachmodus scheitert trotzdem, weil kein Aufnahme-Binary existiert, das von der Quelle liest. Eine Lücke, die sich still versteckt: jede Ebene des Stacks testet einzeln sauber, nur das, was tatsächlich Bytes aus dem Mikrofon liest, fehlt. Die Installation des Pakets hat in diesem Setup den Sprachmodus wiederhergestellt; wer in derselben Sackgasse steckt, wird inshallah mit alsa-utils das Loch schließen. Der Closed-Source-Sprachclient ruft wahrscheinlich arecord oder eines seiner Geschwister auf, aber der strikte Befund bleibt: alsa-utils war das fehlende Stück.

Externer Link www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/PerfectSetup

Schritt 5: testen

Weiterhin auf dem Remote ausführen:

Terminal window
pactl info

Wenn alles funktioniert, zeigt die Ausgabe die Audio-Server-Informationen des lokalen Rechners, einschließlich des lokalen Mikrofons als Standardquelle. Das ist die Bestätigung.

Das Gesamtbild

Von Anfang bis Ende sieht der Audio-Pfad so aus:

%%{init: {"flowchart": {"useMaxWidth": false}} }%%
graph TD
    A["Claude Code (Remote)"] --> B["tcp:127.0.0.1:4713 auf dem Remote"]
    B --> C["SSH-Reverse-Tunnel"]
    C --> D["lokales PipeWire auf Port 4713"]
    D --> E["physisches Mikrofon"]

Die Stimme geht von Hardware → PipeWire → TCP-Socket → SSH-Tunnel → Remote-Server → Claude Code. Der Remote-Rechner braucht nie eigene Audio-Hardware.

Kosten

Was das kostet:

Dauerhaft einrichten

Das bisherige Setup funktioniert für eine Sitzung. Um es dauerhaft zu machen:

Auf dem Remote. PULSE_SERVER gehört in ~/.zshrc (oder ~/.bashrc):

Terminal window
echo "export PULSE_SERVER='tcp:127.0.0.1:4713'" >> ~/.zshrc

Für SSH. RemoteForward in ~/.ssh/config auf dem lokalen Rechner eintragen, damit das -R-Flag nicht jedes Mal nötig ist:

Host your-server
HostName your-server-ip
User your-user
RemoteForward 4713 127.0.0.1:4713

Ein einfaches ssh your-server richtet den Tunnel nun automatisch ein.

Für das lokale TCP-Modul. In Schritt 1 bereits erledigt. Port 4713 öffnet sich bei jedem Login.

Nebeneffekt: wenn die RemoteForward-Warnung auftaucht

Sobald RemoteForward 4713 127.0.0.1:4713 in ~/.ssh/config steht, versucht jede SSH-Verbindung zu diesem Host, die Audio-Weiterleitung aufzubauen. Es gibt zwei Situationen, in denen das nicht geht, und beide erzeugen dieselbe Warnung:

Warning: remote port forwarding failed for listen port 4713

Die Verbindung kommt trotzdem zustande (das Deploy läuft durch, die Shell öffnet sich normal), aber die Warnung sieht alarmierend aus. Zwei unterschiedliche Szenarien teilen das Symptom.

Szenario 1: CI-Skripte erben die Direktive

Woodpecker CI läuft auf dem lokalen Rechner. Ein leichtgewichtiges, selbst gehostetes CI-System, das Git-Pushes per Webhook entgegennimmt und die Build-Schritte direkt auf dem Host ausführt (keine Container). Der Deploy-Schritt nutzt ssh und rsync, um die gebaute Seite auf einen Remote-Server zu schicken. Beide Befehle verbinden sich mit demselben Host-Alias, der jetzt RemoteForward 4713 in seiner SSH-Konfiguration hat.

Jedes Deploy begann, die Warnung auszugeben. Das Deploy selbst lief sauber: rsync beendete sich, der Symlink wurde umgelegt, die Seite ging live. In einem CI-Log ist diese Art von Warnung eine, die einen innehalten und nachsehen lässt, selbst wenn nichts falsch ist.

Der CI-Prozess läuft in einer Umgebung ohne das Audio-Runtime des lokalen Benutzers: anderer Benutzer, keine interaktive Shell, und der lokale PipeWire-Socket ist von dort, wo der CI-Agent läuft, nicht erreichbar. Die Weiterleitung hat nichts, worauf sie landen kann, also gibt SSH auf und gibt die Warnung aus.

Lösung: Weiterleitung für Skripte deaktivieren

SSH kennt ClearAllForwardings, was es anweist, alle LocalForward- und RemoteForward-Direktiven aus der Konfigurationsdatei zu ignorieren. Pro Befehl setzen mit -o:

Terminal window
ssh -o ClearAllForwardings=yes g12 "mkdir -p /some/path"

Für ein Deploy-Skript mit mehreren SSH-Aufrufen einmal oben definieren:

Terminal window
SSH="ssh -o ClearAllForwardings=yes"
export RSYNC_RSH="$SSH"

RSYNC_RSH sagt rsync, welchen SSH-Befehl es nutzen soll. Jeder $SSH-Aufruf und jeder rsync-Transfer im Skript verbindet sich nun, ohne irgendeine Port-Weiterleitung zu versuchen. Die Warnung verschwindet. Interaktive ssh g12-Sitzungen bekommen weiterhin den Audio-Tunnel, weil sie diesen Override nicht nutzen.

Externer Link man.openbsd.org/ssh_config.5

Szenario 2: eine zweite interaktive Sitzung zum selben Host

Der andere Fall zeigt sich, wenn zwei Terminals (oder dasselbe Terminal zu zwei Zeiten) beide ssh g12 aufrufen. Die erste Sitzung baut den RemoteForward auf und bindet Port 4713 auf dem Remote. Die zweite Sitzung liest dieselbe Konfiguration, versucht, 4713 auf dem Remote zu binden, und findet ihn belegt.

Die Warnung erscheint bei der zweiten Sitzung. Der Sprachmodus funktioniert dort trotzdem, überraschenderweise: der Tunnel der ersten Sitzung wird transparent geteilt, weil der gebundene Port zurück zum selben lokalen PipeWire routet. Das Audio fließt. Die Warnung führt in die Irre: sie suggeriert, dass die Weiterleitung für alles fehlgeschlagen ist, obwohl nur der Versuch dieser spezifischen Sitzung, sie ebenfalls zu binden, scheiterte.

Die eigentliche Konsequenz bleibt verborgen: wenn die erste Sitzung jemals geschlossen wird, stirbt der Tunnel für beide. Die zweite Sitzung sieht aus, als wäre sie noch verbunden, aber der Sprachmodus funktioniert nicht mehr, bis eine frische erste Sitzung geöffnet wird.

Lösung: SSH-Verbindungs-Multiplexing

OpenSSH bringt Verbindungs-Multiplexing über ControlMaster bereits mit. Die erste Sitzung öffnet eine Master-Verbindung; spätere Sitzungen zum selben Host teilen sie sich, statt eine neue TCP-Verbindung aufzubauen. Es gibt nur einen RemoteForward, nur einen Tunnel, keine Warnungen bei späteren Sitzungen.

Im Host-Block in ~/.ssh/config ergänzen:

Host your-server
HostName your-server-ip
User your-user
RemoteForward 4713 127.0.0.1:4713
ControlMaster auto
ControlPath ~/.ssh/cm_%r@%h:%p
ControlPersist 10m

ControlMaster auto aktiviert das Teilen. ControlPath bestimmt, wo die Socket-Datei der gemultiplexten Verbindung liegt; %r, %h, %p werden zu Benutzer, Host, Port expandiert. ControlPersist 10m hält die Master-Verbindung zehn Minuten nach dem Schließen der letzten Sitzung am Leben, sodass beim Öffnen eines neuen Terminals nicht wieder der SSH-Handshake-Aufwand anfällt.

Damit lassen sich beliebig viele ssh g12-Terminals parallel öffnen: ein einziger Tunnel unter der Haube, keine Warnungen bei späteren Sitzungen, und der Sprachmodus wird aus jedem von ihnen inshallah erreichbar sein. Das Schließen eines Terminals zieht den anderen nicht den Boden unter den Füßen weg. Das ist das Standard-Multiplexing-Muster von OpenSSH, angewendet auf das Audio-Tunnel-Szenario; der zugrundeliegende Mechanismus ist gut dokumentiert, einen Test im eigenen Setup aber wert, bevor man sich darauf verlässt.

Externer Link man.openbsd.org/ssh_config.5

Eine saubere Trennung über beide Szenarien hinweg: Skripte steigen aus der Weiterleitung aus, interaktive Sitzungen teilen sich einen Tunnel.

Zusammenfassung

WasWoBefehl
TCP-Modul (dauerhaft)Lokal (~/.config/pipewire/pipewire-pulse.conf)"tcp:127.0.0.1:4713" zu server.address hinzufügen
SSH-Tunnel (dauerhaft)Lokal (~/.ssh/config)RemoteForward 4713 127.0.0.1:4713 unter Host your-server
Mehrsitzungs-MultiplexingLokal (~/.ssh/config)ControlMaster auto + ControlPath ~/.ssh/cm_%r@%h:%p + ControlPersist 10m
Audio-Server setzenRemote (~/.zshrc)export PULSE_SERVER='tcp:127.0.0.1:4713'
ALSA konfigurierenRemote (~/.asoundrc)pcm.!default { type pulse }
Pakete installierenRemotesudo apt install libasound2-plugins pulseaudio-utils alsa-utils
CI-/Skript-OverrideDeploy-Skriptessh -o ClearAllForwardings=yes + RSYNC_RSH

Fünf Schritte für das Audio-Setup, ein Block für Multiplexing, ein Override für Skripte, die die Weiterleitung gar nicht tragen sollten. Die Teile sind alle schon an Bord: PipeWire, OpenSSH, ALSA. Die Arbeit liegt darin, sie aufeinander auszurichten.

Luft- und Raumfahrtingenieur

Ethischer Unternehmer in der Öffentlichkeit

Sie kümmern sich um Ihr Geschäft

Ich kümmere mich um die digitale Seite

Zusammenarbeiten
  • KI mit Ehrlichkeit
  • Private Infrastruktur
  • Websites die performen

Erzählen Sie mir von Ihrer Situation:

javed@javedab.com Mehr über mich