Modo de voz de Claude Code por SSH
Cuando tu servidor no tiene micrófono
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Tamaño
Espaciado
Fuente
- El micrófono que falta
- Por qué falla
- Por qué aun así tiene solución
- Qué más podría servir
- La configuración
- Paso 1: abrir el servidor de audio local a TCP
- Paso 2: entrar por SSH con un túnel inverso
- Paso 3: apuntar el remoto al servidor de audio
- Paso 4: encaminar ALSA a través de PulseAudio
- Paso 5: probarlo
- El cuadro completo
- Contrapartidas
- Hacerlo permanente
- Efecto colateral: cuándo aparece el aviso de RemoteForward
- Escenario 1: scripts de CI que heredan la directiva
- Solución: excluir el reenvío en los scripts
- Escenario 2: una segunda sesión interactiva al mismo host
- Solución: multiplexado de conexiones SSH
- Resumen
El micrófono que falta
El modo de voz de Claude Code necesita un micrófono. En un servidor Linux remoto accedido por SSH, no hay ninguno. Al activar el modo de voz allí, la pila de audio lanza:
cannot find card '0'El micrófono no está averiado. Los servidores no suelen traer hardware de audio. La solución: reenviar el micrófono local por SSH para que el proceso remoto lo use como si fuera propio.
Por qué falla
El error lleva el formato de ALSA: card '0' es el dispositivo de audio a nivel de kernel que la capa espera encontrar. Por cualquier ruta que tome el modo de voz para leer el micrófono, acaba en ALSA. Esta busca un dispositivo físico, no encuentra ninguno y devuelve el error anterior.
Los daemons de espacio de usuario que normalmente se sitúan entre la aplicación y el hardware, PulseAudio y PipeWire, tampoco corren en una instalación de servidor habitual. Y aunque corrieran, no tendrían nada que gestionar.
El problema, por tanto, no es una mala configuración. En la máquina no existe ninguna capa de audio.
Por qué aun así tiene solución
La suposición de que la captura de voz depende del hardware local es lo que hace que parezca irresoluble. En Linux no es así: el daemon es un servicio en espacio de usuario, y el hardware se sitúa detrás. PipeWire y PulseAudio exponen el mismo protocolo sobre un socket de red, no solo sobre el socket Unix local. Con la configuración adecuada, una aplicación en una máquina puede grabar del micrófono de otra.
Ese es todo el planteamiento: activar TCP en el daemon local, reenviar el puerto por SSH, apuntar el proceso remoto hacia él.
Qué más podría servir
El audio sobre red tiene varias modalidades en Linux. Los vecinos de este enfoque:
- Los módulos RTP de PipeWire (
module-rtp-source/module-rtp-sink). Transmiten PCM en bruto, Opus o MIDI por UDP. Se usan sobre todo para multicast en LAN (grupo por defecto224.0.0.56) y no para punto a punto, con potencial de menor latencia que TCP, sin cifrado incorporado, y ambos extremos necesitan una configuración coincidente de dirección y formato. module-tunnel-sourcede PulseAudio. El remoto carga este módulo apuntando al servidor PulseAudio de la máquina local (o a la capa de compatibilidad Pulse de PipeWire). Mismo protocolo nativo, sin túnel SSH de por medio. Ese es también el coste: sin cifrado, y la cookie de autenticación viaja en claro por la red.- Reenvío X11. Se propone a menudo en discusiones sobre audio por SSH, pero
ssh -Xno transporta audio. Los montajes que pegan audio sobre X siguen necesitando una sesión gráfica y un transporte de audio aparte:module-x11-publishde PulseAudio (que anuncia la dirección del servidor PA mediante propiedades X11), o NX/x2go (que añaden un canal de audio independiente junto a una sesión X). Nada de eso aplica en un servidor sin pantalla. - Ejecutar Claude Code en local y ya está. Siempre es una opción. El coste: renunciar a los recursos de la máquina remota, a la accesibilidad desde cualquier sitio y a una infraestructura bajo tu control.
La ruta TCP sobre SSH elegida aquí no añade nada nuevo en ninguno de los dos lados (PipeWire ya está en el portátil, OpenSSH está en ambos), y queda cifrada gracias al túnel SSH.
La configuración
Tres piezas móviles:
- El daemon de audio local escucha en un puerto TCP, además del socket Unix habitual.
- SSH lleva ese puerto a la máquina remota mediante un túnel inverso.
- La pila de audio del remoto (tanto aplicaciones que hablan PulseAudio como las que solo usan ALSA) se apunta al túnel.
Paso 1: abrir el servidor de audio local a TCP
En la máquina local, copia la configuración por defecto de PipeWire-PulseAudio a tu directorio de usuario:
cp /usr/share/pipewire/pipewire-pulse.conf ~/.config/pipewire/pipewire-pulse.confAbre ~/.config/pipewire/pipewire-pulse.conf, busca el bloque server.address y añade la dirección TCP:
server.address = [ "unix:native" "tcp:127.0.0.1:4713" # solo localhost, para reenvío de audio por SSH]Reinicia PipeWire para aplicar el cambio:
systemctl --user restart pipewire pipewire-pulsePipeWire ahora escucha en el puerto TCP 4713, pero solo en localhost. No es accesible desde la red. Al estar el cambio en el archivo de configuración, persiste entre reinicios.
Paso 2: entrar por SSH con un túnel inverso
En lugar de un simple ssh user@server, añade el flag -R:
ssh -R 4713:127.0.0.1:4713 user@your-serverEl flag -R le dice a SSH: cualquier cosa que se conecte al puerto 4713 en la máquina remota debe reenviarse al puerto 4713 de la máquina local. El tráfico de audio viaja dentro de la conexión SSH cifrada.
Paso 3: apuntar el remoto al servidor de audio
Una vez dentro del remoto, define esta variable de entorno:
export PULSE_SERVER="tcp:127.0.0.1:4713"Cualquier aplicación que use PulseAudio (o la capa de compatibilidad Pulse de PipeWire) se conectará ahora a ese puerto, que desemboca directamente en el micrófono local.
Paso 4: encaminar ALSA a través de PulseAudio
Algunas aplicaciones acceden a ALSA directamente, sin pasar por las variables de entorno de PulseAudio. El modo de voz de Claude Code es una de ellas, a juzgar por el formato original del error (cannot find card '0' es un mensaje de ALSA). Para esas aplicaciones, ALSA necesita su propia regla de enrutamiento. Crea ~/.asoundrc en la máquina remota:
pcm.!default { type pulse }ctl.!default { type pulse }Esto le indica a ALSA: cuando una aplicación pida el dispositivo de audio por defecto, que lo encamine por PulseAudio en vez de buscar hardware. Instala los paquetes necesarios:
sudo apt install libasound2-plugins pulseaudio-utils alsa-utilsalsa-utils aporta arecord y los binarios de grabación de ALSA que necesita la ruta de captura de voz. Sin el paquete, la ruta de audio puede estar plenamente operativa (túnel levantado, micrófono visible con pactl info) y el modo de voz seguirá fallando porque no existe ningún binario de grabación que lea de la fuente. El tipo de brecha que se esconde en silencio: cada capa de la pila se prueba bien por separado, pero lo que realmente lee bytes del micrófono falta. Instalar el paquete fue lo que devolvió el modo de voz a la vida en esta configuración; para quien se tope con el mismo callejón sin salida, inshallah alsa-utils cerrará la brecha. El cliente de voz de código cerrado probablemente invoca arecord o alguno de sus hermanos, pero la afirmación estricta se queda en que alsa-utils era la pieza que faltaba.
Paso 5: probarlo
Sigue en el remoto y ejecuta:
pactl infoSi todo funciona, la salida muestra la información del servidor de audio de la máquina local, incluido el micrófono local listado como fuente por defecto. Esa es la confirmación.
El cuadro completo
De extremo a extremo, la ruta de audio es:
%%{init: {"flowchart": {"useMaxWidth": false}} }%%
graph TD
A["Claude Code (remoto)"] --> B["tcp:127.0.0.1:4713 en el remoto"]
B --> C["túnel inverso SSH"]
C --> D["PipeWire local en el puerto 4713"]
D --> E["micrófono físico"] La voz va de hardware → PipeWire → socket TCP → túnel SSH → servidor remoto → Claude Code. La máquina remota nunca necesita hardware de audio propio.
Contrapartidas
Lo que cuesta:
- Latencia. El flujo del micrófono pasa por PipeWire, un socket TCP, una vuelta de cifrado SSH y vuelve. En una conexión doméstica a un servidor cercano no se nota para dictado; en un enlace lento o con un servidor lejano, sí se notará.
- Ancho de banda. El flujo es PCM sin comprimir. El modo de voz va en ráfagas cortas, así que el volumen en la práctica es pequeño, aunque un enlace compartido con móvil o con límite lo acusará.
- Alcance de confianza. El proceso remoto recibe tu micrófono en vivo mientras la sesión SSH esté abierta. Cualquier otra cosa que corra en la máquina remota, bajo el mismo usuario, puede leer de la misma fuente mientras el túnel esté arriba. Es normal para cualquier dispositivo reenviado, pero conviene tenerlo presente.
- Supuesto de una única máquina. El montaje se ata a localhost en el remoto, y eso es lo que lo hace seguro. También significa que solo los procesos de esa única máquina remota pueden usar el micrófono, no una segunda máquina alcanzada por el mismo salto.
Hacerlo permanente
El montaje anterior funciona para una sesión. Para dejarlo permanente:
En el remoto. PULSE_SERVER debería estar en ~/.zshrc (o ~/.bashrc):
echo "export PULSE_SERVER='tcp:127.0.0.1:4713'" >> ~/.zshrcPara SSH. Añade RemoteForward a ~/.ssh/config en la máquina local, así el flag -R deja de hacer falta en cada conexión:
Host your-server HostName your-server-ip User your-user RemoteForward 4713 127.0.0.1:4713Un simple ssh your-server ahora establece el túnel automáticamente.
Para el módulo TCP local. Ya quedó hecho en el Paso 1. El puerto 4713 se abre en cada inicio de sesión.
Efecto colateral: cuándo aparece el aviso de RemoteForward
Una vez que RemoteForward 4713 127.0.0.1:4713 vive en ~/.ssh/config, toda conexión SSH a ese host intenta montar el reenvío de audio. Hay dos situaciones en las que no puede, y ambas producen el mismo aviso:
Warning: remote port forwarding failed for listen port 4713La conexión sigue funcionando (el despliegue acaba, el shell abre con normalidad), pero el aviso parece alarmante. Dos escenarios distintos comparten el síntoma.
Escenario 1: scripts de CI que heredan la directiva
Woodpecker CI corre en la máquina local. Un sistema de CI ligero y autohospedado que recoge git push por webhook y ejecuta los pasos de build directamente sobre el host (sin contenedores). El paso de despliegue usa ssh y rsync para subir el sitio construido a un servidor remoto. Ambos comandos se conectan al mismo alias de host que ahora lleva RemoteForward 4713 en su configuración SSH.
Cada despliegue empezó a imprimir el aviso. El despliegue en sí funcionaba bien: rsync completaba, el symlink cambiaba, el sitio salía en directo. En un log de CI, esa clase de aviso es del tipo que hace parar e investigar, aunque no haya nada mal.
El proceso de CI corre en un entorno sin el runtime de audio del usuario local: otro usuario, sin shell interactivo, y el socket local de PipeWire no es alcanzable desde donde se ejecuta el agente de CI. El reenvío no tiene nada donde aterrizar, así que SSH se rinde e imprime el aviso.
Solución: excluir el reenvío en los scripts
SSH dispone de ClearAllForwardings, que le indica ignorar todas las directivas LocalForward y RemoteForward del archivo de configuración. Se activa por comando con -o:
ssh -o ClearAllForwardings=yes g12 "mkdir -p /some/path"Para un script de despliegue con varias llamadas SSH, defínelo una vez al principio:
SSH="ssh -o ClearAllForwardings=yes"export RSYNC_RSH="$SSH"RSYNC_RSH le dice a rsync qué comando SSH usar. Cada llamada $SSH y cada transferencia rsync del script se conecta ya sin intentar ningún reenvío de puertos. El aviso desaparece. Las sesiones interactivas ssh g12 siguen obteniendo el túnel de audio porque no usan esta sobrescritura.
Escenario 2: una segunda sesión interactiva al mismo host
El otro caso aparece cuando dos terminales (o el mismo terminal en dos momentos) hacen ssh g12. La primera sesión monta el RemoteForward y enlaza el puerto 4713 en el remoto. La segunda sesión lee la misma configuración, intenta enlazar el 4713 en el remoto, y lo encuentra ocupado.
El aviso aparece en la segunda sesión. El modo de voz, sorprendentemente, sigue funcionando en esa segunda sesión: el túnel de la primera sesión se comparte de manera transparente, porque el puerto enlazado desemboca en el mismo PipeWire local. El audio fluye. El aviso engaña: sugiere que el reenvío ha fallado para todo, cuando solo ha fallado el intento de esa sesión concreta de enlazarlo también.
La consecuencia real queda oculta: si la primera sesión llega a cerrarse, el túnel muere para las dos. La segunda sesión parece seguir conectada, pero el modo de voz deja de funcionar hasta que se abre una primera sesión nueva.
Solución: multiplexado de conexiones SSH
OpenSSH trae multiplexado de conexiones integrado, vía ControlMaster. La primera sesión abre una conexión maestra; las sesiones posteriores al mismo host la comparten en vez de abrir una nueva conexión TCP. Hay un único RemoteForward, un único túnel, sin avisos en sesiones posteriores.
Añade al bloque Host en ~/.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 activa la compartición. ControlPath es donde reside el archivo de socket de la conexión multiplexada; %r, %h, %p se expanden a usuario, host, puerto. ControlPersist 10m mantiene la conexión maestra viva diez minutos después de que se cierre la última sesión, de modo que abrir un nuevo terminal no paga de nuevo el coste del handshake SSH.
Con esto en su sitio, se pueden abrir tantos terminales ssh g12 en paralelo como se quiera: un único túnel debajo, sin avisos en sesiones posteriores, e inshallah el modo de voz quedará accesible desde cualquiera de ellos. Cerrar un terminal no le tira la alfombra a los demás. Esto aplica el patrón estándar de multiplexado de OpenSSH al escenario del túnel de audio; el mecanismo subyacente está bien documentado, aunque conviene verificarlo en la configuración propia antes de depender de él.
Una separación limpia en ambos escenarios: los scripts quedan fuera del reenvío, las sesiones interactivas comparten un único túnel.
Resumen
| Qué | Dónde | Comando |
|---|---|---|
| Módulo TCP (permanente) | Local (~/.config/pipewire/pipewire-pulse.conf) | Añadir "tcp:127.0.0.1:4713" a server.address |
| Túnel SSH (permanente) | Local (~/.ssh/config) | RemoteForward 4713 127.0.0.1:4713 bajo Host your-server |
| Multiplexado multisesión | Local (~/.ssh/config) | ControlMaster auto + ControlPath ~/.ssh/cm_%r@%h:%p + ControlPersist 10m |
| Fijar servidor de audio | Remoto (~/.zshrc) | export PULSE_SERVER='tcp:127.0.0.1:4713' |
| Configurar ALSA | Remoto (~/.asoundrc) | pcm.!default { type pulse } |
| Instalar paquetes | Remoto | sudo apt install libasound2-plugins pulseaudio-utils alsa-utils |
| Sobrescritura para CI/scripts | Scripts de despliegue | ssh -o ClearAllForwardings=yes + RSYNC_RSH |
Cinco pasos para el montaje de audio, un bloque para el multiplexado, una sobrescritura para los scripts que no deberían arrastrar el reenvío. Las piezas ya están en la caja: PipeWire, OpenSSH, ALSA. El trabajo consiste en hacer que apunten unas a otras.
Editores de IA 1 de 1
Volver a Editores de IAIngeniero aeroespacial
Emprendedor ético en público
Tú te ocupas de tu negocio
Yo me encargo del lado digital
Trabaja conmigo
- IA con honestidad
- Infraestructura privada
- Sitios web que rinden
Cuéntame sobre tu situación:
javed@javedab.com Más sobre mí