A desk phone becomes the line between you and your Claude Code sessions.
Came from Instagram? Watch it in action, then follow the setup below to build your own.
Mac only (Apple Silicon, M1 or later). It relies on macOS throughout: iTerm2 scripting to type into sessions, the
sayvoices, the keychain and launchd. There are no Windows, Linux or Intel Mac instructions.
- Claude rings you: a session calls the
call_<name>MCP tool (call_samforowner_name = "Sam"). The phone rings withrepo/branchas caller ID; the spoken answer comes back as the tool result. - Claude announces:
notify_<name>rings, reads a short announcement and hangs up. - Transfers:
transfer_callputs you through to another session when you ask the wrong one. - You ring Claude: every live session has an extension (301-399). Dial it, speak, hang up; the words are typed into that session's iTerm2 tab and submitted.
- Voicemail: unanswered questions light the message LED. Press Message, answer each one; answers are typed into the asking sessions.
Everything runs locally on one Mac: Asterisk (the phone exchange), whisper.cpp (speech to text), macOS say
or ElevenLabs (text to speech) and a small Python daemon. Your voice never leaves the Mac; the only things sent out
are text to ElevenLabs, if you choose it, and missed-call pushes, if you set them up. Why each piece is built the way it is: docs/build-notes.md.
- An Apple Silicon Mac with iTerm2 and Homebrew; Claude Code runs in iTerm2 tabs.
- A SIP desk phone on the same network as the Mac, plugged into the router with a network cable. Built and tested on a Yealink T31P; docs/t31p-setup.md walks through it. The T31P is powered over the network cable (PoE), and second-hand ones often come without a power supply: without a PoE switch or injector, buy the Yealink 5 V power adapter as well. Other SIP phones should work but are untested; per-repo ringtones and the live session list are Yealink features.
- No desk phone yet? Linphone (free, Mac or mobile) works for trying everything out.
- Python 3.12+, Xcode command line tools (to build Asterisk), and about 1 GB of disk for the speech model.
- No accounts or subscriptions. ElevenLabs (nicer voices) and ntfy (missed-call pushes) are optional.
| Ext | What |
|---|---|
| 301-399 | live Claude Code sessions (phone list). Mid-task, you get hold music until its turn ends; # leaves your message now |
| 200 | T31P |
| 201 | Linphone (testing) |
| 0 | operator: say who you want ("the backend on the AGS branch") and it puts you through |
| 9 | all-hands: your message is typed into every live session |
| 7233 | Crack the Safe (S-A-F-E): find the 4-digit code in 8 tries; type 4 digits then #, the board shows after each guess while a guard heckles; Exit closes it to type the next one. Needs screen_push |
| 500 | voicemail (T31P Message key) |
| 600 | echo test |
| 601 | 1 kHz tone |
On any recording: # ends it, hanging up also ends it. In voicemail: # during a question skips it, 7 deletes it.
- Tools:
xcode-select --install, thenbrew install openssl@3 whisper-cpp. - Asterisk (about 10 minutes):
scripts/build-asterisk.shbuilds 22.11 into~/.claude-phone/asterisk. The first start after a build takes about 45 s while macOS scans the new libraries. - Speech model: the first start (step 6) downloads
ggml-large-v3-turbo-q5_0.bin(~550 MB) on first run. - Python:
python3 -m venv .venv && .venv/bin/pip install -e . - Config:
cp config.example.toml ~/.claude-phone/config.tomland setowner_name. Setring_ext = "201"to test on Linphone. - Secrets are in the login keychain (service
claude-phone):phone secret sipprints the SIP password for the phones;ariis internal. ElevenLabs:ELEVENLABS_API_KEYor keychain accountelevenlabs. - Start:
.venv/bin/phone install-services(runs everything now and at every login), then.venv/bin/phone status. - Desk phone: docs/t31p-setup.md. Dial 600 and you should hear yourself (echo test).
- Claude Code wiring (global; see below).
- CLAUDE.md: add docs/claude-md-snippet.md to
~/.claude/CLAUDE.md. - Try it: open a new Claude Code session in iTerm2 and ask it to ring you. Then dial its extension
(
phone list) and speak: the first time, macOS asks to let the daemon control iTerm2. Allow it.
From the repo folder:
claude mcp add --scope user claude-phone -- "$PWD/.venv/bin/phone" mcpplus four hooks in ~/.claude/settings.json (replace /path/to/claude-phone):
"hooks": {
"SessionStart": [{"hooks": [{"type": "command", "command": "/path/to/claude-phone/.venv/bin/phone hook session-start"}]}],
"SessionEnd": [{"hooks": [{"type": "command", "command": "/path/to/claude-phone/.venv/bin/phone hook session-end"}]}],
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "/path/to/claude-phone/.venv/bin/phone hook prompt"}]}],
"Stop": [{"hooks": [{"type": "command", "command": "/path/to/claude-phone/.venv/bin/phone hook stop"}]}]
}Then start Claude Code sessions in iTerm2 tabs; each one gets an extension (phone list).
Out of the box every call uses the Mac's built-in Daniel voice (British English, macOS say): free, offline,
no account. It's the voice in the original demo video. Set say_voice in config.toml to change it; say -v '?' lists the voices, and better
"Enhanced" and "Premium" ones download in System Settings → Accessibility → Spoken Content → System Voice.
The other demo clips use Jessica from the ElevenLabs voice library. To use her, or any ElevenLabs voice:
tts_provider = "elevenlabs"
elevenlabs_voice_id = "r1KmysJdVYZjJCm4mL3b" # Jessica
operator_provider = "elevenlabs" # the operator on 0
operator_voice = "r1KmysJdVYZjJCm4mL3b"and put your API key in the keychain (security add-generic-password -s claude-phone -a elevenlabs -w) or
ELEVENLABS_API_KEY. Speech is cached, so repeated phrases cost nothing. If ElevenLabs fails or there's no key,
that call falls back to the Mac voice rather than going silent. Each repo can have its own voice (repo_voices
in config.example.toml).
With screen_push = true, every call and announcement also leaves a short summary on the phone's screen
(sessions write it in the tools' screen field). With meeting_alerts = true, two minutes before each meeting
in the Mac's Calendar app (any synced account: Google, iCloud, Exchange) the phone answers itself on the
speaker and whispers "Pssst, you have a meeting", without ringing, then shows the time, title, who it's with
and when it ends. From then until the meeting ends, Claude's calls are held: questions go to voicemail,
announcements go to the screen only, and nothing is pushed to your mobile.
- Phone: the screen needs the T31P to trust the Mac (docs/t31p-setup.md step 11).
- Mac: the first time, allow calendar access and local network access for Python when macOS asks (System Settings → Privacy & Security → Calendars / Local Network).
- DND key: the Mac switches the phone's DND on at each alert and off when the meeting ends. Switch it off yourself to end the hold early; switch it on any time to hold Claude's calls (step 12 of the phone guide).
phone meetingslists your calendars and the meetings it will use;meeting_calendars = ["Work"]limits it.phone meeting-testplays a made-up alert so you can hear and see it. Calls are not held.- The whisper is the Mac's Whisper voice by default. Jessica whispering (ElevenLabs v3):
whisper_provider = "elevenlabs_v3",whisper_voice = "r1KmysJdVYZjJCm4mL3b",whisper_text = "[whispers] Pssst... you have a meeting."
Install the free ntfy app, run phone push in a terminal and subscribe to the topic it prints (treat it as a password), then phone push test. A call_<name> that goes to voicemail, or a notify_<name> that isn't heard, then pushes to the app. Pushes pass through the public ntfy.sh server; set push_text = false in config.toml to send only who called.
Everything runs as launchd user agents (com.claude-phone.{asterisk,whisper,daemon}, KeepAlive, start at login):
phone install-services / phone uninstall-services. Without them, phone up runs the same processes by hand.
phone restart daemon # after config changes (asterisk: also regenerates its config)
phone up # asterisk, whisper-server, daemon (each also: phone up daemon)
phone status # processes, daemon health, registered phones
phone list # live sessions and extensions
phone vm # pending voicemail questions
phone down
tail -f ~/.claude-phone/logs/phone.log # calls, transcripts, every injectionphone is .venv/bin/phone.
| Path | What |
|---|---|
~/.claude-phone/registry.db |
sessions, voicemails, injection log (SQLite) |
~/.claude-phone/logs/phone.log |
the log to tail |
~/.claude-phone/logs/{asterisk,whisper,daemon}.out |
process output |
~/.claude-phone/logs/asterisk/messages.log |
Asterisk log |
~/.claude-phone/etc/asterisk/ |
generated Asterisk config (from asterisk/*.tmpl, phone config) |
~/.claude-phone/sounds/ |
TTS cache |
.venv/bin/pip install -e '.[dev]' then .venv/bin/pytest. They run against a throwaway home and a fake call: no Asterisk, audio, whisper or iTerm needed.
- Call connects, no audio: every endpoint needs
media_address,bind_rtp_to_media_address=yesandrtp_ipv6=no(asterisk/pjsip.conf.tmpl). If the Mac's IP changes, updatelan_ipand runphone config && phone restart asterisk. - Phone won't register:
phone statusshows contacts. Check the password (phone secret sip) and that the phone is onlan_cidr. Asterisk log:~/.claude-phone/logs/asterisk/messages.log. - Asterisk console:
~/.claude-phone/asterisk/sbin/asterisk -C ~/.claude-phone/etc/asterisk/asterisk.conf -r call_<name>always goes to voicemail:phone status→desk_online. Asterisk qualifies the phone every 10 s, so a phone that was just plugged in can take that long to count as online.- Text not typed: the tab must be in
phone list. iTerm2 needs Automation permission for whatever runs the daemon (System Settings → Privacy & Security → Automation). - Wrong words for repo names: whisper is prompted with live repo and branch names; check
phone listspelling.
Built by @risingpower with Claude Code.
MIT. See LICENSE.