Projects March 16, 2026 4 min read

Give Claude Code a Voice with a Stop Hook

TL;DR

A small bash script that makes Claude Code speak its responses out loud. Pair it with voice dictation and you get a two-way conversation with your terminal.

  • Claude Code hooks let you run scripts when certain events fire — this one triggers after every response
  • The script reads the assistant's last message and pipes it through your system's text-to-speech engine
  • It piggybacks on Claude Code's built-in /voice toggle — one command enables both voice input and voice output
  • The result: a two-way voice conversation with your terminal
Erik Ros
Erik Ros Founder, Devilsberg

The Idea

Claude Code Terminal has a /voice setting now. Users can dictate their input, but Claude answers in written text. obviously not fun. So I've created a hook that will allow Claude to talk back using very basic text-to-speech (TTS)

It is not particularly useful, but a good bit of silly fun.

How Claude Code Hooks Work

Claude Code has a hooks system that lets you run shell commands in response to events. One of those events is Stop — it fires every time the assistant finishes a response. The hook receives the conversation context as JSON on stdin. The hook is fired without any token cost or API overhead.

That's all you need. A Stop hook that reads the last message and speaks it.

The Script

The full script is available as a GitHub Gist. Here's what it does, step by step.

Now, it can get quite annoying, so first, it checks whether voice mode is enabled. Claude Code's built-in /voice command writes a voiceEnabled flag to ~/.claude/settings.json. The hook reads that same flag, so /voice becomes a single toggle for both input and output — no extra configuration needed. If the flag is missing or false, the script exits silently.

SETTINGS="$HOME/.claude/settings.json"

if [[ ! -f "$SETTINGS" ]] || \
   ! jq -e '.voiceEnabled == true' "$SETTINGS" >/dev/null 2&1; then
  exit 0
fi

Next, it detects which TTS engine is available. Linux has spd-say (from speech-dispatcher), macOS has the built-in say command. If neither exists, it exits quietly.

if command -v spd-say &>/dev/null; then
  TTS=spd-say
elif command -v say &>/dev/null; then
  TTS=say
else
  exit 0
fi

The hook receives its input on stdin as JSON. The script extracts the assistant's last message using jq.

INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.last_assistant_message // ""')

Here's where it gets opinionated. Not every response should be spoken. Long responses are not conversational — they're reference material. Code blocks are useless as audio. The script skips messages over 300 characters and anything that starts with a code fence.

if [[ ${#MESSAGE} -gt 300 ]]; then
  exit 0
fi

if echo "$MESSAGE" | grep -qE '^\s*```'; then
  exit 0
fi

Finally, it strips markdown formatting — headings, bold, inline code, lists — and speaks the clean text. The TTS runs in the background so it doesn't block Claude Code from accepting the next input.

Setting It Up

Three steps. Save the script somewhere — for example ~/.claude/hooks/stop-speak.sh — make it executable, and register it in your settings:

{
  "hooks": {
    "Stop": [{
      "type": "command",
      "command": "~/.claude/hooks/stop-speak.sh"
    }]
  }
}

Then just run /voice in Claude Code. That enables both voice input (built-in) and voice output (the hook) with a single toggle.

On Linux you'll need jq and speech-dispatcher. On macOS, jq is the only dependency — say is built in.

The Experience

It's a bit silly. Let's be honest about that. But there's something oddly satisfying about dictating a command and hearing the response spoken back. I guess I am still yearning for a STAR TREK computer.

The 300-character limit is the key design choice. Short, conversational responses get spoken: confirmations, explanations, questions. Long responses with code output stay silent. This means the voice kicks in exactly when it's useful — during the back-and-forth of working through a problem — and stays out of the way during heavy output.

I find myself using it most when when I'm in the mood for speech input. after a few inputs, I get back into grind mode and switch to typing.

Limitations

System TTS voices are not going to win any awards. spd-say on Linux sounds like a robot from a 90s movie. macOS say is better but still unmistakably synthetic. If you want natural-sounding speech, you'd need to swap in a cloud TTS API — but that adds latency and cost and that's not taking it to far.

The markdown stripping is rough. It handles the common cases but will occasionally mangle unusual formatting. Good enough for spoken output, not perfect.

And of course, your coworkers might look at you funny when your terminal starts talking.

Why Bother

I built this because I wanted to get some experience with the hooks system in Claude Code. Most people use hooks for linting or auto-formatting. But the Stop event gives you access to every assistant response, and that opens up creative possibilities beyond code quality.

Voice output is one. You could also build notification hooks, logging hooks, hooks that trigger external workflows. The pattern is the same: listen for an event, extract the data, do something with it.

This particular hook makes your terminal talk. And sometimes, that's exactly what you need.