Getting Claude Code Voice Mode Working Inside an OrbStack Linux Container
If you run Claude Code from inside an OrbStack Linux container on a Mac, you’ve probably hit this wall:
Voice mode requires SoX for audio recording. Install it with: sudo apt-get install sox
The frustrating part? SoX is already installed. This article explains what’s actually going on and how to fix it with PulseAudio audio forwarding.
What’s Actually Wrong
The error message is misleading. Claude Code detects SoX not by checking if the binary exists, but by actually running rec and checking the exit code. When you run inside an OrbStack Linux container, the container has no audio devices — there’s no /dev/snd, no ALSA hardware, nothing. So rec fails:
rec FAIL sox: Sorry, there is no default audio device configured
Exit code: 1
Claude Code sees exit code 1 and reports “SoX not installed,” when the real problem is the container has no audio hardware access.
The Solution: PulseAudio Forwarding
PulseAudio is a sound server that supports networked audio over TCP. The plan:
- Run a PulseAudio daemon on the Mac, backed by CoreAudio (the real hardware)
- Open a TCP port so the Linux container can connect to it
- Tell SoX in the container to use PulseAudio instead of ALSA
- Point the container’s PulseAudio client at the Mac host
OrbStack exposes the Mac host to containers as host.internal, which makes this clean to set up.
Setup
macOS Side
Install Homebrew if you don’t have it:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Install PulseAudio:
brew install pulseaudio
Allow TCP connections from the container by adding the TCP module to the PulseAudio config:
echo "load-module module-native-protocol-tcp auth-anonymous=1" >> /opt/homebrew/etc/pulse/default.pa
Prevent the daemon from auto-exiting when idle (it defaults to exiting after 20 seconds with no clients, which would kill the connection any time you’re not actively recording):
echo "exit-idle-time = -1" >> /opt/homebrew/etc/pulse/daemon.conf
Start the daemon:
pulseaudio --daemonize=yes
Verify it’s running and the TCP port is open:
pulseaudio --check; echo "running: $?"
nc -z localhost 4713 && echo "TCP port 4713 is open"
Make it auto-start on login so you don’t have to do this after every reboot:
brew services start pulseaudio
Linux Container Side
Install PulseAudio client libraries and the SoX PulseAudio format driver:
sudo apt-get install -y pulseaudio pulseaudio-utils libsox-fmt-pulse
Without libsox-fmt-pulse, SoX only has ALSA support compiled in and can’t talk to PulseAudio at all.
Configure the container to use the Mac’s PulseAudio server by adding this to ~/.bashrc:
export PULSE_SERVER=tcp:host.internal:4713
host.internal is OrbStack’s hostname for the Mac host, reachable from any container.
Reload your shell:
source ~/.bashrc
Troubleshooting: The Daemon Startup Failure
When first trying pulseaudio --start on the Mac, you may hit:
W: [] caps.c: Normally all extra capabilities would be dropped now, but that's
impossible because PulseAudio was built without capabilities support.
E: [] main.c: Daemon startup failed.
This is caused by a stale PID file from a previous failed or killed PulseAudio process. Running pulseaudio -v to diagnose shows it actually starts fine — it detects your microphones (MacBook Pro Microphone, any connected iPhone mic, etc.) and loads all modules including the TCP one — but daemon mode trips over the stale PID.
Fix:
rm -f ~/.config/pulse/*.pid
Then retry pulseaudio --daemonize=yes. It will start cleanly.
One more trap: when debugging with pulseaudio -v 2>&1 | grep ... | head -20, the output will show “Daemon shutdown initiated” and “Daemon terminated.” That’s not a crash — it’s PulseAudio receiving SIGPIPE when the head -20 command closes the pipe. The daemon started fine.
Verifying the Full Chain
From the Linux container, confirm the connection is live:
pactl info | grep -E "Server|Default Source"
You should see something like:
Server String: tcp:host.internal:4713
Server Name: pulseaudio
Server Version: 17.0
Default Source: MacBook_Pro_Microphone
Then do a quick test recording:
rec -t pulseaudio -n trim 0 0.5
If it captures 0.5 seconds without error, audio is flowing from your Mac’s mic through PulseAudio over TCP into the container. You’re done.
Relaunch Claude Code and run /voice — it should come up immediately.
Why This Works
Mac microphone
↓ CoreAudio
PulseAudio daemon (macOS, port 4713)
↓ TCP over OrbStack virtual network
PulseAudio client (Linux container)
↓ libsox-fmt-pulse
SoX / rec
↓
Claude Code /voice
The OrbStack host.internal hostname is the glue — it gives the container a stable, always-valid address for the Mac host regardless of which network you’re on.
Bonus: Why Is It Recording from My iPhone?
If you notice voice mode is capturing audio through your iPhone instead of the MacBook’s built-in mic, that’s macOS Continuity Microphone at work. Introduced in macOS Ventura, it automatically promotes a nearby iPhone to the system default audio input because the iPhone’s microphone hardware is generally better than the one built into a MacBook.
You can see it happen in the PulseAudio startup log — the iPhone is loaded first and immediately set as the default source:
I: [] module-coreaudio-device.c: Initializing module for CoreAudio device 'Christopher's iPhone Microphone' (id 92)
...
I: [] core.c: default_source: (unset) -> 1 ← iPhone mic becomes default
I: [] module-coreaudio-device.c: Initializing module for CoreAudio device 'MacBook Pro Microphone' (id 87)
PulseAudio simply mirrors whatever CoreAudio reports as the system default input, so it inherits the choice.
To switch to the MacBook mic, go to:
System Settings → Sound → Input → MacBook Pro Microphone
Moving the iPhone away from the Mac also works — Continuity Microphone yields the default automatically when the iPhone is out of range or locked.
You can also override the default from the Linux container side without touching macOS settings at all:
# List available sources
pactl list sources short
# Set whichever one is the MacBook mic
pactl set-default-source <source-name>
Summary of Changes
| Location | Change |
|---|---|
/opt/homebrew/etc/pulse/default.pa | Added load-module module-native-protocol-tcp auth-anonymous=1 |
/opt/homebrew/etc/pulse/daemon.conf | Added exit-idle-time = -1 |
~/.bashrc (Linux container) | Added export PULSE_SERVER=tcp:host.internal:4713 |
| Linux packages | Installed pulseaudio, pulseaudio-utils, libsox-fmt-pulse |
| macOS packages | Installed pulseaudio via Homebrew |