通过 SSH 使用 Claude Code 语音模式

当服务器没有麦克风的时候

08.04.2026 | 20 Shawwal 1447
17 min read

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

缺失的麦克风

Claude Code 的语音模式需要一个麦克风。在通过 SSH 接入的远程 Linux 服务器上,这样的硬件并不存在。在那里打开语音模式,音频栈抛出:

cannot find card '0'

麦克风并没有坏。服务器出厂通常不带音频硬件。解决办法:通过 SSH 把本地麦克风转发过去,让远程进程像使用自己的设备一样使用它。

为什么会失败

这条错误采用的是 ALSA 的格式:card '0' 是这一层期望找到的内核级音频设备。不管语音模式走哪条路径去读麦克风,最终都会落到 ALSA。ALSA 扫描物理设备,找不到任何一个,然后返回上面那条错误。

通常夹在应用和硬件之间的用户态守护进程,PulseAudio 和 PipeWire,在一台典型的服务器安装里同样不在运行。哪怕它们真的在跑,也没有什么可以管理的。

问题并不是配置错了。这台机器上根本就没有音频这一层。

为什么它仍然有解

让它看起来无解的,是这样一个假设:语音采集必须发生在本地硬件上。在 Linux 上这不成立:守护进程是一个用户态服务,硬件只是藏在它后面。PipeWire 和 PulseAudio 都通过网络套接字对外提供同一套线协议,而不仅仅是本地 Unix 套接字。只要配置得当,一台机器上的应用可以从另一台机器的麦克风录音。

整套思路就是这样:在本地守护进程上打开 TCP,把端口通过 SSH 转发过去,让远程进程指向它。

还有哪些选项

在 Linux 上,把音频跑在网络上有几种做法。这条路线的邻居:

这里选的 TCP over SSH 路径在两端都不引入新东西(PipeWire 已经在笔记本上,OpenSSH 两端都有),并且因为走 SSH 隧道而天然加密。

整套搭法

三个活动部件:

  1. 本地音频守护进程在一个 TCP 端口上监听,外加它平常的 Unix 套接字。
  2. SSH 通过一条反向隧道把这个端口带到远程机器。
  3. 远程端的音频栈(既包括懂 PulseAudio 的应用,也包括只用 ALSA 的那些)都被指向这条隧道。

步骤 1:让本地音频服务器接受 TCP

本地机器上,把 PipeWire 的默认 PulseAudio 配置复制到用户目录:

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

打开 ~/.config/pipewire/pipewire-pulse.conf,找到 server.address 块,把 TCP 地址加进去:

server.address = [
"unix:native"
"tcp:127.0.0.1:4713" # 仅 localhost,用于 SSH 音频转发
]

重启 PipeWire 让改动生效:

Terminal window
systemctl --user restart pipewire pipewire-pulse

PipeWire 现在在 TCP 端口 4713 上监听,但只限 localhost。从网络上无法到达它。因为改动写在配置文件里,所以重启之后依然有效。

外部链接 docs.pipewire.org/page_module_protocol_pulse.html

步骤 2:通过 SSH 加反向隧道登入

不用简单的 ssh user@server,而是加上 -R 参数:

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

-R 参数告诉 SSH:任何连到远程机器 4713 端口的东西,都应当被转发到本地机器的 4713 端口。音频流量走在加密的 SSH 连接里。

外部链接 man.openbsd.org/ssh_config.5

步骤 3:把远程端指向音频服务器

在远程端登入之后,设置这个环境变量:

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

任何使用 PulseAudio(或 PipeWire 的 Pulse 兼容层)的应用,现在都会连到这个端口,而这个端口直通回本地麦克风。

步骤 4:让 ALSA 走 PulseAudio

有些应用直接去找 ALSA,而不是走 PulseAudio 的环境变量。从最初那条错误信息的格式看,Claude Code 的语音模式就是其中之一(cannot find card '0' 是一条 ALSA 消息)。对这类应用,ALSA 需要它自己的一条路由规则。在远程机器上创建 ~/.asoundrc

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

这是在告诉 ALSA:有应用要默认的音频设备时,就把它走 PulseAudio,而不是去找硬件。安装所需的软件包:

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

alsa-utils 提供 arecord 以及语音采集路径需要的录音端 ALSA 二进制。没有这个包,音频路径可以完全跑得通(隧道已起,pactl info 里能看到麦克风),语音模式仍然会失败,因为没有一个录音二进制来从源头读取数据。那种悄悄藏起来的缺口:栈里每一层单独测试都没问题,但真正从麦克风读字节的那一块缺席了。在这套环境里,正是安装这个软件包把语音模式带回了生活;碰到同样死胡同的人,alsa-utils 会因沙安拉填上这个缺口。那个闭源语音客户端大概调用的是 arecord 或它的某个兄弟,但严格来讲只能说:alsa-utils 就是当时缺的那块拼图。

外部链接 www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/PerfectSetup

步骤 5:测试

还在远程端,执行:

Terminal window
pactl info

如果一切正常,输出里显示的是本地机器的音频服务器信息,包括作为默认源列出的本地麦克风。这就是确认。

全貌

从头到尾,音频路径是这样:

%%{init: {"flowchart": {"useMaxWidth": false}} }%%
graph TD
    A["Claude Code (远程)"] --> B["tcp:127.0.0.1:4713 在远程端"]
    B --> C["SSH 反向隧道"]
    C --> D["本地 PipeWire 在端口 4713"]
    D --> E["物理麦克风"]

声音从硬件 → PipeWire → TCP 套接字 → SSH 隧道 → 远程服务器 → Claude Code 一路走过。远程机器从头到尾都不需要自己的音频硬件。

代价

它的代价:

让它持续生效

上面的搭法只对当前一次会话有效。要让它长期生效:

在远程端。 PULSE_SERVER 应该写进 ~/.zshrc(或 ~/.bashrc):

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

对于 SSH。RemoteForward 加到本地机器的 ~/.ssh/config 里,这样就不用每次都写 -R 参数:

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

一条普通的 ssh your-server 现在就会自动把隧道搭起来。

对于本地 TCP 模块。 步骤 1 已经处理过了。4713 端口在每次登录时都会打开。

副作用:什么时候会冒出 RemoteForward 警告

一旦 RemoteForward 4713 127.0.0.1:4713 住进了 ~/.ssh/config,到这台主机的每一次 SSH 连接都会尝试建起音频转发。有两种场景下这件事做不成,并且两种场景产生同一条警告:

Warning: remote port forwarding failed for listen port 4713

连接本身仍然会通过(部署能完成,shell 也会正常打开),但这条警告看起来很吓人。两种不同的场景共享同一个症状。

场景 1:CI 脚本继承了这条指令

Woodpecker CI 跑在本地机器上。一个轻量、自托管的 CI 系统,它通过 webhook 接收 git push,然后直接在宿主机上跑构建步骤(没有容器)。部署步骤用 sshrsync 把构建好的站点推到远程服务器。这两条命令连接的都是同一个主机别名,而这个别名的 SSH 配置里现在带着 RemoteForward 4713

每次部署都开始打印那条警告。部署本身一切正常:rsync 跑完、软链接换好、站点上线。但在 CI 日志里,这种级别的警告属于那种哪怕一切如常你也会想停下来看看的类型。

CI 进程跑在一个没有本地用户音频运行时的环境里:用户不同,没有交互式 shell,而本地的 PipeWire 套接字从 CI 代理运行的位置过不去。转发无处着陆,SSH 就放弃,并打印那条警告。

解决:让脚本跳出转发

SSH 提供了 ClearAllForwardings,它的作用是让 SSH 忽略配置文件里所有的 LocalForwardRemoteForward 指令。用 -o 按命令来设:

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

对一个带多次 SSH 调用的部署脚本,在最上面定义一次:

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

RSYNC_RSH 告诉 rsync 要用哪条 SSH 命令。脚本里每一次 $SSH 调用、每一次 rsync 传输,现在都不会再尝试任何端口转发。警告就此消失。交互式的 ssh g12 会话仍然能拿到音频隧道,因为它们不会使用这个覆盖。

外部链接 man.openbsd.org/ssh_config.5

场景 2:向同一台主机开第二个交互会话

另一种情况出现在两个终端(或同一个终端的两个不同时刻)都执行 ssh g12 的时候。第一个会话把 RemoteForward 建起来,并在远程端占住 4713 端口。第二个会话读的是同一份配置,也试图在远程端占住 4713,却发现它已经被占了。

警告出现在第二个会话上。出乎意料的是,语音模式在第二个会话里照样工作:第一个会话的隧道被透明共享,因为被占住的端口其实通回的是同一个本地 PipeWire。音频照样流动。那条警告其实在误导:它让人以为整个转发都失败了,实际上只是这一个具体会话想要也去占用它的尝试失败了。

真正的后果藏在背后:第一个会话一旦关闭,隧道对两边都断了。第二个会话看上去像是还连着,但语音模式要等到有人重新开出第一个会话才会恢复。

解决:SSH 连接复用

OpenSSH 自带连接复用,靠的是 ControlMaster。第一个会话打开一条主控连接;后续到同一台主机的会话共享它,而不是新开一条 TCP 连接。只有一份 RemoteForward,只有一条隧道,后面的会话上不再出警告。

~/.ssh/configHost 块里加上:

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 启用共享。ControlPath 决定复用连接的套接字文件放在哪里;%r%h%p 会被展开成用户、主机、端口。ControlPersist 10m 让主控连接在最后一个会话关闭之后再活十分钟,这样开新终端时就不用再付一次 SSH 握手的代价。

有了这一段,就可以并行开任意多个 ssh g12 终端:底下共用一条隧道,后续会话上没有警告,语音模式会因沙安拉从其中任意一个都能到达。关掉其中一个终端,也不会连累其他终端跟着断。这是把 OpenSSH 标准的复用模式套用到音频隧道场景上;底层机制本身文档充足,不过在真正依赖它之前,还是值得在自己的环境里先验证一下。

外部链接 man.openbsd.org/ssh_config.5

两种场景之间一刀切得干净:脚本跳出转发,交互式会话共享同一条隧道。

总结

做什么在哪里命令
TCP 模块(持久)本地(~/.config/pipewire/pipewire-pulse.conf"tcp:127.0.0.1:4713" 加进 server.address
SSH 隧道(持久)本地(~/.ssh/configHost your-server 下写 RemoteForward 4713 127.0.0.1:4713
多会话复用本地(~/.ssh/configControlMaster auto + ControlPath ~/.ssh/cm_%r@%h:%p + ControlPersist 10m
设置音频服务器远程(~/.zshrcexport PULSE_SERVER='tcp:127.0.0.1:4713'
配置 ALSA远程(~/.asoundrcpcm.!default { type pulse }
安装软件包远程sudo apt install libasound2-plugins pulseaudio-utils alsa-utils
CI/脚本覆盖部署脚本ssh -o ClearAllForwardings=yes + RSYNC_RSH

音频搭法五步,复用一块,加一个覆盖给那些根本不该带上转发的脚本。各个部件都已经在盒子里:PipeWire、OpenSSH、ALSA。真正的工作是把它们指到对方身上。

航空航天工程师

公开透明的道德创业者

你专注你的事业

我负责数字化的部分

与我合作
  • 诚实的 AI
  • 私有基础设施
  • 高性能网站

告诉我您的情况:

javed@javedab.com 了解更多