Spoken summaries for Claude Code, marked inline by Claude itself.
Claudio is a small CLI + hook that makes Claude Code speak. Claude wraps a short summary of every answer in a <speak>...</speak> tag — claudio reads the transcript, extracts those blocks, and plays them through a TTS provider while you read the full reply.
You: fix the failing test
Claude: [writes code, runs tests, all green]
<speak>Tests pass, three fixes committed.</speak>
↑ this is what you hear, ~3 seconds of audio
You read code faster than you read prose. Most of Claude's reply is detail you can skim. The one-line "what happened" is the part you'd actually want to know while looking somewhere else. Claudio plays that line in your ear while your eyes scan the diff.
- Two providers out of the box: ElevenLabs (neural, paid, premium voices) and macOS
say(offline, free, native multi-language voices like Yelda for Turkish). - Streaming mode (
live): plays the speak block between tool calls, not just at the end of the turn. You hear "looking at the index file..." while the tool runs. - Interactive TUI (
claudio configure): switch provider, browse voices by language, search by name, preview the selected voice. - Stop on demand (
claudio stop): kills any currently playing audio. - Sanitizer: strips markdown, backticks, HTML tags, code fences from the speak text before sending it to TTS. Your TTS won't say "less than slash speak greater than" out loud.
- Race-resilient hook: retries the transcript reader and checks for late-arriving entries, so audio matches the message you actually see.
- Session-scoped state: multiple Claude Code windows do not stomp on each other.
- Silent failure: a broken TTS provider never blocks Claude. Errors go to the log, not to your face.
You need Bun (>= 1.1) and macOS (the player and the system provider both call afplay / say).
git clone https://github.com/<you>/claudio
cd claudio
bun install
bun link # registers `claudio` on your PATH
claudio install # writes the hook into ~/.claude/settings.jsonclaudio install is idempotent. Re-run it any time; it merges into your existing settings.json and never touches hooks owned by other tools.
claudio configureYou will see your current settings, then a menu:
What now?
> Change voice
Change provider
Change streaming mode (stop / live)
Preview current voice
Save and exit
Voices are pulled live from the active provider — say -v ? for the system provider, the /v1/voices endpoint for ElevenLabs. Long lists are filterable by language and substring.
To use ElevenLabs, put your key in your shell environment:
export ELEVENLABS_API_KEY=sk_... # in ~/.zshrc or ~/.zshenvOr, if you do not want it in env, edit ~/.claude/claudio/config.json directly and put the literal key in the apiKey field.
stop (default) Audio plays after the turn finishes. Simple and predictable.
live Audio also plays between tool calls. More responsive on
multi-step replies, but Claude's pre-tool text must include
a `<speak>` block for there to be anything to play.
live mode is opt-in. Toggle it in claudio configure under "Change streaming mode" — no reinstall needed, the hook reads the config on every fire.
Add a CLAUDE.md block in your project (or in ~/.claude/CLAUDE.md for every session). A starter template is in this repo at CLAUDE.md — copy it as-is, or edit the tone and length to taste. The short version:
End every substantive response with one
<speak>...</speak>block — one sentence, ≤ 15 words, in the user's language. If you are about to run tools, also write a short pre-tool sentence and wrap it in<speak>so the user hears what is about to happen while the tools run.
| Command | What it does |
|---|---|
claudio install |
Add Stop + PostToolUse hooks to ~/.claude/settings.json, migrate config from legacy locations. |
claudio configure |
Interactive TUI: provider, voice, streaming mode, preview. |
claudio test "<text>" |
Synthesize and play one phrase through the current provider. Useful for sanity checks. |
claudio stop |
Kill any currently playing claudio audio across all sessions. |
claudio hook |
Internal — invoked by Claude Code's Stop / PostToolUse hooks; reads JSON payload on stdin. |
claudio --version |
Print the version. |
- Claude writes a reply that contains one or more
<speak>...</speak>blocks. - Claude Code fires a Stop hook (and, in live mode, PostToolUse hooks between tool calls). The hook runs
claudio hook. claudioreads the session's transcript file at the path Claude Code gave it, finds the most recent assistant text entry, and extracts the speak blocks.- Per-session state (
~/.claude/claudio/state/<session_id>.json) remembers which message uuid was last spoken — the same message is not spoken twice even when both Stop and PostToolUse hooks see it. - The text is sanitized, sent to the configured provider, played through
afplayas a detached child, with a PID file so a later hook can interrupt it.
If the hook fails for any reason — network down, API quota exhausted, no audio device — Claude is never blocked. The failure is logged to ~/.claude/claudio/log and the next turn proceeds normally.
Today: macOS only. The system provider (say) and the audio player (afplay) are macOS binaries. ElevenLabs works wherever Bun runs, but the player path does not.
Linux / Windows support is welcome as a contribution — the player is a single small module and the system provider abstracts over say. Drop-in replacements with mpg123 (Linux) or powershell -c (New-Object Media.SoundPlayer) (Windows) would not be a large change.
~/.claude/claudio/config.json. Created by claudio install. Validated by a strict zod schema (typos in top-level keys are rejected).
I get no audio at all.
Run claudio test "hello". If you hear nothing, the pipeline is broken before Claude Code is involved — check the audio output device and the provider. If you hear it, the hook side is the problem; look at ~/.claude/claudio/log.
The hook fires but no audio plays.
Check the log. The most common cause is a provider error (401 from ElevenLabs, missing say voice on a fresh macOS). Switching provider to system is the quickest reset.
Audio cuts off mid-sentence.
Most likely a PostToolUse fire is interrupting the previous audio. Either switch to stop mode, or wait a turn — the dedup will catch up.
The hook reads stale text. Already mitigated: the orchestrator does a stability check after finding a fresh transcript entry, so it does not commit to playing an intermediate chunk. If you still see this, your transcript flush is slower than 5 × 200 ms — open an issue with timing.
bun test # 98 tests, ~3s
bun run lint # biome
bunx tsc --noEmit # strict typecheck
bun run src/cli/index.ts test "merhaba"The codebase is small (~1.5k lines including tests). Modules are flat: audio, config, hook, providers, cli, util. No internal cycles.
MIT.
{ "enabled": true, "provider": "system", // "elevenlabs" or "system" "streamingMode": "stop", // "stop" or "live" "providers": { "elevenlabs": { "apiKey": "$ELEVENLABS_API_KEY", // env interpolation supported "voiceId": "FGY2WhTYpPnrIDTdsKH5", "model": "eleven_turbo_v2_5" }, "system": { "voice": "Yelda" } }, "cache": { "enabled": true, "maxSizeMb": 500 }, "queue": { "interruptOnNewPrompt": true }, "filter": { "minLength": 3, "maxLength": 1000 } }