Eyes-free text-to-speech for Claude Code on macOS — an accessibility tool for blind and low-vision developers.
Sonari reads Claude Code's output aloud — prose, plans, multiple-choice questions, and permission prompts — in order, plays a distinct sound the instant a decision needs you, and lets you answer and control the speech without looking. Run a full session with the screen off.
- Ordered narration — prose, plans, questions, and permissions are spoken in order, never out of sequence.
- Per-decision earcons — a distinct sound the moment a question, plan, permission, or error appears.
- Selection by number — answer prompts with the option's number; no key injection.
- Global hotkeys — stop, repeat, jump between sessions, jump-to-decision, where-am-I, re-read options, verbosity, rate — all work mid-speech. Catch-up and skip-the-pile ship unbound, ready to bind.
- Catch-up summarizes a session via your own logged-in coding-agent CLI (no separate API key). It draws from that subscription's usage — roughly 16–32k tokens a press, far cheaper on repeats within the hour — and falls back to a plain last-line digest when the summary is unavailable.
- Self-contained core — the speech engine runs on the macOS system Python; no pip, no third-party packages. (The optional Kokoro neural voice is a separate ~316 MB add-on — see Voices.)
Sonari runs entirely on your own Mac. It collects nothing, sends nothing over the network, and has no servers, telemetry, or analytics.
Sonari stores session text locally in ~/.sonari/state.json so unheard speech survives restarts. Nothing leaves your Mac. Uninstall preserves this file — delete ~/.sonari to remove everything. See PRIVACY.md.
- macOS (Sonari uses the built-in
sayandafplaycommands). - Python 3.9 or newer — macOS ships
/usr/bin/python3, which is enough. Sonari picks the bestpython3 >= 3.9it can find automatically. - Xcode Command Line Tools for global hotkeys —
xcode-select --install. (Speech works without them; only the hotkeys needswiftc.) - Claude Code 2.1.162 or newer.
Sonari installs from a Claude Code marketplace. You start hearing Claude as soon as the plugin is enabled; one more command turns on global hotkeys and autostart.
- Add the marketplace:
/plugin marketplace add nimkimi/sonari(or, in a shell,claude plugin marketplace add nimkimi/sonari). - Install the plugin:
/plugin install sonari@sonari(orclaude plugin install sonari@sonari). The marketplace is namedsonari, so the install target issonari@sonari. You will start hearing Claude immediately — the daemon lazy-starts on the first hook. - Run
/sonari:installfrom inside Claude Code to finish setup (each step is printed and spoken). Until you run it, every new session Sonari reminds you once: "Sonari is reading aloud. To enable hotkeys and autostart, run /sonari:install." - Run
/sonari:doctorto confirm everything is green (the only expected failure isswiftc/ Xcode Command Line Tools on a machine without them — speech still works; only the hotkeys need them).
For local development you can skip the marketplace and load the repo per session with
claude --plugin-dir <path-to-sonari>.
If you already have sonari on your PATH, the CLI equivalent of step 3 is:
sonari installsonari install resolves the best python3 >= 3.9, copies the runtime to
~/.sonari/app (so it survives plugin auto-updates), builds the hotkey
daemon, writes both LaunchAgents, and places the ~/.local/bin/sonari launcher.
After a plugin update, Sonari says once — "Sonari was updated. Run /sonari:install
to apply." — so you can refresh the copy.
Contributors can run the test suite from a venv:
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest -qThe public install path above does not use pip — the venv is for tests only.
You ask Claude to refactor a file and look away:
"I'll update the parser to handle nested blocks. First I'll read the current implementation…" — spoken as it streams, sentence by sentence.
♪ chime — the permission earcon, the instant Claude asks. "Edit parser.py. Press Control Command Return to approve."
You press ⌃⌘Return without looking up. The voice continues: "Done. The parser now handles nested blocks." ♪ ding — your turn.
A second session finishing in the background plays its own turn-done sound; press ⌃⌘J to jump the voice over to it, or ⌃⌘W anytime to hear where everything stands.
Every sound Sonari makes comes from one registry (src/sonari/cues.py). A
transient tone plays immediately and never stacks with another; a prelude
is bound to the sentence it decorates and plays with it as one unit.
| Sound | Plays as | When you hear it |
|---|---|---|
turn_done |
transient | A session finished its turn |
choice |
transient | A question with options is waiting |
plan |
transient | A plan is ready for review |
permission |
transient | A permission ask is waiting |
error |
transient | That press had nothing to act on |
error_misdirected |
transient | Valid answer, wrong session |
error_system |
transient | Sonari itself failed; the content is preserved unheard |
permission_expired |
transient | A permission ask timed out unanswered |
submit_ack |
transient | Your prompt was submitted (off by default) |
repoint |
transient | Your click moved the workspace to a different session |
pitch_up |
prelude | Rising chirp bound to the front of an approval |
pitch_down |
prelude | Falling chirp bound to the front of a denial |
callsign |
prelude | The asking session's spoken label, bound to its own utterance |
crossing |
prelude | A neutral marker where the voice crossed to a session whose call-sign is not cached yet |
speech |
queued | Spoken readout of session output |
summary_voice |
queued | The catch-up summary's island voice |
alarm_daemon_down |
alarm | hotkeyd lost the daemon and sounds the alarm itself |
alarm_hotkeys_down |
alarm | The daemon lost hotkeyd and sounds the alarm itself |
Start with ⌃⌘W — it's home base. It speaks a terse status of every session you have running: what the voice is on, what's piled up behind it, and where your hands are focused right now. Press it whenever you lose track; everything below is easier once you know you can always ask.
Default modifier is Ctrl+Cmd (rebindable via ~/.sonari/keymap.json). A tiny Swift
helper registers these with Carbon RegisterEventHotKey, so no macOS accessibility
permission is needed.
| Hotkey | Effect |
|---|---|
| Ctrl+Cmd+→ | Step forward one item in the current turn |
| Ctrl+Cmd+← | Step back one item in the current turn |
| Ctrl+Cmd+↑ | Jump back one whole reply |
| Ctrl+Cmd+↓ | Jump forward one whole reply |
| Ctrl+Cmd+S | Stop/resume the current session's voice |
| Ctrl+Cmd+M | Stop every session's voice |
| Ctrl+Cmd+J | Move the voice to a background session that is waiting |
| Ctrl+Cmd+D | Jump to the pending decision |
| Ctrl+Cmd+R | Re-speak the last utterance |
| Ctrl+Cmd+Tab | Browse sessions forward (hold chord, tap Tab) |
| Ctrl+Cmd+Shift+Tab | Browse sessions backward |
| Ctrl+Cmd+W | Speak a terse status of all sessions |
| Ctrl+Cmd+Return | Approve the pending permission request |
| Ctrl+Cmd+Esc | Deny the pending permission request |
| Ctrl+Cmd+= | Speak faster |
| Ctrl+Cmd+- | Speak slower |
| Ctrl+Cmd+O | Re-speak the pending question's options |
| Ctrl+Cmd+V | Cycle verbosity: everything / medium / quiet |
Available but unbound by default (bind via ~/.sonari/keymap.json):
| Action | Effect | Suggested binding |
|---|---|---|
skip_pile |
Settle the unheard backlog without hearing it | Ctrl+Cmd+Shift+↓ |
catch_up |
Hear a summary of the unheard backlog | Ctrl+Cmd+L |
learn_mode |
Toggle learn mode: keys speak what they do instead of doing it | — |
query_actions |
Speak the actions available right now | — |
When a question, permission prompt, or plan (AskUserQuestion / permission /
ExitPlanMode) appears, choose an option by pressing its number (1-9), or Esc to
cancel — using Claude Code's native numeric selection, no key injection. For a
multi-select question, press each option's number (or Space on the highlighted item),
then Enter to confirm. If a question has more than nine options, numbers cover 1-9;
use the arrow keys plus Enter for the tenth and beyond. Sonari speaks these cues when
they apply.
Digits answer the prompt on screen; digits while holding the chooser chord (⌃⌘Tab) switch sessions instead — two different digit meanings, never active at once.
The voice only ever follows one session at a time — the speaker. Run several sessions side by side and the rest keep going in the background: their prose and decisions pile up silently in their own queue (nothing is lost), and each one plays a short earcon the moment it finishes a turn or needs you, so you know something happened without hearing it live.
Press ⌃⌘J to jump the voice straight to whichever background session is waiting, or hold ⌃⌘Tab and tap through the chooser to browse the fleet by number. Left alone, Sonari's keep-going behavior also advances the voice to the longest-waiting background session once the current one runs out of queued speech — closed sessions are skipped; their backlog stays discoverable via ⌃⌘W and readable on purpose, via catch-up (⌃⌘L) or by stepping through the transcript (⌃⌘←/→).
Wherever you answer a question or approve a permission is simply whatever terminal you're typing in — your response always goes there, whether or not that session currently has the voice.
Sonari defaults to the best enhanced/neural English voice it can find and falls back to Samantha.
Enhanced voices sound dramatically better and are free and offline. To install one:
- Open System Settings → Accessibility → Spoken Content.
- Click System Voice → Manage Voices….
- Pick an English voice marked (Enhanced) or (Premium) — e.g. Ava (Premium), Zoe (Premium), or Allison — and download it.
- Run
sonari doctorto confirm Sonari picks it up, or pin it explicitly:
sonari voice "Ava (Premium)"/sonari:voices provisions the optional Kokoro neural voice — a ~316 MB download that
installs its own Python runtime via uv/pip. Quality is comparable to Apple's Premium voices;
try those (free, no download tooling) first.
Sonari's setup and speech-control commands are also available as sonari CLI subcommands
and, where it makes sense inside a session, as namespaced slash commands.
| Slash command | CLI | Effect |
|---|---|---|
/sonari:doctor |
sonari doctor |
Run Sonari health checks (TTS, voice, daemon, hooks, hotkeys) |
/sonari:install |
sonari install |
One-time Sonari setup — autostart, global hotkeys, control CLI |
/sonari:keepalive |
sonari keepalive |
Toggle the Bluetooth keep-alive (holds the audio device open while sessions are live; fixes clipped speech on Bluetooth headsets) |
/sonari:keymap |
sonari keymap |
List Sonari hotkey bindings (incl. unbound); ' clear' to unbind |
/sonari:minqueue |
sonari minqueue |
Set how many items Sonari batches before reading (1 = read immediately) |
/sonari:rate |
sonari rate |
Set Sonari speech rate in words per minute |
| — | sonari skip |
skip the current item |
/sonari:status |
sonari status |
Show Sonari speech daemon status (verbosity, rate, voice, queue) |
| — | sonari stop |
stop all speech and clear the queue |
/sonari:uninstall |
sonari uninstall |
Remove Sonari's autostart, hotkey helper, launcher, and app copy |
/sonari:verbosity |
sonari verbosity |
Set Sonari verbosity (everything | medium | quiet) |
/sonari:voice |
sonari voice |
Set the Sonari say voice (omit the name to list installed voices) |
/sonari:voices |
sonari voices |
Install or remove Sonari neural (Kokoro) voices |
Three live-switchable levels (earcons fire in all of them):
- everything (default) — prose narration, questions, plans, permissions, and brief tool announcements (a short summary of what's running, e.g. "Running git status").
- medium — prose narration plus decisions (questions / plans / permissions); drops routine tool announcements.
- quiet — decisions only (questions / plans / permissions); drops both tool announcements and prose narration. Earcons still fire at every level.
Sonari's voice never jumps ahead of you. Spoken content is strictly first-in, first-out: a question, plan, or permission is voiced in its natural place — after the prose that explains it — so if the voice is mid-sentence when a permission appears, you still hear the remaining sentences first, then the permission. What is instant is the alert: the moment any decision appears, a short distinct earcon plays immediately (a different sound for permission, choice, plan, error, turn-done), while the spoken detail waits its turn in the queue. Claude Code blocks on the prompt until you respond, so hearing the context first costs nothing. "Higher priority" therefore means "alert you instantly with a sound," never "speak it out of order."
Run sonari doctor first — it reports each check as pass/fail. Common issues:
- No speech at all. Confirm
sonari statusshows your session as the foreground. The daemon starts lazily on the first hook; if the socket is unreachable, runsonari installto (re)load the daemon (sonari doctortells you whether the socket is reachable), or check~/.sonari/speechd.log. - Robotic voice. No enhanced voice is installed; see Voices above.
- Hooks not firing. Re-enable
sonarivia/plugin(or re-launch withclaude --plugin-dir /path/to/sonari), then runsonari doctorand confirm theplugin hooks.jsoncheck passes. - Speech too fast/slow.
sonari rate 180(default is 200 wpm). - Too chatty.
sonari verbosity mediumorsonari verbosity quiet. - Everything is stuck.
sonari stopclears the queue and cancels the current utterance.
State, config, the socket, and logs all live under ~/.sonari/
(config.json, speechd.sock, speechd.log).
Disable the sonari plugin via /plugin (or stop passing --plugin-dir), then run
sonari uninstall (or, from inside a session, /sonari:uninstall).
This removes the LaunchAgents, the hotkey helper, the ~/.local/bin/sonari launcher, and
the stable app copy at ~/.sonari/app. It preserves config.json, keymap.json, and
state.json (your session text) — delete ~/.sonari to remove everything.
MIT — see LICENSE.