A Claude Code plugin that gives Claude a spoken voice. The natural-language parts of Claude's responses are spoken aloud through one of three TTS backends — Deepgram Aura-2 (cloud), Piper (local neural), or macOS say (local classic) — with automatic fallback if the primary fails. Code, tool calls, file paths, and other non-prose are filtered out.
- Hooks into Claude Code's
Stopevent (always),PreToolUsein Mode B for short tool cues, andNotificationin Mode C, to extract or generate the speakable text for each event. - Strips code blocks, file paths, URLs, and markdown formatting so only natural sentences remain.
- Optionally runs the result through Claude Haiku 4.5 to lightly polish phrasing (off by default — see
config.rewrite). - Sends the polished text to Deepgram Aura-2 for high-quality TTS, with
sayas a local fallback if Deepgram is unconfigured or unreachable. - Streams Deepgram's audio bytes directly into
ffplay's stdin so the first sentence starts playing within ~1s instead of waiting for the full mp3. Long replies split into sentence-sized chunks; consecutive chunks share oneffplayso audio plays gaplessly across chunk boundaries. - Falls back to file-based
afplayplayback ifffplayis not installed. - Audio from a prior turn is killed the moment you send a new prompt.
| Mode | When Claude speaks |
|---|---|
| A (default) | Final response only — one clip per turn. Fully hook-driven and deterministic. |
| B | Same as A, plus automatic short cues ("Looking that up", "Pulling it up") before tool calls via a hook-driven PreToolUse event. Both end-of-turn and pre-tool speech are fully deterministic and hook-driven. speak_cli is reserved for between-tool interjections only. |
| C | Final response plus distinct alerts when Claude hits a Notification event (asks for permission, signals a blocker, etc.). |
Mode is set via config.json. The setup skill regenerates hooks/hooks.json so only the hooks for the current mode are registered. In mode B, a SessionStart hook injects instructions forbidding Claude from narrating tool calls (the PreToolUse hook handles this deterministically) and reserving speak_cli for optional interjections between tool calls.
- macOS (the
sayfallback is Darwin-only) - Python 3.10+
- Claude Code
- Optional but recommended: a Deepgram API key (free tier works; Aura-2 voices are well above
sayquality) - Optional but recommended:
ffplayfromffmpeg(brew install ffmpeg). Withffplay, audio streams directly from Deepgram for ~1s time-to-first-audio. Without it, the plugin falls back to writing temp mp3s and playing withafplay. - Optional: Piper for local neural TTS that beats
sayquality without needing a network. See the Voices section below.
Inside a Claude Code session:
/plugin marketplace add jason-c-dev/claude-code-speak
/plugin install claude-speech@claude-code-speak
/reload-plugins
Or from a local clone:
git clone https://github.com/jason-c-dev/claude-code-speak ~/dev/claude-code-speak/plugin marketplace add ~/dev/claude-code-speak
/plugin install claude-speech@claude-code-speak
/reload-plugins
Then ask Claude to set it up:
> set up speech
The setup skill walks you through installing the Python dependency (claude-agent-sdk), entering your Deepgram key (or skipping for say-only mode), picking a voice, picking a mode, and running a smoke test. About a minute, end to end.
Plugin-level config lives in config.json at the repo root. The setup skill regenerates this — you shouldn't normally edit it by hand. Example:
{
"enabled": true,
"mode": "A",
"voice": "aura-2-thalia-en",
"primary_tts": "deepgram",
"fallback_tts": "say",
"piper_voice": "",
"say_voice_map": {},
"rewrite": false,
"speech_rate": 1.0
}primary_tts and fallback_tts each accept "deepgram", "piper", or "say". The plugin tries primary first; if that backend's prerequisites are missing or it returns no audio, it falls through to fallback. See Voices for backend details.
Your Deepgram key lives outside the repo at ~/.claude/voice/.env:
DEEPGRAM_API_KEY=...
This survives plugin reinstall and never lands in version control.
To change voice or mode later, just say change voice or change voice mode and re-run the setup flow.
To temporarily mute Claude:
echo '{"enabled": false}' | jq -s '.[0] * .[1]' config.json - > /tmp/c.json && mv /tmp/c.json config.json(Or simply uninstall the plugin via /plugin uninstall.)
Three TTS backends are supported. Mix-and-match via primary_tts / fallback_tts in config.json.
Aura-2 neural voices. Pick one via voice in config; the setup skill knows the curated set:
| Voice id | Description |
|---|---|
aura-2-thalia-en (default) |
Clear, confident, energetic American female |
aura-2-orion-en |
Approachable American male |
aura-2-luna-en |
Friendly young-adult American female |
aura-2-asteria-en |
Knowledgeable, energetic American female |
aura-2-zeus-en |
Deep, trustworthy American male |
aura-2-pandora-en |
Smooth, calm British female |
Any other Aura-2 model id (e.g. aura-2-callista-en) also works — see Deepgram's docs for the full list. Streaming via ffplay keeps time-to-first-audio around 1s.
Piper runs CPU-only with Apple-silicon-fast latency (~100ms). Quality is comparable to entry-tier Deepgram and fully local. To enable:
# Install the Piper CLI. pipx is recommended; pip works too.
pipx install piper-tts # `brew install pipx` first if you don't have pipx
# Download a voice. Pick any from https://huggingface.co/rhasspy/piper-voices
mkdir -p ~/piper-voices && cd ~/piper-voices
curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx
curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx.jsonThen in config.json:
{
"primary_tts": "piper",
"fallback_tts": "say",
"piper_voice": "~/piper-voices/en_US-amy-medium.onnx"
}Each .onnx file ships with a sibling .onnx.json; both must live in the same directory. Piper voices range from ~20MB (low) to ~75MB (high) — medium is usually the sweet spot.
Always available without configuration. The voice field is a Deepgram id, so when say is in use the plugin maps Deepgram → say voice via a lookup table. Defaults:
| Deepgram voice | say voice |
|---|---|
aura-2-thalia-en / -luna-en / -asteria-en |
Samantha (en_US) |
aura-2-orion-en |
Alex (en_US) |
aura-2-zeus-en |
Daniel (en_GB) |
aura-2-pandora-en |
Karen (en_AU) |
Override the mapping in config.json to use any system voice (say -v '?' lists every voice on your machine):
{
"primary_tts": "say",
"say_voice_map": {
"aura-2-thalia-en": "Reed (English (US))"
}
}The newer macOS voices — Reed, Sandy, Flo, Eddy, Grandma, Grandpa, Rocko, Shelley — sound noticeably better than legacy ones. They use Apple's newer neural engine. Some come pre-installed on macOS 14+; others can be added via System Settings → Accessibility → Spoken Content → System Voice → Manage Voices.
You don't write speech tags. The plugin extracts speech in three steps:
-
Strip — drop code blocks, inline code, file paths, URLs, markdown, emoji.
-
Rewrite (optional, off by default) — if
config.rewriteistrue, the stripped text is passed through Claude Haiku 4.5 with a prompt that says: "rewrite as one or two natural spoken sentences, or return empty if there's nothing worth saying aloud." This step requires a Claude Code Max plan (or API auth) to call Haiku; with rewrite off, the stripped text is spoken directly.Tradeoff: rewrite produces nicer, more conversational summaries — but each turn pays a Haiku round-trip (typically 1–3s) before the first audio plays. The default (off) keeps end-of-turn audio near-instant; flip it on if you'd rather have polished phrasing than low latency. Mode B's pre-tool cues are unaffected — they always come from the static lookup table, no rewrite.
-
Synthesize — Aura-2 turns it into audio.
When rewrite is enabled, Haiku returning the empty string is a valid signal — Claude says nothing aloud for that turn. Pure tool-use turns naturally produce nothing speakable and stay silent.
Every failure mode is "log and silent skip". The plugin will not block your session, throw errors back into Claude Code, or partially break a turn. Worst case: Claude doesn't speak. Logs go to ~/.claude/voice/voice.log for diagnosis.
Common fallbacks:
| Situation | What happens |
|---|---|
ffplay not installed |
Use file-based playback (Deepgram → temp mp3 → afplay) |
| Deepgram 5xx, timeout, or mid-stream error | Fall through to file-based playback, then to the configured fallback backend (piper or say) |
| No Deepgram key configured | Skip Deepgram, speak via the fallback backend |
| Piper binary or voice model missing | Skip Piper, fall through to say |
| Haiku auth fails (when rewrite is enabled) | Speak the heuristic-stripped text raw |
| Stripped text is empty | Stay silent for this turn |
| Audio device unavailable | Log and stay silent |
~/.claude/voice/
├── .env # DEEPGRAM_API_KEY
├── voice.log # diagnostic log
├── state/<session_id>.json # per-session state (offsets, current pid)
└── tmp/<uuid>.{mp3,aiff} # transient audio, deleted after playback
The plugin repo itself contains code, hooks, the setup skill, and example config. No user data lives in the repo.
The user-data directory stays at ~/.claude/voice/ even though the plugin renamed from claude-voice to claude-speech. Keeping the path stable means existing setups (your Deepgram key, voice log, session state) survive the rename without migration.
- Stripped + rewritten text is sent to Deepgram (TTS) and to Anthropic (Haiku rewrite via the Claude Agent SDK, billed against your Max plan).
- The full transcript is not sent to either — the plugin only reads what's needed for the current event.
- Audio files are local. They're deleted after playback and on session end.
| Command | What it does |
|---|---|
/speech-mute |
Toggle the plugin on/off. When muting, also kills any in-flight audio so you get immediate silence. |
/speech-stop |
Stop only the audio currently playing/queued for this session, without disabling the plugin. |
Named speech-* rather than voice-* to avoid clashing with Anthropic's built-in /voice (speech-to-text dictation).
- Overlap synthesis: kick off the next chunk's Deepgram request before the current chunk finishes streaming, so its bytes are ready the moment the previous audio drains.
- Piper streaming via
--output_rawpiped intoffplay(currently file-based). - Linux/Windows fallback (replace
saywithespeak-ng). - Cost telemetry surfaced through the setup skill.
MIT. See LICENSE for the full text.