Voice mode fails on Termux/Android: `which` blocked by kernel (EPERM)

Resolved 💬 3 comments Opened Mar 21, 2026 by daniel-thisnow Closed Mar 23, 2026

Environment

  • Platform: Android 13 (Samsung), Termux
  • Claude Code version: latest
  • Shell: zsh
  • Arch: aarch64-linux-android

Description

/voice mode fails on Termux with the message:

Voice mode requires SoX for audio recording. Install SoX manually: ...

SoX is installed and fully functional (sox, rec, play all present at /data/data/com.termux/files/usr/bin/). The issue is not SoX being missing — it's that the availability check itself fails.

Root Cause

The Fn() helper checks for binaries using execFileSync('which', ['rec'], ...). On Android/Termux, spawning which via Node.js execFileSync returns EPERM:

spawnSync which EPERM

This is an Android kernel restriction on certain subprocess spawning patterns — the same restriction that breaks Claude Code's bundled ripgrep on arm64-android. Because Fn("rec") always throws/returns false, the voice availability check always reports SoX as missing regardless of whether it's installed.

Reproduction

On any Termux (Android) install with SoX installed:

# SoX is present:
which rec   # → /data/data/com.termux/files/usr/bin/rec

# But Node.js can't run `which`:
node -e "const {execFileSync} = require('child_process'); execFileSync('which', ['rec'], {stdio:'pipe', timeout:3000})"
# → spawnSync which EPERM

Suggested Fix

In the Fn() binary check, fall back to fs.existsSync() across known PATH entries when execFileSync('which', ...) throws EPERM. For example:

function Fn(cmd) {
  try {
    return execFileSync('which', [cmd], { stdio: 'pipe', timeout: 3000 }).status === 0;
  } catch (e) {
    if (e.code === 'EPERM' || e.code === 'EACCES') {
      // Fallback for Android/Termux: walk PATH manually
      const PATH = process.env.PATH || '';
      return PATH.split(':').some(dir => {
        try { return require('fs').existsSync(require('path').join(dir, cmd)); } catch { return false; }
      });
    }
    return false;
  }
}

Related

This is the same class of issue as the bundled ripgrep binary failing on arm64-android (ENOENT for vendor/ripgrep/arm64-android/rg). Both stem from Android's restrictions on native binary execution in certain contexts.

Android users are a real use case — Termux gives Claude Code a fully functional Linux-like environment on a phone, and voice input via SoX would be genuinely useful there.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗