Modo de voz de Claude Code por SSH

Cuando tu servidor no tiene micrófono

08.04.2026 | 20 Shawwal 1447
11 min read

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

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:

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:

  1. El daemon de audio local escucha en un puerto TCP, además del socket Unix habitual.
  2. SSH lleva ese puerto a la máquina remota mediante un túnel inverso.
  3. 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:

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

Abre ~/.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:

Terminal window
systemctl --user restart pipewire pipewire-pulse

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

Enlace Externo docs.pipewire.org/page_module_protocol_pulse.html

Paso 2: entrar por SSH con un túnel inverso

En lugar de un simple ssh user@server, añade el flag -R:

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

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

Enlace Externo man.openbsd.org/ssh_config.5

Paso 3: apuntar el remoto al servidor de audio

Una vez dentro del remoto, define esta variable de entorno:

Terminal window
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:

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

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

Enlace Externo www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/PerfectSetup

Paso 5: probarlo

Sigue en el remoto y ejecuta:

Terminal window
pactl info

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

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

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

Para 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:4713

Un 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 4713

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

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

Para un script de despliegue con varias llamadas SSH, defínelo una vez al principio:

Terminal window
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.

Enlace Externo man.openbsd.org/ssh_config.5

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 10m

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

Enlace Externo man.openbsd.org/ssh_config.5

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óndeComando
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ónLocal (~/.ssh/config)ControlMaster auto + ControlPath ~/.ssh/cm_%r@%h:%p + ControlPersist 10m
Fijar servidor de audioRemoto (~/.zshrc)export PULSE_SERVER='tcp:127.0.0.1:4713'
Configurar ALSARemoto (~/.asoundrc)pcm.!default { type pulse }
Instalar paquetesRemotosudo apt install libasound2-plugins pulseaudio-utils alsa-utils
Sobrescritura para CI/scriptsScripts de desplieguessh -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.

Ingeniero 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í