Mode vocal de Claude Code via SSH

Quand ton serveur n'a pas de micro

08.04.2026 | 20 Shawwal 1447
12 min read

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

Le micro manquant

Le mode vocal de Claude Code a besoin d’un micro. Sur un serveur Linux distant accédé via SSH, il n’y en a aucun. On active le mode vocal là-bas, et la pile audio renvoie :

cannot find card '0'

Le micro n’est pas en panne. Les serveurs sortent en général sans matériel audio. La solution : rediriger le micro local via SSH pour que le processus distant s’en serve comme s’il lui appartenait.

Pourquoi ça échoue

L’erreur porte le format d’ALSA : card '0' est le périphérique audio de niveau noyau que la couche s’attend à trouver. Quel que soit le chemin par lequel le mode vocal lit le micro, il aboutit dans ALSA. Celle-ci cherche un périphérique physique, n’en trouve aucun et renvoie l’erreur ci-dessus.

Les daemons en espace utilisateur qui s’intercalent normalement entre l’application et le matériel, PulseAudio et PipeWire, ne tournent pas non plus sur une installation serveur type. Et même s’ils tournaient, ils n’auraient rien à gérer.

Le problème n’est donc pas une mauvaise configuration. Il n’y a simplement aucune couche audio sur la machine.

Pourquoi c’est quand même résolvable

L’hypothèse selon laquelle la capture vocale est liée au matériel local est ce qui rend la chose insoluble en apparence. Sous Linux, elle ne tient pas : le daemon est un service en espace utilisateur, et le matériel se range derrière lui. PipeWire et PulseAudio exposent le même protocole via une socket réseau, et pas seulement via la socket Unix locale. Avec la bonne configuration, une application sur une machine peut enregistrer depuis le micro d’une autre.

C’est toute l’approche : activer TCP sur le daemon local, rediriger le port via SSH, orienter le processus distant vers lui.

Ce qui pourrait aussi fonctionner

L’audio sur réseau a plusieurs variantes sous Linux. Les voisins de cette approche :

La route TCP via SSH choisie ici n’ajoute rien de neuf de part et d’autre (PipeWire est déjà sur l’ordinateur portable, OpenSSH est sur les deux), et elle est chiffrée par le tunnel SSH.

L’installation

Trois pièces mobiles :

  1. Le daemon audio local écoute sur un port TCP, en plus de sa socket Unix habituelle.
  2. SSH transporte ce port vers la machine distante via un tunnel inverse.
  3. La pile audio côté distant (les applications qui parlent PulseAudio comme celles qui n’utilisent qu’ALSA) est orientée vers le tunnel.

Étape 1 : ouvrir le serveur audio local en TCP

Sur la machine locale, copier la configuration par défaut de PipeWire-PulseAudio dans ton répertoire utilisateur :

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

Ouvre ~/.config/pipewire/pipewire-pulse.conf, repère le bloc server.address et ajoute l’adresse TCP :

server.address = [
"unix:native"
"tcp:127.0.0.1:4713" # localhost uniquement, pour la redirection audio par SSH
]

Redémarre PipeWire pour appliquer :

Terminal window
systemctl --user restart pipewire pipewire-pulse

PipeWire écoute maintenant sur le port TCP 4713, mais uniquement sur localhost. Il n’est pas accessible depuis le réseau. Le changement est dans le fichier de configuration, donc il survit aux redémarrages.

Lien Externe docs.pipewire.org/page_module_protocol_pulse.html

Étape 2 : se connecter en SSH avec un tunnel inverse

Au lieu d’un simple ssh user@server, ajoute le flag -R :

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

Le flag -R dit à SSH : tout ce qui se connecte au port 4713 sur la machine distante doit être redirigé vers le port 4713 sur la machine locale. Le trafic audio voyage à l’intérieur de la connexion SSH chiffrée.

Lien Externe man.openbsd.org/ssh_config.5

Étape 3 : pointer la machine distante vers le serveur audio

Une fois connecté sur la machine distante, définis cette variable d’environnement :

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

Toute application qui utilise PulseAudio (ou la couche de compatibilité Pulse de PipeWire) se connectera désormais à ce port, qui redescend directement vers le micro local.

Étape 4 : router ALSA via PulseAudio

Certaines applications attaquent ALSA directement, sans passer par les variables d’environnement de PulseAudio. Le mode vocal de Claude Code en fait partie, au vu du format d’erreur d’origine (cannot find card '0' est un message d’ALSA). Pour ces applications, ALSA a besoin de sa propre règle de routage. Crée ~/.asoundrc sur la machine distante :

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

Cela indique à ALSA : quand une application demande le périphérique audio par défaut, route-la via PulseAudio plutôt que de chercher du matériel. Installe les paquets nécessaires :

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

alsa-utils fournit arecord et les binaires d’enregistrement ALSA dont le chemin de capture vocale a besoin. Sans ce paquet, le chemin audio peut être entièrement fonctionnel (tunnel monté, micro visible via pactl info) et le mode vocal échouera quand même, parce qu’il n’existe aucun binaire d’enregistrement pour lire la source. Le genre de brèche qui se cache en silence : chaque couche de la pile se teste proprement isolément, mais ce qui lit vraiment les octets du micro manque. Installer le paquet est ce qui a ramené le mode vocal à la vie dans cette configuration ; pour qui se retrouve dans la même impasse, alsa-utils va inshallah combler la brèche. Le client vocal propriétaire appelle probablement arecord ou un de ses frères, mais l’affirmation stricte se limite à ceci : alsa-utils était la pièce manquante.

Lien Externe www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/PerfectSetup

Étape 5 : tester

Toujours sur la machine distante, lance :

Terminal window
pactl info

Si tout fonctionne, la sortie affiche les informations du serveur audio de la machine locale, y compris le micro local listé comme source par défaut. C’est la confirmation.

Le tableau d’ensemble

De bout en bout, le chemin audio ressemble à ceci :

%%{init: {"flowchart": {"useMaxWidth": false}} }%%
graph TD
    A["Claude Code (distant)"] --> B["tcp:127.0.0.1:4713 sur le distant"]
    B --> C["tunnel inverse SSH"]
    C --> D["PipeWire local sur le port 4713"]
    D --> E["micro physique"]

La voix va de matériel → PipeWire → socket TCP → tunnel SSH → serveur distant → Claude Code. La machine distante n’a jamais besoin de son propre matériel audio.

Compromis

Ce que ça coûte :

Rendre ça permanent

Le montage ci-dessus fonctionne pour une session. Pour le rendre permanent :

Sur la machine distante. PULSE_SERVER devrait figurer dans ~/.zshrc (ou ~/.bashrc) :

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

Pour SSH. Ajoute RemoteForward à ~/.ssh/config sur la machine locale, pour ne plus avoir à taper le flag -R à chaque fois :

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

Un simple ssh your-server met désormais le tunnel en place automatiquement.

Pour le module TCP local. Déjà traité à l’étape 1. Le port 4713 s’ouvre à chaque ouverture de session.

Effet de bord : quand l’avertissement RemoteForward apparaît

Une fois que RemoteForward 4713 127.0.0.1:4713 vit dans ~/.ssh/config, chaque connexion SSH à cet hôte tente d’établir la redirection audio. Il y a deux situations où elle n’y arrive pas, et les deux produisent le même avertissement :

Warning: remote port forwarding failed for listen port 4713

La connexion passe quand même (le déploiement se termine, le shell s’ouvre normalement), mais l’avertissement a l’air alarmant. Deux scénarios distincts partagent le symptôme.

Scénario 1 : des scripts de CI qui héritent de la directive

Woodpecker CI tourne sur la machine locale. Un système de CI léger, auto-hébergé, qui récupère les git push via webhook et exécute les étapes de build directement sur l’hôte (sans conteneurs). L’étape de déploiement utilise ssh et rsync pour envoyer le site construit vers un serveur distant. Les deux commandes se connectent au même alias d’hôte qui porte maintenant RemoteForward 4713 dans sa configuration SSH.

Chaque déploiement s’est mis à afficher l’avertissement. Le déploiement en lui-même se passait bien : rsync terminait, le lien symbolique basculait, le site partait en ligne. Dans un log de CI, ce genre d’avertissement est du type qui fait s’arrêter et aller voir, même quand il n’y a rien qui cloche.

Le processus de CI tourne dans un environnement sans le runtime audio de l’utilisateur local : autre utilisateur, pas de shell interactif, socket PipeWire local non accessible depuis l’endroit où tourne l’agent de CI. La redirection n’a nulle part où atterrir, alors SSH abandonne et affiche l’avertissement.

Solution : sortir les scripts de la redirection

SSH propose ClearAllForwardings, qui lui dit d’ignorer toutes les directives LocalForward et RemoteForward du fichier de configuration. Se règle par commande avec -o :

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

Pour un script de déploiement qui enchaîne plusieurs appels SSH, définis-le une fois en haut :

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

RSYNC_RSH indique à rsync quelle commande SSH utiliser. Chaque appel $SSH et chaque transfert rsync du script se connecte désormais sans tenter la moindre redirection de port. L’avertissement disparaît. Les sessions interactives ssh g12 continuent d’obtenir le tunnel audio, parce qu’elles n’utilisent pas cette surcharge.

Lien Externe man.openbsd.org/ssh_config.5

Scénario 2 : une deuxième session interactive vers le même hôte

L’autre cas se présente quand deux terminaux (ou le même terminal à deux moments) font tous les deux ssh g12. La première session monte le RemoteForward et occupe le port 4713 sur la machine distante. La deuxième session lit la même configuration, tente d’occuper le 4713 sur la machine distante, et le trouve déjà pris.

L’avertissement apparaît sur la deuxième session. Le mode vocal continue curieusement de fonctionner dans cette deuxième session : le tunnel de la première session est partagé de manière transparente, parce que le port occupé redescend vers le même PipeWire local. L’audio passe. L’avertissement induit en erreur : il laisse entendre que la redirection a échoué pour tout, alors que seule la tentative de cette session précise de l’occuper aussi a échoué.

La vraie conséquence reste cachée : si la première session vient à se fermer, le tunnel meurt pour les deux. La deuxième session a l’air encore connectée, mais le mode vocal cesse de fonctionner jusqu’à ce qu’une nouvelle première session soit ouverte.

Solution : multiplexage des connexions SSH

OpenSSH intègre en natif le multiplexage de connexions via ControlMaster. La première session ouvre une connexion maîtresse ; les sessions suivantes vers le même hôte la partagent au lieu d’ouvrir une nouvelle connexion TCP. Il n’y a qu’un seul RemoteForward, qu’un seul tunnel, aucun avertissement sur les sessions suivantes.

Ajoute au bloc Host dans ~/.ssh/config :

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 active le partage. ControlPath désigne l’endroit où vit le fichier de socket de la connexion multiplexée ; %r, %h, %p se développent en utilisateur, hôte, port. ControlPersist 10m garde la connexion maîtresse en vie dix minutes après la fermeture de la dernière session, si bien qu’ouvrir un nouveau terminal ne repaye plus le coût du handshake SSH.

Avec ça en place, on peut ouvrir autant de terminaux ssh g12 en parallèle qu’on veut : un seul tunnel sous le capot, aucun avertissement sur les sessions suivantes, et le mode vocal va inshallah être accessible depuis n’importe laquelle d’entre elles. Fermer un terminal ne tire pas le tapis sous les autres. C’est le motif standard de multiplexage d’OpenSSH appliqué au scénario du tunnel audio ; le mécanisme sous-jacent est bien documenté, mais mérite d’être vérifié dans ta propre installation avant de s’y fier.

Lien Externe man.openbsd.org/ssh_config.5

Une séparation nette entre les deux scénarios : les scripts sortent de la redirection, les sessions interactives partagent un seul tunnel.

Résumé

QuoiCommande
Module TCP (permanent)Local (~/.config/pipewire/pipewire-pulse.conf)Ajouter "tcp:127.0.0.1:4713" à server.address
Tunnel SSH (permanent)Local (~/.ssh/config)RemoteForward 4713 127.0.0.1:4713 sous Host your-server
Multiplexage multi-sessionLocal (~/.ssh/config)ControlMaster auto + ControlPath ~/.ssh/cm_%r@%h:%p + ControlPersist 10m
Définir le serveur audioDistant (~/.zshrc)export PULSE_SERVER='tcp:127.0.0.1:4713'
Configurer ALSADistant (~/.asoundrc)pcm.!default { type pulse }
Installer les paquetsDistantsudo apt install libasound2-plugins pulseaudio-utils alsa-utils
Surcharge CI/scriptsScripts de déploiementssh -o ClearAllForwardings=yes + RSYNC_RSH

Cinq étapes pour le montage audio, un bloc pour le multiplexage, une surcharge pour les scripts qui ne devraient pas traîner la redirection. Les pièces sont déjà dans la boîte : PipeWire, OpenSSH, ALSA. Le travail, c’est de les faire se pointer les unes sur les autres.

Ingénieur aérospatial

Entrepreneur éthique en public

Vous gérez votre activité

Je m'occupe du côté numérique

Travailler avec moi
  • IA avec honnêteté
  • Infrastructure privée
  • Sites web performants

Parlez-moi de votre situation :

javed@javedab.com En savoir plus