Releases: Quigleybits/cctts
Release list
v0.1.2 — multilingual picker + voice resolver fix
cctts v0.1.2
Patch release. The full edge-tts voice catalogue — roughly 322 voices across 70+ languages — is now accessible through a three-stage interactive picker and a magic phrase. In 0.1.1 the voice system was limited to 9 hand-picked English voices; 0.1.2 opens the rest. This release also corrects cross-scope voice precedence so that "last command wins" holds across every terminal / project / global combination.
The centrepiece is /cctts:voice, which now walks through a three-stage picker: first you select a language (e.g. "French", "Japanese", "Welsh"), then a locale if the language has multiple regional variants (fr-FR vs fr-CA, for instance), and finally the voice itself from the short list of voices available for that locale. The picker assigns the chosen voice to the current terminal only — to set it globally or per-project, reach for the magic-phrase forms (cctts -g -voice <name> or cctts -p -voice <name>) once you know which voice you want. The picker never leaves you staring at 320+ names at once; at no stage does the list exceed a handful of options.
Alongside the picker, cctts -voice <name> lets you set a voice by name in one shot when you already know what you want. The usual scope flags apply: cctts -g -voice en-US-AndrewMultilingualNeural assigns it globally, cctts -p -voice ... assigns it to the current project. For quick audition before committing, cctts -test "hello from Andrew" -voice en-US-AndrewMultilingualNeural speaks the sample text through that voice without touching your current state. One argparse quirk to note: the test text must appear before the -voice flag — cctts -test "text" -voice <name> works, but -voice <name> -test "text" treats the voice name as the test string.
The voices.md catalogue that ships with the plugin grows from 9 to 20 slots, adding 11 new voices spanning French, Spanish, Japanese, German, Portuguese (Brazil), Chinese Mandarin, Arabic, and a second Welsh voice. Slots 10–20 are intentionally manual-only — the auto-voice pool (the 9 voices cctts randomly assigns on first install) stays at 9 by design; the new slots are there for users who want to reach further into the catalogue by choice, not on random assignment. A new cli/gen_all_voices.py script regenerates the full catalogue listing by querying edge-tts directly, useful if Microsoft adds or retires voices between cctts releases.
Cross-scope voice precedence (the resolver fix)
Picker and named voices are stored under a new voice_name field; numeric slot picks (cctts -3) still use voice_index. The two fields could conflict across scopes — and in early picker testing they did. cctts -g -voice X followed by cctts -3 in a terminal should speak slot 3 (per the documented terminal > project > global precedence and the "last command wins" intuition), but the early resolver merged the two fields independently, leaving the global voice_name to shadow the terminal voice_index.
The resolver in 0.1.2 treats (voice_name, voice_index) as a single unit per scope: it walks terminal → project → auto-voice → global, and the first scope that has either field set wins the pair. Broadcasts (-g -voice X, -p -voice X) now also clear subordinate-scope voice_index, symmetric with how slot broadcasts have always cleared subordinate voice_name. Same-scope writes clear the sibling field too — so each scope holds at most one of the two, and cctts -status is honest about what's actually set. Net effect: the precedence table in the README behaves exactly as documented, no surprises.
State-file compatibility is fully maintained. The schema gains an optional voice_name field at every scope layer (global, project, per-terminal), but it is nullable — existing 0.1.1 state files load cleanly with voice_name=null, and all lookup logic falls back to the previous voice_index path when voice_name is absent. Upgrading does not require a migration step or a state file wipe.
The features deferred from 0.1.0 — persistent warm worker, audio ducking, alt backends (ElevenLabs/Kokoro), cross-tool support (Codex/Cursor), schema v1 drop — remain deferred. None of them shipped in 0.1.2; they carry forward to 0.3+.
Upgrading
In any Claude Code session, run /plugin update cctts@quigleybits to pick up v0.1.2. Fully restart Claude Code afterward (close + reopen) — /reload-plugins alone is not sufficient to hot-swap hook code. No state migration is needed; your current voice, rate, and scope settings carry forward unchanged.
The new surface at a glance:
/cctts:voice→ three-stage language → locale → voice picker (replaces the flat 9-name list)cctts -voice <name>→ set voice by name in current terminal scopecctts -g -voice <name>/cctts -p -voice <name>→ set voice globally or per-projectcctts -test "text" -voice <name>→ audition a voice without changing state (text arg must precede-voice)voices.md→ 20-slot catalogue (was 9)- Cross-scope voice picks now follow
terminal > project > auto-voice > globalstrictly — last command wins.
v0.1.1 - unified project storage
cctts v0.1.1
Patch release. Project settings move into the central state file, and bare cctts -p becomes a project-scope toggle that matches bare cctts and cctts -g.
In 0.1.0 each repo could carry a .claude/cctts.json file with a voice, rate, pause, or chime preference scoped to that project. The design assumed those settings might be useful to share with collaborators — commit the file to the repo, your teammates inherit the same TTS preferences. In practice cctts is a personal Claude Code plugin and no one was using it that way; the file-per-repo layer was just paying complexity cost and producing one small UX wart: bare cctts toggled the terminal on or off, bare cctts -g toggled global on or off, but bare cctts -p printed the project config instead of toggling. Same prefix, different verb.
0.1.1 unifies project storage into ~/.claude/hooks/.cctts-state.json under a new projects map keyed by git root, and gives cctts -p the toggle semantics it always should have had. Bare cctts -p now flips state.projects[<git-root>].enabled between true and false — first call disables every terminal whose cwd resolves to this repo, second call re-enables it. The previous read-and-print behavior moves to cctts -p -status, which prints the project's current entry as JSON. Everything else — cctts -p -3, cctts -p %150, cctts -p -pause, cctts -p -clear, the broadcast that clears matching per-terminal overrides — behaves the same from the user's point of view; the only difference is that values land in the central state file instead of a per-repo JSON file.
Project key resolution is pure-Python — a walk-up looking for .git, falling back to the resolved cwd when no git is found. Git worktrees (where .git is a file containing a gitdir pointer, not a directory) resolve to the main repo's project key, so settings follow the repo rather than splitting per worktree. The walk runs on every hook fire that involves project-scope state, so it has to be cheap; no subprocess to git, no caching layer, just Path.exists() walking up parents. On Windows that's microseconds; the previous file-read-per-fire was a similar cost.
Lazy migration handles existing v0.1.0 project files invisibly. On the first hook fire (or first cctts -p invocation) in a directory that still has a .claude/cctts.json file, the contents get ingested into state.projects[<git-root>] and the file is deleted. The hook logs one line on a successful migration so post-upgrade verification is possible. Failure modes are conservative: a half-written file (mid-edit) is left alone; a garbage payload is deleted; valid content with mixed valid/invalid fields keeps the valid ones and drops the rest. Once a project entry lives in state, the migration short-circuits even if the user manually re-creates the file later — the state entry is authoritative.
One bug fix tagged along. _valid_voice_index in cli/state.py was accepting True as a valid voice index because Python booleans satisfy isinstance(int). The validator now excludes bools explicitly, matching the same guard _sanitize_sid_int_map already had for kill_seq counters. In practice the bug would have produced voice_index=1 (since True == 1) on a malformed state file — annoying rather than catastrophic, but worth fixing now that 0.1.1's _sanitize_project exposed it.
If you're upgrading from v0.1.0 and you had .claude/cctts.json files in your repos, you don't need to do anything — they'll migrate on the next hook fire in each project. If you want to inspect what migrated, cat ~/.claude/hooks/.cctts-state.json | jq .projects shows the full map after the upgrade settles.
Upgrading
In any Claude Code session, run /plugin update cctts@quigleybits to pick up v0.1.1 — /plugin install is for fresh installs and won't upgrade an already-installed plugin. The bare /plugin slash command opens the management UI as an alternative. After the upgrade, fully restart Claude Code (close + reopen, not /reload-plugins) so the new hook code registers. Migration of any leftover .claude/cctts.json files happens automatically on the next hook fire — no manual cleanup needed.
The CLI surface that changed:
cctts -p(no other flags) → toggle the project's enabled flag (was: print project config)cctts -p -status→ print the project's entry as JSON (new)cctts -p -clear→ remove the entry fromstate.projects(was: delete the.claude/cctts.jsonfile)- Every other
cctts -p ...form behaves the same from the user's POV
v0.1.0 - first public release
cctts v0.1.0
First public release. Speak assistant responses through your speakers, per terminal, with Microsoft edge-tts under the hood.
cctts is a Claude Code plugin that turns the assistant's responses into spoken audio. It exists because reading Claude's output while you're driving the keyboard with two other tools open is a tax on attention, and because hearing the assistant talk back lets you stay in flow on long planning sessions without your eyes locked to the terminal. The synthesis layer is Microsoft edge-tts, accessed over its public WebSocket endpoint with no API key required, so the cost of running the plugin is zero ongoing dollars and the audio quality is comparable to commercial neural TTS.
The first principle to know about cctts is that it is per-terminal by convention. Every flag that changes how speech behaves — picking a voice, changing rate or volume, pausing, resuming, toggling the Notification readback or the tool-call chime — defaults to scoping that change to the terminal you ran it in. If you have three Claude Code sessions open and you change the voice in one of them, only that session changes. The other two keep whatever voice they were using. This sounds obvious in description but it is genuinely the design decision that makes the plugin pleasant to live with. Use dash-g to broadcast a change globally, dash-p to write it as a per-project override, or omit both to scope to the current terminal alone. Precedence is fixed and easy to keep in your head: per-terminal beats project beats auto-voice beats global default.
Auto-voice is the feature that takes per-terminal scope and makes it concrete. On first contact, each terminal gets assigned a distinct slot from a curated pool of nine English voices spanning US and UK accents. The pool is deliberately small so that you can tell at a glance which terminal is speaking just by the voice, and the assignment hands out the lowest free slot — terminal one hears Aria, terminal two hears Ava, terminal three hears Jenny, and so on, cycling back to slot one only once all nine are in use. If you want to pin a specific terminal to a specific voice, the slash command cctts colon voice lets you browse and choose; the magic phrase cctts dash N (with N from one to nine) does the same in one keystroke. The voices themselves are edge-tts neural voices and they pronounce technical jargon, file paths, and command flags surprisingly well.
The control surface in the running terminal is built around a small vocabulary of magic phrases you type as a raw prompt — no slash, no LLM round-trip. Bare cctts on its own toggles speech on or off for this terminal. cctts dash pause pauses playback; cctts dash resume picks it back up from where it stopped; cctts dash skip jumps past the currently-playing block and continues the queue; cctts dash stop kills the current block and any queued blocks for this terminal only; cctts dash reset clears this terminal's voice, rate, volume and toggle overrides (add dash-g to clear every terminal). cctts dash notify toggles spoken readback of Notification events — both permission prompts and the idle "Claude is waiting for your input" nudge — and cctts dash chime toggles the PreToolUse tool-call chime and matching Stop chime — both are independent of prose, which always speaks when cctts is on. Rate and volume have their own sigils: cctts percent-N sets speech rate as a percent of original speed (percent-100 is one-times, percent-115 is the default, percent-200 is two-times), and cctts bang-N does the same for volume between bang-1 and bang-200. You can chain them — cctts dash 3 percent-150 bang-80 sets voice, rate, and volume for this terminal in one shot. In daily use the workhorse trio is pause, resume, and skip — they're the commands that respect your actual attention, letting you bring the audio back when you tune in and silence it when you tune out. cctts dash replay re-speaks the most recent block when you missed something, and cctts dash status prints the resolved configuration for the current terminal stacked against project and global so you can see exactly which setting is winning where.
Project overrides exist for the case where one repo benefits from different defaults than the rest of your work. A noisy build pipeline on a particular project might want the chime off; an audio-heavy app might want the assistant to default to faster speech. The dash-p form of any flag scopes the change to the project root and writes it to dot-claude slash cctts dot json, so opening Claude Code in that directory uses the project settings automatically and your other projects are unaffected. It's the same flag grammar as global, just narrower in reach. Project overrides also include a paused field — cctts dash p dash pause silences every terminal whose working directory is inside this repo, which is the right tool for projects whose output you'd rather not have read aloud at all.
Two underlying capabilities shape what the plugin can do and deserve a callout. The first is the F3 prewarm cache. Run cctts dash prewarm once after install and the plugin synthesizes the canned acknowledgments and chimes for every voice in the pool and stashes the resulting audio on disk. When the assistant says one of those phrases later, playback starts effectively instantly because the audio is already on disk. The prewarm takes about forty-five seconds, the footprint is a few megabytes, and it's safe to re-run — it skips entries that are already cached. The full speech cache builds up the same way during normal use, keyed on text, voice, rate, and volume, evicting LRU at one hundred megabytes or two hundred files. The second capability is the five-thousand-character page cap. When an assistant response exceeds five thousand characters, cctts speaks the first page, chimes, and announces "five thousand character limit reached, say cctts minus more to continue." Type cctts dash more and the next page resumes from the last clean sentence boundary before the cap — the cut is sentence-aware so you never get clipped mid-thought, and the pending continuation is per-terminal so two parallel sessions can each have their own tail waiting.
A few smaller things round out the picture. The slash commands are a discoverable companion to the magic phrases for when you don't remember the flag — cctts colon settings opens an interactive picker for voice, rate, toggle, and test; cctts colon voice opens the voice picker on its own; cctts colon test with no argument also opens the voice picker, while cctts colon test followed by a number plays a fixed test phrase in that voice, and cctts colon test followed by free text speaks that text at the current voice. cctts colon help is the reference card. One privacy note worth saying out loud: every spoken block leaves your machine to be synthesized by Microsoft's edge-tts endpoint, so don't enable cctts in sessions handling secrets or anything sensitive — the environment variable CLAUDE underscore TTS underscore DISABLED equals one is a hard kill switch for such sessions.
cctts ships under the MIT licence. Platform support is honest: Windows is first-class and tested across the release-prep cycle, while macOS and Linux code paths exist in the source but are flagged experimental until somebody runs the manual smoke tests on real hardware. If you're on Windows, install through the Claude Code marketplace, fully restart Claude Code so the UserPromptSubmit hook registers, let the assistant speak its first response, and you should be off to the races. If you're on macOS or Linux, the plugin will probably work, but please file issues with anything that misbehaves around audio paths or pause-resume timing — those reports are the bottleneck on flipping the platform notice to first-class for everyone.
Install via slash plugin marketplace add Quigleybits slash cctts followed by slash plugin install cctts at quigleybits, fully restart Claude Code (a soft reload won't pick up the hook), run cctts dash help for the command reference, and try cctts dash test quote hello quote to confirm audio is flowing. From there the assistant should start talking on the next response.