Talk to your coding agent — hands-free, from your phone.
English | 简体中文
This is the self-hosted backend for the Handsfree iOS app: it runs on the computer where your coding agent lives, drives it on your behalf, and exposes a WebSocket your phone connects to.
100% on-device voice — and completely free. Speech-to-text and text-to-speech run entirely on Apple's built-in, on-device engines inside the iOS app. There's no cloud voice API, no separate voice model to host, no subscription and no per-minute fees — nothing extra to pay for. Talk to your coding agent and hear it reply, fully hands-free, anywhere and any time.
- Free, on-device voice. All audio stays on the phone (Apple's built-in speech recognition + text-to-speech). No online voice model, no API keys for voice, no extra cost — the bridge itself is text-only.
- Drives Claude Code (via the Claude Agent SDK) and OpenAI Codex (via the Codex CLI), side by side.
- Streams replies token-by-token and routes tool-permission prompts and multiple-choice questions to the phone — the bridge stays the permission gate.
- One live session per
(project, agent); multiple phones can view and drive the same session at once. - LAN out of the box, or Tailscale for talking to your agent from anywhere. Optional shared-secret token.
The bridge speaks a plain text/JSON WebSocket protocol (see src/protocol.ts).
flowchart LR
subgraph Phones["📱 Handsfree app(s) — iOS"]
direction TB
STT["Speech → text<br/>(on-device)"]
TTS["Text → speech<br/>(on-device)"]
end
subgraph Host["💻 Your computer"]
direction TB
Bridge["Handsfree Bridge<br/>permission gate · client fan-out<br/>one live session per (project, agent)"]
Claude["Claude Code<br/>(Agent SDK)"]
Codex["OpenAI Codex<br/>(CLI)"]
Files[("Session history<br/>~/.claude · ~/.codex")]
end
Phones <-->|"WebSocket — LAN / Tailscale<br/>(optional token)"| Bridge
Bridge -->|"prompts · permission asks"| Claude
Bridge --> Codex
Claude --> Files
Codex --> Files
Audio stays on the phone; the bridge only moves text and tool-permission prompts. Multiple phones attach to the same live session and stay in sync — the bridge is the single writer (see Sessions & concurrency).
- Node 20+
- A coding agent installed and logged in:
- Claude Code, logged in with a Claude Pro/Max subscription. Keep
ANTHROPIC_API_KEYunset so it runs on your subscription rather than pay-as-you-go API billing. - and/or OpenAI Codex — install the Codex CLI (
npm i -g @openai/codexorbrew install codex) and runcodex login. Optional.
- Claude Code, logged in with a Claude Pro/Max subscription. Keep
- (Optional, for remote access) Tailscale on both the computer and the phone, signed in to the same account.
Three quick steps — the same ones the app walks you through.
Your phone and the computer running your coding agent need to reach each other. Do this first — the bridge picks the right address when it starts.
- On the same Wi-Fi — if your phone and computer share a network, there's nothing to set up.
- Away / remote — install Tailscale on both
the computer and the phone and sign in to the same account. The bridge then
advertises your Tailscale address automatically and the phone reaches it from
anywhere — no port forwarding. Leave the default
HANDSFREE_BIND=0.0.0.0, or set it to your Tailscale IP to refuse all other interfaces.
The advertised host is chosen automatically: your Tailscale IP if available,
otherwise your LAN IP, otherwise localhost. Override it with
HANDSFREE_HOST (see Configuration).
Already using a VPN to reach Claude / Codex? (split tunneling)
If your computer needs a VPN to reach your coding agent's API, that VPN and Tailscale both manage routing and can fight each other — a full‑tunnel VPN can starve Tailscale so the phone can't reach the bridge. Two things make them coexist:
- Your phone never talks to the agent — only to the bridge. So the phone only needs Tailscale (or the same Wi‑Fi), not your Claude/Codex VPN. On the same Wi‑Fi the phone needs no VPN at all.
- Split‑tunnel the other VPN on the computer so Tailscale's traffic skips
it. Tailscale uses the CGNAT ranges
100.64.0.0/10andfd7a:115c:a1e0::/48; exclude those from your VPN (or, on an inclusive split tunnel, route only your agent's API endpoints through the VPN). This is what Tailscale officially recommends — see their other‑VPNs FAQ.
Alternatively, skip the system VPN entirely and send only the bridge's
outbound API calls through a proxy: the Claude CLI and Codex honor
HTTPS_PROXY / ALL_PROXY, so HTTPS_PROXY=… npx handsfree-bridge@latest tunnels the
agent traffic while leaving Tailscale untouched.
Caveats: this can't work if the other VPN is an exit‑node‑style full tunnel that
won't let you exclude routes, or if it itself uses the 100.64.0.0/10 range
(a direct conflict with Tailscale).
On the computer where your coding agent runs, start the bridge and leave it running:
npx handsfree-bridge@latestNeeds Node 20+, with your coding agent installed and logged in (see
Prerequisites). Configure it with the environment variables in
Configuration, e.g. HANDSFREE_TOKEN=… npx handsfree-bridge@latest.
Or run from source
git clone https://github.com/devtheicetea/handsfree.git
cd handsfree
npm install && npm run build
npm startOn start, the bridge prints a pairing QR code and a handsfree://connect?…
URL in its terminal, with the host and port (default 8744) beneath it. In the
Handsfree app:
- tap Scan QR Code and point the camera at it, or
- tap Enter connection manually and type the host and port shown.
That's it — start talking to your coding agent, hands-free.
All configuration is via environment variables:
| Variable | Default | Description |
|---|---|---|
HANDSFREE_PORT |
8744 |
Listen port. |
HANDSFREE_BIND |
0.0.0.0 |
Bind address. Set to your Tailscale IP to listen only there. |
HANDSFREE_HOST |
auto | Host advertised in the pairing QR/URL. Auto = Tailscale IP → LAN IP → localhost. |
HANDSFREE_TOKEN |
(none) | Optional shared secret; the client must send it in its hello. |
HANDSFREE_SAFELIST |
Read,Grep,Glob,LS,TodoWrite,CodexApplyPatch |
Comma-separated tools auto-approved in safelist permission mode. |
HANDSFREE_MODEL |
(SDK default) | Model for Claude sessions (e.g. sonnet, opus, or a full model id). |
HANDSFREE_CODEX_PATH |
resolve from PATH |
Full path to the codex binary. |
HANDSFREE_ENV |
prod |
Run mode. debug turns on verbose per-message console logging; prod keeps the console quiet. |
Every tool call is gated. Each session runs in one of three modes, switchable from the phone:
- safelist — tools in
HANDSFREE_SAFELISTauto-approve; everything else asks. - ask_all — every tool call prompts on the phone.
- auto — everything auto-approves (use with care).
The bridge is always the gate: prompts stream to the connected client(s) and the agent waits for an Allow / Allow-for-session / Deny answer.
The bridge assumes the cardinal rule of Claude Code / the Agent SDK: exactly one live process may write to a session's history at a time. Two live writers on one session produce interleaved, divergent, or corrupt history. Everything below is a strategy to respect that rule.
- Many phones, one session, at the same time → yes. Multiple Handsfree clients subscribe to the same single bridge-owned session and fan out: there's still one live writer (the bridge), and the phones are just synchronized views with input.
- Phone joining a session your terminal is running (mirror) → safe by forking. The phone watches the live CLI session read-only; on its first prompt the bridge forks a new session id (copies the history, then diverges) instead of becoming a second writer on the terminal's session.
- Resuming a bridge session in your terminal (
claude --resume <id>/codex resume) → sequential handoff only. This continues the same id (no fork), so stop the bridge first to release the session, then resume in the terminal. Running the bridge and a terminal resume on one id simultaneously is two writers — don't.
In short: same live session on multiple devices at once is supported through the bridge's fan-out; the bridge and a native resume co-driving one id is not.
- The bridge has no auth beyond an optional
HANDSFREE_TOKEN. Don't expose it to the public internet. Bind to localhost/LAN, or use Tailscale so only your own devices can reach it. - The agent runs with your local permissions in the chosen project directory. Pick your permission mode accordingly.
MIT © Contrast Studio LLC