[BUG] macOS: voice input dead until restart after sleep/wake — the audio-capture addon is memoized for the process lifetime and never re-probed
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
On macOS, voice input stops working permanently after the machine sleeps (close the lid, reopen it). Every subsequent recording attempt fails with a microphone error, and the only fix is to fully restart Claude Code. /voice off/on does not help, and neither does starting a new session inside the same process.
I dug into the shipped bundle for 2.1.245 to find out why, and the cause looks structural: the native audio-capture addon handle is memoized for the entire process lifetime and is never re-probed or invalidated.
There are two independent caches:
- In the audio-capture NAPI wrapper — a module-level singleton with a "tried once" flag:
``js`
function r(){ if(i) return a; i=!0; /* ...dlopen audio-capture.node... */ }
var a=null, i=!1;
i
Once is set, r() returns the same handle forever. If the load ever fails, a stays null` forever.
- In the voice module — a cached promise on the session store:
``js``
function u(e){ return e.audioNapiPromise ??= (async()=>{ ... })(), e.audioNapiPromise }
When macOS sleeps, it tears down the audio HAL and invalidates the CoreAudio input device. Claude Code keeps holding the stale handle. Nothing in the voice module listens for a wake notification, a kAudioHardwarePropertyDefaultInputDevice change, or any device-invalidation event, so nothing ever drops the cached addon.
This is made worse by the health check trusting the cache:
async function Ee(e,o={}){
...
if((await u(r)).isNativeAudioAvailable()) return {available:!0, reason:null};
...
}
isNativeAudioAvailable() only reports "the .node file loaded", not "a capture device can actually be opened". After a wake, the addon is still loaded, so checkRecordingAvailability cheerfully returns available: true and the failure only surfaces later as a mic error or an empty transcript.
There is a SoX fallback in startRecording:
if(n){ ...; if(a.startNativeRecording(...)) return t.nativeRecordingActive=!0,k }
return q(t,o,r,i) // spawns SoX `rec`
but it only engages when startNativeRecording returns false. In the stale-handle case where the device "opens" and then delivers silence, the fallback never runs and you just get "No audio detected from microphone."
What Should Happen?
Voice input should recover on its own after the machine wakes, without restarting the process.
Error Messages/Logs
Depending on how the stale handle fails, one of:
No audio detected from microphone. Check that the correct input device is selected and that Claude Code has microphone access.
Voice mode requires a microphone, but SoX could not open an audio capture device.
Voice input is failing repeatedly and has been paused. Check your microphone and try again in a moment.
Steps to Reproduce
- macOS.
"voice": { "enabled": true }in settings. Confirm dictation works. - Close the laptop lid and let the machine sleep. Reopen it.
- Return to the still-running Claude Code session and try to dictate.
- Recording fails. It keeps failing for the life of the process. Restarting Claude Code fixes it.
Suggested fix
Roughly in order of how cheap they are:
- Make the cache invalidatable. When
startNativeRecordingfails, or a recording completes having seen zero non-silent samples, clearaudioNapiPromiseand reset the addon'sa/isingleton so the next attempt re-dlopens and re-probes the device instead of reusing a dead handle. This alone converts "broken until restart" into "broken for one attempt". - Actually probe in
checkRecordingAvailability. Open the device and read a buffer rather than trustingisNativeAudioAvailable(). A loaded.nodeis not a working microphone. - Listen for the event. Register a CoreAudio listener on
kAudioHardwarePropertyDefaultInputDevice/ device-alive, or observeNSWorkspaceDidWakeNotification, and drop the cached handle when it fires. This is the actual fix. - Add a no-sample watchdog so the existing SoX fallback can engage in the "opens but returns silence" case, which today it cannot.
As a user-side workaround for anyone hitting this: brew install sox gives a fallback recorder that spawns a fresh rec process per recording, so it re-opens the device every time and is immune to the stale handle — but only for failure mode (1), not the silent-capture one.
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.245
Platform
Anthropic API
Operating System
macOS 26.1 (25B78), Apple Silicon