npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

claude-tts

v1.3.0

Published

Speak Claude Code's responses aloud. Reads what you select, only from Claude Code. Zero dependencies, works on macOS, Windows and Linux.

Readme

claude-tts

Speak Claude Code's responses aloud. Zero dependencies, free, works on macOS, Windows and Linux.

npx claude-tts init

That's it. Claude Code will speak from your next response.


Why

Long responses are easy to lose track of when you're tabbed away. claude-tts reads the opening of each reply out loud so you know when Claude has finished and roughly what it said, without watching the terminal.

It runs as a Claude Code Stop hook: after every response, the hook reads the transcript, strips the Markdown, and speaks the result.

Install

npx claude-tts                         # interactive setup — does everything
npx claude-tts --yes                   # same, no prompts

Bare npx claude-tts fetches, installs, registers the hooks, and offers to add the status-line indicator. It also drops a claude-tts launcher on your PATH (~/.local/bin or similar), so the command keeps working after npx discards the package. If no writable PATH directory exists it says so and prints npx claude-tts … commands instead.

For finer control:

npx claude-tts init                    # sensible defaults
npx claude-tts init --dry-run          # show what would change, write nothing
npx claude-tts init --mode sentence    # speak only the opening sentence
npx claude-tts init --engine edge      # neural voices (see below)

Restart any running Claude Code session afterwards so it picks up the hook.

Engines

| Engine | Quality | Cost | Network | Setup | |---|---|---|---|---| | native | Robotic but clear | Free | None | Built in on macOS/Windows | | edge | Neural, very good | Free | Sends text to Microsoft | pipx install edge-tts |

native resolves per platform:

  • macOSsay, preinstalled
  • Windows — SAPI via System.Speech, preinstalled
  • Linuxspd-say, espeak-ng, espeak or festival, whichever is found

Linux is the one platform with no guaranteed synthesizer. If none is present, init tells you the install command for your distro.

A note on edge

The edge engine uses Microsoft Edge's read-aloud voices. They're free, need no API key, and sound dramatically better than the built-in ones.

It sends your response text to Microsoft's servers. That's why native is the default and edge is opt-in — if Claude is discussing proprietary code, this is your call to make deliberately, not a default you inherited.

It also depends on the community edge-tts CLI rather than reimplementing the protocol, because Microsoft gates the endpoint behind rotating tokens that break hand-rolled clients.

Usage

claude-tts ui                  # live control panel (see below)
claude-tts status              # configuration and health
claude-tts test                # speak a sample
claude-tts test --dry          # show what would be spoken, no audio
claude-tts off                 # mute (instant, doesn't touch settings.json)
claude-tts on                  # unmute
claude-tts last                # speak the most recent response
claude-tts clip                # speak what you just selected
claude-tts watch               # speak every selection (on by default)
claude-tts save                # keep a passage in a file
claude-tts stop                # interrupt the current utterance
claude-tts doctor              # diagnose problems
claude-tts uninstall           # remove the hook

Speaking only what you want

By default, responses are not spoken at all — you hear what you select. The clipboard watcher is the primary path; auto-reading every response gets tiring fast.

To change that:

claude-tts config trigger manual    # never read responses    (default)
claude-tts config trigger marker    # only prompts containing !say
claude-tts config trigger always    # read every response

marker — opt in per prompt

Put the marker anywhere in your message and only that turn gets spoken:

explain what this regex does !say

Nothing else is read aloud. Change the marker to whatever you like:

claude-tts config triggerMarker "@read"

The hook finds your prompt by walking back from the reply to the nearest user message — past all the tool calls in between — and ignores harness-injected text like attachment notices and system reminders, so the marker is matched against what you actually typed.

manual — ask for it after the fact

claude-tts last           # speak the most recent response
claude-tts last --full    # all of it, not just the opening
claude-tts last --dry     # print what would be spoken

last finds the most recently written transcript across all your projects, so it always targets the session you were just in. It ignores the mute flag — you asked for it explicitly.

Inside Claude Code, run it without leaving the session: !claude-tts last.

Speak exactly what you highlight

Selecting text in a terminal copies it, so the clipboard is the most precise "read this" there is — no markers, no config:

This is on by default. A SessionStart hook brings the watcher up with every Claude Code session, so selecting text just works. One watcher serves every session — a second window finds the first still running.

claude-tts clip            # speak the current selection, once
claude-tts watch           # start the watcher by hand
claude-tts watch stop      # stop it
claude-tts watch status    # is it running?
claude-tts config autoWatch false   # do not start it automatically

Highlight a paragraph, hear it; highlight the next, the previous is interrupted. claude-tts watch --fg runs it in the foreground for debugging.

It only reads Claude's own prose

A watcher that read the whole clipboard would happily announce a password copied from a manager in another window. So a selection is spoken only if it actually appears in a recent Claude Code transcript:

claude-tts config watchScope claude   # only Claude's prose   (default)
claude-tts config watchScope any      # anything you copy

Commands and code are never spoken, even when Claude wrote them. Copying in a terminal is overwhelmingly copying a command or a path in order to paste it somewhere, and hearing a command spelled out loud is noise. Fenced blocks, indented output and anything that reads as a command line rather than a sentence are excluded, so the two things you do in the same window stay distinct: select an explanation and hear it, copy a command and get silence.

If a selection is not read aloud, ask why:

claude-tts clip --check

It reports the watcher state, engine, selection length and the exact reason — usually that the selection was a command rather than prose, or too short to attribute (under 8 letters).

Rather than guessing at the focused application — unreliable, and different on every platform — this compares the copied text against what Claude wrote. Comparison is on alphanumerics only, because the transcript stores raw Markdown (spoken **whole**) while the terminal copies it rendered, and selections pick up wrapping, indentation and ANSI codes.

Scoring is line by line rather than whole-string. A terminal selection is mixed — a whole answer carries prose, code blocks and output together — so demanding an exact match on the entire selection would reject it. Half the prose lines must be attributable for a selection to be spoken, which a copied command never manages and a selected answer comfortably does.

Transcripts reach several megabytes, so the haystack is built incrementally — each poll parses only newly appended bytes. A cold build plus five lookups is ~7ms; 200 cached lookups are ~1ms.

A selection is spoken wholemode is not applied, since you already chose the extent yourself. Markdown is still stripped, so code blocks are not read out. Both clip and last speak even while muted, because you asked directly.

Requires a clipboard reader: built in on macOS (pbpaste) and Windows (Get-Clipboard); on Linux install xclip, xsel, or wl-clipboard.

On Windows the polling happens inside one long-lived PowerShell process, which reports only changes. Starting a PowerShell costs a couple of hundred milliseconds, so a 400ms poll implemented as one spawn per tick would keep the machine launching PowerShell more or less continuously.

Save a passage instead of only hearing it

claude-tts save              # append the current selection
claude-tts save --last       # append the most recent response
claude-tts save --to notes.md
claude-tts save --show       # print the destination

Appends timestamped Markdown, so the file reads as a running log. The default lives at ~/.claude/claude-tts/saved.md; change it with claude-tts config saveFile <path>.

Or filter by content

Works in any trigger mode:

claude-tts config skipPatterns "^(done|ok|fixed)\b"

Settings editor

claude-tts ui

Every setting in one place, navigable with arrow keys — nothing to memorise:

  claude-tts                            ▬ armed
  ──────────────────────────────────────────────

  ▸ speech          on
    read responses  manual
    voice           Samantha en_US
    mode            paragraph
    rate            1.0×
    max chars       500
    watcher         running
    watch scope     claude
    auto-start      yes
    status icon     block
    debug log       off

  ──────────────────────────────────────────────
  master switch

  ↑↓ move   ←→ change   t test   s stop   q quit

| Key | Action | |---|---| | | Move between settings | | | Change the selected setting | | t | Speak a test line | | s | Stop speaking | | q | Quit |

Changing the voice plays a sample immediately — the only way to judge one. Changing the rate does too. The watcher can be started and stopped from here.

Changes save as you make them and apply to the next utterance. If you would rather edit the file:

claude-tts config --edit    # opens config.json in $EDITOR

Status line indicator

Show speech state inside Claude Code itself:

claude-tts statusline install     # wraps whatever you already have
claude-tts statusline preview     # see the segment
claude-tts statusline remove      # restore your original line

A single block glyph, one column wide, so it reads as part of the status line rather than a pasted-in icon:

| State | Glyph | |---|---| | Armed (watcher running) | white | | Idle (no watcher) | grey | | Speaking | a block that fills ▁▂▃▄▅▆▇█ as the utterance plays | | Muted | red |

How the speaking meter moves

Claude Code re-runs the status line on events, not on a timer — measured gaps between repaints ranged from 0.5s to 21s. A clock-driven animation therefore either never moves or jumps to a random frame.

So the block's height tracks progress through the utterance instead. Every repaint that does happen shows it higher, and the glyph carries real information rather than decoration:

▁ ▂ ▃ ▅ ▆ ▬        (sampled every 500ms across a 3s utterance)

Duration is estimated from word count and rate, since no engine reports progress. It only has to be roughly right — the alternative is no indication at all. Motion is as smooth as the host repaints, which is not very; for a genuinely live display run claude-tts ui, which owns its own render loop.

claude-tts config statusStyle emoji   # 🔈 👂 🔊 🔇 instead
claude-tts config statusStyle text    # words
claude-tts config statusStyle off     # nothing

Claude Code allows only one statusLine command, so this composes rather than replaces. It runs your existing command — MarkR, starship, anything — feeds it the same session payload, and attaches its segment to the first line.

Why a single emoji on the first line

Both halves of that are the result of real failures, not taste.

Status lines that fill the terminal ignore the width advertised in the payload — MarkR produces byte-identical output whether you tell it 60 columns or 140. A wider segment therefore gets pushed onto a second line or truncated away entirely. Two columns survive regardless of what the wrapped command does.

And the segment attaches to the first line because trailing lines are the ones that get truncated or scrolled out of view.

When prefixing, the wrapped line's own leading indent is collapsed to statusGap spaces — status lines often indent or right-align their first segment, which would otherwise leave a wide gulf between the icon and the content.

The width reservation is still applied, since well-behaved status lines do respect it — it just can't be relied on alone.

claude-tts config statusStyle text      # emoji | text | off
claude-tts config statusPosition end    # start | end of the first line
claude-tts config statusGap 0           # tighten the gap to the next segment

remove recovers the original command by parsing it back out of the wrapper.

Toggling is a command, since a status line cannot take input:

claude-tts toggle

Inside Claude Code, run it without leaving the session by typing !claude-tts toggle.

Just ask Claude

init writes a short block into ~/.claude/CLAUDE.md, which Claude Code loads every session. That means you can change things conversationally instead of remembering flags:

"mute the speech" → Claude runs claude-tts off "read that again" → claude-tts last "read what I copied" → claude-tts clip "only read when I ask" → claude-tts config trigger marker

The block sits between <!-- claude-tts:start --> and <!-- claude-tts:end -->, so it is rewritten cleanly on upgrade and removed on uninstall. Anything else in your CLAUDE.md is left untouched — there are tests asserting a byte-for-byte round trip.

Skip it with claude-tts init --no-memory.

Configuration

claude-tts config                        # show everything
claude-tts config mode full              # set a value
claude-tts config --reset                # back to defaults

| Key | Default | Meaning | |---|---|---| | enabled | true | Master switch, flipped by on / off | | engine | auto | auto | native | edge | none | | mode | paragraph | How much to read: full | paragraph | sentence | | maxChars | 500 | Hard ceiling on spoken characters | | minChars | 12 | Responses shorter than this are skipped | | rate | 1.0 | Speed multiplier, 0.53.0 | | voice | null | Engine-specific voice id | | interrupt | true | Stop the previous utterance before starting | | skipPatterns | [] | Regexes; matching responses are never spoken | | trigger | manual | Read responses: manual | marker | always | | triggerMarker | !say | Text that opts a prompt in when trigger is marker | | autoWatch | true | Start the clipboard watcher with each session | | watchScope | claude | Which selections to read: claude | any | | saveFile | null | Where save appends; null uses the default | | statusStyle | block | Status line: block | emoji | text | off | | statusGap | 1 | Spaces between the icon and the rest of the line | | statusPosition | start | Which end of the first status line it attaches to | | debug | false | Write a diagnostic log |

Picking a voice

claude-tts voices           # numbered list, your language first
claude-tts voices 12        # set by number
claude-tts voices samantha  # set by name — case and punctuation are ignored
claude-tts voices --pick    # hear each one, then choose
claude-tts voices --all     # every language

Two things make voice names hostile, and both are handled:

They are hard to type. macOS ships 177 voices, 110 of them parenthesised like Eddy (English (US)). So selection accepts a list number, a case-insensitive name, or a unique prefix — sam finds Samantha.

A wrong name fails silently. say -v NotARealVoice exits 0 and speaks in the default voice, so a typo gives you the wrong voice with no error. Every selection is therefore validated against the engine's list before being saved, and an unknown name is refused with suggestions.

Why paragraph is the default

Reading a full response takes minutes, and you can't skim audio. paragraph speaks the opening — enough to know what happened — and leaves the detail on screen. It skips past short leads and headings so you don't just hear "Here's the plan:".

Use skipPatterns to mute noisy categories, e.g. claude-tts config skipPatterns "^(done|ok)\b".

How it works

Claude Code finishes a response
  └─ Stop hook: node ~/.claude/hooks/claude-tts/speak.mjs
       ├─ reads the transcript tail backwards → newest assistant text
       ├─ strips Markdown → selects by mode → truncates
       └─ spawns the synthesizer detached, records its PID, exits 0

Three design constraints drive the implementation:

The hook never blocks. Speech is spawned detached and the process exits immediately, so audio length has no effect on Claude Code's responsiveness.

The hook never fails loudly. Every error path exits 0 silently. A broken speech feature must not surface as a hook error on your response.

npx is the installer, not the runtime. init copies the runtime into ~/.claude/hooks/claude-tts/ and registers that path. Wiring the hook to npx claude-tts speak would add package resolution — and possibly a network round trip — to every single response.

Speaking the right message

Claude Code buffers transcript writes, so when the Stop hook fires the newest line on disk is frequently still the previous turn's message — its timestamp field is creation time, not flush time. Reading immediately speaks the wrong response.

The hook therefore identifies messages by uuid and remembers the last one it spoke, per session, in ~/.claude/claude-tts/spoken.json. If the newest message on disk is one it has already spoken, it polls briefly (up to ~1.9s, well inside the 10s hook timeout) for a genuinely new one, and skips rather than repeating if none arrives.

Interruption

Speech is stopped by PID, recorded in ~/.claude/claude-tts/speaking.pid, not by process name. pkill say has no Windows equivalent, and killing by image name there would take down your own PowerShell sessions. Records older than 15 minutes are ignored, since the OS recycles PIDs.

What it touches

| Path | Purpose | |---|---| | ~/.claude/hooks/claude-tts/ | Installed runtime | | ~/.claude/claude-tts/ | Config, pid files, log | | ~/.claude/settings.json | Entries under hooks.Stop and hooks.SessionStart | | ~/.claude/CLAUDE.md | One delimited block so Claude knows the commands |

init backs up settings.json before writing, refuses to proceed if it doesn't parse rather than overwriting it, and adds only its own hook. uninstall removes exactly that entry and leaves everything else alone.

Troubleshooting

Run claude-tts doctor first — it checks each stage and tells you what to fix.

Nothing is spoken. Restart Claude Code after init; the hook is read at session start.

Nothing is spoken, and the hook clearly ran. Hooks are spawned by Claude Code, not by your login shell, so they may get a minimal PATH or none at all. Engines therefore resolve binaries by absolute path (/usr/bin/say, %SystemRoot%\System32\...\powershell.exe, /usr/bin/espeak-ng) rather than trusting PATH. If you hit a synthesizer this misses, claude-tts config debug true will name it in the log.

"Stop hook registered, but points elsewhere". The registered command embeds an absolute path to the Node binary that was active at install time. Switching Node versions (nvm, Homebrew, a system upgrade) invalidates it. Re-run npx claude-tts init.

Diagnosing a specific response. Turn on the log:

claude-tts config debug true
tail -f ~/.claude/claude-tts/claude-tts.log

Linux: no synthesizer. sudo apt install espeak-ng (or your distro's equivalent).

Development

npm test          # 118 tests, no dependencies

Tests cover Markdown sanitizing, transcript parsing, config validation, and the settings.json merge — including that unrelated hooks and settings survive install and uninstall untouched.

License

MIT