Claude reads its replies aloud only when you type /voice. Recommended voice model: Kokoro-82M (af_aoede), swappable for any read-along voice. /v aliases…

<img width="2172" height="724" alt="Speakup, a mod for Claude Code: type /voice and Claude reads its replies aloud, on your own machine." src="https://github.com/user-attachments/assets/de7b126a-ce12-44ae-b881-61dcfd9c024e" />
A mod for Claude Code: Claude reads its replies to you out loud, but only when you ask. Every session starts silent. Type /voice and it starts talking; type it again and it stops. The recommended voice model is Kokoro-82M, with its natural af_aoede voice, and it's what speakup sets up by default. Prefer another voice? Swap it out.
Speakup is for anyone who wants Claude's voice some of the time, not all of it: on for a long explanation you'd rather listen to while you look at the code, off again in a meeting, a shared office, or a quick back-and-forth. It's a thin layer on top of read-along, which does the speaking and highlights the sentence you're hearing.
/voice toggles it, and says "Voice on" so you know it worked./v for everything else. A shorthand for read-along's controls: /v pause, /v stop, /v speed 1.2.Kokoro-82M, by hexgrad, is the voice model we recommend, and the one speakup sets up by default: an open-weight, 82-million-parameter text-to-speech model (Apache 2.0) that sounds close to a person reading, yet is small enough to run on a laptop's CPU at about three times real time. No cloud service, no API key, no GPU.
Speakup picks its af_aoede voice (American English; the af is American, female) at 0.9× speed. read-along's setup installs kokoro-onnx to run it, about 1 GB with the model. Kokoro also speaks British English, Spanish, French, Hindi, Italian, Portuguese, Chinese and Japanese (its voices), and read-along switches voice by itself for a sentence in another script.
It's a recommendation, not a requirement. Speakup works with any voice read-along can use: another Kokoro voice, your system's built-in voices, or Pocket TTS and a voice of your own. If Kokoro-82M isn't installed, read-along falls back to the best voice the machine has. See Swapping the voice.
ffplay (comes with ffmpeg) or SoX's play.The easiest way is to let Claude do it. Paste this at the Claude Code prompt:
Install the speakup mod by following https://github.com/tim-rayner/mods/blob/main/speakup/README.md (the "Agent setup" section).
Claude asks before it installs anything. Or do it yourself, at the prompt:
/plugin install read-along --marketplace tomfluff/claude-read-along
/read-along setup
/plugin install speakup --marketplace tim-rayner/mods
Answer y to add each marketplace, then pick the user scope. /read-along setup walks you through installing Kokoro-82M, the recommended voice model (you can decline it and use your system's voices). Then open a new session and type /voice.
Steps for an agent installing speakup for someone. Run them in order; each is safe to re-run. Ask the person before steps that install software (2 and 3), saying what and how big.
python3 and claude on the PATH. Kokoro's packages need Python 3.10 to 3.13, or uv. claude plugin marketplace add tomfluff/claude-read-along
claude plugin install read-along@claude-read-along
setup.py line and read-along uses the machine's own voices. read-along's setup downloads the model, checks it against its SHA-256, and installs its runtime in a folder of its own: RA="$(ls -d ~/.claude/plugins/cache/claude-read-along/read-along/*/ | sort -V | tail -1 | sed 's#/$##')"
python3 -I "$RA/bin/setup.py" kokoro # a few minutes: run it in the background and wait
python3 -I "$RA/bin/speakd.py" --doctor # "[x] Kokoro" and a Player line mean it's ready
If the doctor says there's no player: on macOS brew install ffmpeg; on Debian, Ubuntu or WSL sudo apt install ffmpeg (ask before using sudo). If setup.py says the Python is too new or old, install uv (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh) and run it again. read-along's /read-along setup skill covers anything else.
claude plugin marketplace add tim-rayner/mods
claude plugin install speakup@tim-mods
/reload-plugins in this one) and type /voice. They should hear "Voice on" in the aoede voice (or a system voice, without Kokoro), then each reply read aloud.There's nothing to configure by hand: speakup sets read-along up itself (Settings).
Type /voice to turn reading on for this session; Claude says "Voice on", and from then on each reply is read aloud when it's done. Type it again to stop. Next session, it's off again.
| Command | |
|---|---|
/voice | toggle reading replies aloud for this session |
/voice on, /voice off | set it either way (off also stops anything being read) |
/voice all | read every update Claude writes between tool calls too, not just the final reply |
/voice dictation … | pass through to Claude Code's built-in /voice (dictation), where your build has one |
/v … | shorthand for /read-along … |
/v pause, /v stop | pause and resume, or stop |
/v read | read the last reply |
/v speed 1.2 | set the speed (0.5 to 4) |
/v voice kokoro:af_heart | change the voice for this session |
/v save | keep this session's voice and speed as the defaults |
/v | open read-along's settings |
read-along's buttons work as usual: hover a message to play it, and use the player above the prompt to pause, skip or change speed.
Kokoro-82M with af_aoede is the recommended default, not a requirement. Speakup sets the voice once, on its first session, so whatever you choose afterwards stays, and /voice works the same with all of them.
| Voice | Example | Setup |
|---|---|---|
| Another Kokoro voice | kokoro:af_heart, kokoro:bf_emma, kokoro:am_michael | none: about 50 come with Kokoro-82M (--japanese at setup adds Japanese) |
| macOS voices | mac:Samantha, mac:Ava (Premium) | built in; Enhanced and Premium voices are free in System Settings → Accessibility → Spoken Content |
| Windows voices | win:Microsoft Aria | built in, on WSL |
| eSpeak NG | espeak:en-us | sudo apt install espeak-ng; robotic, but about 100 languages |
| Pocket TTS | pocket:alba, or pocket:me for your own voice | python3 -I "$RA/bin/setup.py" pocket (about 1.3 GB) |
Try one with /v voice <voice>, list what's installed with /v voices, and keep it with /v save. The system voices need no download, so skip Kokoro-82M at install if 1 GB is too much. Pocket TTS can clone a voice from a short recording: see read-along's Pocket TTS notes, and only clone your own voice or one whose owner agreed.
To change the default for everyone you share speakup with, fork this repository and change VOICE and SPEED at the top of hooks/register.tsx.
Speakup has no settings of its own. It sets three of read-along's, in /config under read-along:
| Setting | Speakup sets | When |
|---|---|---|
| Auto-read Claude's replies | off | at the start of every session, so only /voice turns it on |
| Voices | main=kokoro:af_aoede, auto | once, on its first session |
| Speed | 0.9 | once, on its first session |
/voice changes auto-read for the current session only, the way /read-along auto replies final would. Everything else is read-along's: see its settings.
Speakup sends nothing over the network. It talks only to read-along's speech service, over a Unix socket on your machine, and to Claude Code's own settings. Kokoro-82M runs locally; the network is used once, by read-along's setup, to fetch the model and its Python packages. See read-along's privacy notes.
/voice didn't answer "Voice on" or "Voice off"? The session started before speakup was installed or updated: run /reload-plugins./voice? Speakup isn't loaded. Check claude plugin list shows speakup@tim-mods enabled, then open a new session./read-along restart./read-along doctor, then look at the service's log: /tmp/read-along-<your uid>.log./voice; use /voice dictation for Claude Code's own.flowchart LR
start(["new session"]) -- "auto-read off" --> ra
voice["/voice"] --> mod["speakup<br/>(in Claude Code)"]
mod -- "auto-read on or off" --> ra["read-along<br/>(in Claude Code)"]
mod -. "Voice on" .-> service
ra -- "each reply" --> service["speech service<br/>(on your machine)"]
service --> kokoro["Kokoro-82M, af_aoede<br/>(recommended; swappable)"] --> speakers(["your speakers"])
hooks/register.tsx is all of speakup. At session start it registers /voice and /v and turns read-along's auto-read off. /voice runs /read-along auto replies final (or off, then stop), and on the way on sends "Voice on" to read-along's speech service in the same voice. /v hands its arguments to /read-along.
Speakup is built on read-along by Yotam. read-along does all of the speaking: the speech service, the Kokoro setup, the voices, the highlight and the player. Speakup only decides when it speaks, and wouldn't exist without it. Speakup also follows read-along's own conventions to talk to its speech service (the socket, the /speak request, and how the Voices setting is read), and this README follows the structure of read-along's. read-along is MIT licensed, © Yotam; speakup bundles none of its code, and you install it from its own repository.
The recommended voice model is Kokoro-82M by hexgrad (Apache-2.0), run by kokoro-onnx (MIT), both installed by read-along's setup.
Speakup itself is MIT licensed; see LICENSE.
hooks/register.tsx 121 lines1// speakup: built on read-along by Yotam (https://github.com/tomfluff/claude-read-along, MIT).
2import { atom, read, update } from 'claude-code'
3import type { ConfigRow, EngineInterface, Register } from 'claude-code'
4
5const voiceOn = atom({ plugin: 'speakup', key: 'voiceOn' } as const, false)
6
7/** Runs `/read-along <args>` once the calling command has returned (a command.run hook can't await another command: it would wait on itself), then `after`. */
8const readAlong = ($: EngineInterface, runs: string[], after?: () => Promise<unknown>) =>
9 $.clock.after(0, async () => {
10 for (const args of runs) await $.command.run({ command: 'read-along', args })
11 await after?.()
12 })
13
14/** The voice speakup sets up: Kokoro's af_aoede, at 0.9×. */
15export const VOICE = 'main=kokoro:af_aoede, auto'
16export const SPEED = 0.9
17
18/** read-along's `/config` row for one of its options. */
19const readAlongRow = (rows: readonly ConfigRow[], field: string) =>
20 rows.find(r => r.provider.plugin === 'read-along' && r.key.endsWith(`.${field}`))
21
22/**
23 * Keeps read-along's auto-read off at the start of every session (/voice turns it on),
24 * and gives it speakup's voice and speed once, the first time both are there to set.
25 */
26async function setUpReadAlong($: EngineInterface) {
27 const rows = await $.config.list()
28 const set = async (field: string, value: string | number) => {
29 const row = readAlongRow(rows, field)
30 if (row && row.value !== value) await $.config.set({ key: row.key, value })
31 }
32 await set('autoReadReplies', 'off')
33 if (!(await $.store.get('voiceApplied')) && readAlongRow(rows, 'voices')) {
34 await set('voices', VOICE)
35 await set('speed', SPEED)
36 await $.store.set('voiceApplied', true)
37 }
38}
39
40/** read-along's `voices` option, `main=aoede, auto; ja=…`, as the first voice of each group: adapted from read-along's `parseVoices` (MIT, © Yotam). */
41const firstVoices = (option: string) => {
42 const out: Record<string, string> = {}
43 for (const part of option.split(';')) {
44 const m = part.match(/^\s*([a-z]{2,4})\s*=(.*)$/)
45 const first = (m?.[2] ?? part).split(',').map(v => v.trim()).find(Boolean)
46 if (first) out[m?.[1] ?? 'main'] = first
47 }
48 return out
49}
50
51/**
52 * Says `text` in read-along's voice, through its speech service (the socket its
53 * mod starts at session start); in the system voice where that service can't be reached.
54 */
55async function sayAloud($: EngineInterface, text: string) {
56 try {
57 const rows = await $.config.list()
58 const option = (field: string) => readAlongRow(rows, field)?.value
59 const voices = option('voices')
60 const speed = Number(option('speed'))
61 const volume = Number(option('volume'))
62 const res = await $.http.fetch('http://speakd/speak', {
63 method: 'POST',
64 headers: { 'Content-Type': 'application/json' },
65 body: JSON.stringify({
66 text,
67 key: 'voice-mod',
68 label: 'voice',
69 mode: 'now',
70 voices: typeof voices === 'string' ? firstVoices(voices) : { main: 'auto' },
71 speed: Number.isFinite(speed) && speed > 0 ? speed : 1,
72 volume: Number.isFinite(volume) ? volume / 100 : 1,
73 }),
74 socketPath: `/tmp/read-along-${(await $.env.get('USER')) ?? 'user'}.sock`,
75 })
76 if (!res.ok) throw new Error(`speech service answered ${res.status}`)
77 } catch {
78 await $.audio.speak(text).catch(() => {})
79 }
80}
81
82export const register: Register = on => {
83 on('session.start', async ($, e, next) => {
84 await $.command.register({
85 name: 'v',
86 description: 'Alias for /read-along: read | stop | pause | speed N | …',
87 argumentHint: '[read|stop|pause|speed N|…]',
88 })
89 // Claude Code ships a /voice (dictation) in some builds; registering over a built-in is refused, but the hook below still answers it.
90 await $.command
91 .register({
92 name: 'voice',
93 description: 'Toggle read-along reading replies aloud (off at the start of each session)',
94 argumentHint: '[on|off|all|dictation …]',
95 })
96 .catch(() => {})
97 $.clock.after(0, () => setUpReadAlong($).catch(() => {})) // read-along may not be installed yet
98 return next(e)
99 })
100
101 // /v → /read-along, arguments and all.
102 on('command.run', { command: 'v' }, async ($, e) => {
103 readAlong($, [e.args])
104 return { text: '' }
105 })
106
107 // /voice toggles read-along's auto-read for this session, and says "Voice on" aloud when it turns on.
108 on('command.run', { command: 'voice' }, async ($, e, next) => {
109 const [verb = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
110 if (verb === 'dictation') return next({ ...e, args: rest.join(' ') }) // the built-in /voice, if this build has one
111
112 const turnOn = verb === 'on' || verb === 'all' ? true : verb === 'off' ? false : verb === '' ? !(await read($, voiceOn)) : undefined
113 if (turnOn === undefined) return { text: 'Usage: /voice [on|off|all|dictation …]' }
114
115 await update($, voiceOn, () => turnOn)
116 if (turnOn) readAlong($, [`auto replies ${verb === 'all' ? 'all' : 'final'}`], () => sayAloud($, 'Voice on'))
117 else readAlong($, ['auto replies off', 'stop'])
118 return { text: turnOn ? `Voice on: replies are read aloud${verb === 'all' ? ', every block as it streams' : ''}.` : 'Voice off.' }
119 })
120}
121types/index.d.ts 9 lines1declare module 'claude-code' {
2 interface PluginState {
3 'speakup': {
4 /** Whether read-along reads replies aloud this session; off until /voice. */
5 voiceOn: boolean
6 }
7 }
8}
9