You can make Claude Code read its replies aloud with a Stop hook: a short script that takes last_assistant_message from the hook's input and pipes it into macOS's say. It takes about 20 lines and ten minutes. It also breaks in a handful of predictable ways, which I'll get to.
The hook
Claude Code runs a Stop hook when the main agent finishes a reply. The hook gets JSON on stdin, and since recent versions that JSON includes last_assistant_message, the text of the reply. You don't need to parse the transcript file for it.
Save this as ~/.claude/hooks/speak.sh and make it executable (chmod +x):
#!/bin/bash
# Read Claude Code's last reply aloud (macOS). Needs jq (built into macOS 15 and later).
input=$(cat)
# Claude is continuing because of another Stop hook: stay quiet.
[ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ] && exit 0
# Background work still running: Claude replies again when it's done.
[ "$(jq '.background_tasks // [] | length' <<<"$input")" -gt 0 ] && exit 0
text=$(jq -r '.last_assistant_message // empty' <<<"$input")
[ -z "$text" ] && exit 0
# Drop code blocks, then the worst markdown: backticks, stars, underscores, headings, quotes, link targets.
text=$(printf '%s\n' "$text" \
| sed -e '/^```/,/^```/d' -e 's/\[\([^]]*\)\]([^)]*)/\1/g' -e 's/[`*_#>]//g' \
| head -c 600)
# One voice at a time: stop whatever is still talking.
pkill -x say 2>/dev/null
nohup say "$text" >/dev/null 2>&1 &
exit 0
Then register it in ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/speak.sh" }] }
]
}
}
Restart Claude Code, ask it something, and it talks. I tested this with Claude Code 2.1.282 on macOS: code blocks are skipped, and pressing Esc to interrupt keeps it quiet. It doesn't fire on /clear or /compact either.
Where it breaks
This is where it gets interesting. These are the problems, roughly in the order you'll hit them.
It reads markdown literally
The sed line above removes the worst of it, but agent replies are dense: file paths, snake_case names, "46/46", arrows, tables. say reads src/components/EmptyState.tsx character group by character group. A table becomes a list of pipes. You end up writing a markdown-to-speech converter, one regex at a time.
It reads the whole thing
The first 600 characters of a long reply are usually the least useful part: "I'll start by looking at…". What you want is the result, which is at the end. Cutting from the end is no better. What you actually want is a summary, and that needs a model.
Tabs talk over each other
With two sessions open, two replies finish close together and you hear both at once. The pkill line stops that, but now the second reply cuts off the first. A proper fix is a queue: one voice at a time, in the order replies arrived. That's a small daemon, not a hook.
You don't know which tab is talking
"All tests pass." Which project? The hook doesn't know which terminal tab it came from; it has no terminal of its own. You can prefix the reply with the folder name from cwd, which helps until two tabs work in the same folder.
It talks before the work is done
When Claude starts a long job in the background, it ends its turn with something like "Started the build." The Stop hook fires on that, so you hear "started", and then Claude wakes up and replies again when the job is done. The Stop input has a background_tasks array for this: it lists what's still running, so the script above stays quiet until it's empty and you only hear the real result.
Codex is almost the same, with one catch
Codex takes the same hook JSON as Claude Code, so the script works there too. But Codex hashes each hook and asks you to trust it again in /hooks whenever the script changes. Edit the regex, re-trust the hook.
The voice
say is clear, and after an hour it's tiring. That's the one thing the hook can't fix on its own.
Is it worth it?
For one tab and the odd reply: yes. It's free, it's yours, and you learn how Claude Code's hooks work.
For five or six tabs all day, each problem above needs real code: a summarizer, a queue, a way to know the tab, a better voice. That's why I built Pintail. If you'd rather keep it free and local, Aloud solves several of these too. The comparison has the rest.