Mode vocal de Claude Code via SSH
Quand ton serveur n'a pas de micro
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Taille
Espacement
Police
- Le micro manquant
- Pourquoi ça échoue
- Pourquoi c’est quand même résolvable
- Ce qui pourrait aussi fonctionner
- L’installation
- Étape 1 : ouvrir le serveur audio local en TCP
- Étape 2 : se connecter en SSH avec un tunnel inverse
- Étape 3 : pointer la machine distante vers le serveur audio
- Étape 4 : router ALSA via PulseAudio
- Étape 5 : tester
- Le tableau d’ensemble
- Compromis
- Rendre ça permanent
- Effet de bord : quand l’avertissement RemoteForward apparaît
- Scénario 1 : des scripts de CI qui héritent de la directive
- Solution : sortir les scripts de la redirection
- Scénario 2 : une deuxième session interactive vers le même hôte
- Solution : multiplexage des connexions SSH
- Résumé
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 :
- Les modules RTP de PipeWire (
module-rtp-source/module-rtp-sink). Diffusent du PCM brut, de l’Opus ou du MIDI sur UDP. Utilisés surtout pour du multicast LAN (groupe par défaut224.0.0.56) plutôt que du point à point, potentiel de latence plus faible que TCP, aucun chiffrement intégré, et les deux extrémités doivent avoir une configuration d’adresse et de format cohérente. module-tunnel-sourcede PulseAudio. La machine distante charge ce module en pointant sur le serveur PulseAudio de la machine locale (ou sur la couche de compatibilité Pulse de PipeWire). Même protocole natif, pas de tunnel SSH dans le circuit. C’est aussi le prix à payer : aucun chiffrement, et le cookie d’authentification voyage en clair sur le réseau.- Redirection X11. Souvent suggérée dans les discussions sur l’audio via SSH, mais
ssh -Xne transporte pas d’audio. Les montages qui greffent de l’audio sur X ont toujours besoin d’une session graphique et d’un transport audio séparé : lemodule-x11-publishde PulseAudio (qui annonce l’adresse du serveur PA via des propriétés X11), ou NX/x2go (qui ajoutent un canal audio à part aux côtés d’une session X). Rien de tout cela ne s’applique sur un serveur sans écran. - Simplement lancer Claude Code en local. Option toujours valable. Le prix : renoncer aux ressources de la machine distante, à l’accessibilité depuis n’importe où, et à une infrastructure sous ton contrôle.
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 :
- Le daemon audio local écoute sur un port TCP, en plus de sa socket Unix habituelle.
- SSH transporte ce port vers la machine distante via un tunnel inverse.
- 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 :
cp /usr/share/pipewire/pipewire-pulse.conf ~/.config/pipewire/pipewire-pulse.confOuvre ~/.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 :
systemctl --user restart pipewire pipewire-pulsePipeWire é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.
Étape 2 : se connecter en SSH avec un tunnel inverse
Au lieu d’un simple ssh user@server, ajoute le flag -R :
ssh -R 4713:127.0.0.1:4713 user@your-serverLe 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.
Étape 3 : pointer la machine distante vers le serveur audio
Une fois connecté sur la machine distante, définis cette variable d’environnement :
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 :
sudo apt install libasound2-plugins pulseaudio-utils alsa-utilsalsa-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.
Étape 5 : tester
Toujours sur la machine distante, lance :
pactl infoSi 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 :
- Latence. Le flux du micro passe par PipeWire, une socket TCP, un tour de chiffrement SSH, et retour. Sur une connexion à domicile vers un serveur proche, ce n’est pas perceptible pour la dictée ; sur un lien lent ou vers un serveur lointain, ça le sera.
- Bande passante. Le flux est du PCM non compressé. Le mode vocal envoie par rafales courtes, donc le volume reste modeste en pratique, mais un lien partagé avec un mobile ou bridé le sentira.
- Périmètre de confiance. Le processus distant reçoit ton micro en direct aussi longtemps que la session SSH reste ouverte. Tout le reste qui tourne sur la machine distante, sous le même utilisateur, peut lire la même source pendant que le tunnel est en place. C’est normal pour tout périphérique redirigé, mais bon à avoir en tête.
- Hypothèse d’une seule machine. Le montage se lie à localhost sur la machine distante, et c’est précisément ce qui le rend sûr. Ça veut dire aussi que seuls les processus sur cette unique machine distante peuvent utiliser le micro, et non une seconde machine atteinte via le même saut.
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) :
echo "export PULSE_SERVER='tcp:127.0.0.1:4713'" >> ~/.zshrcPour 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:4713Un 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 4713La 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 :
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 :
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.
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 10mControlMaster 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.
Une séparation nette entre les deux scénarios : les scripts sortent de la redirection, les sessions interactives partagent un seul tunnel.
Résumé
| Quoi | Où | Commande |
|---|---|---|
| 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-session | Local (~/.ssh/config) | ControlMaster auto + ControlPath ~/.ssh/cm_%r@%h:%p + ControlPersist 10m |
| Définir le serveur audio | Distant (~/.zshrc) | export PULSE_SERVER='tcp:127.0.0.1:4713' |
| Configurer ALSA | Distant (~/.asoundrc) | pcm.!default { type pulse } |
| Installer les paquets | Distant | sudo apt install libasound2-plugins pulseaudio-utils alsa-utils |
| Surcharge CI/scripts | Scripts de déploiement | ssh -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.
Éditeurs IA 1 sur 1
Retour à Éditeurs IAIngé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