readback hooks into Claude Code via the Stop hook — a shell command that Claude Code fires every time an assistant turn finishes. The hook reads the most recent assistant text from the session transcript and pipes it through readback say for playback.
- You run
readback install-hookonce. This copieshooks/claude-code-stop.shinto~/.claude/hooks/readback-stop-hook.shand adds aStopentry to~/.claude/settings.json. - From inside an active Claude Code chat, you run
readback pin. This writes the current session id into~/.config/readback/sessions. - When Claude Code finishes responding, the hook fires for every session. It reads
~/.config/readback/sessionsand only speaks if the current session id is in there. All other sessions exit silently. - For pinned sessions, the hook walks the session's JSONL transcript in reverse, stops at the most recent user entry (end of current turn), collects text blocks from assistant entries in between (skipping tool calls and thinking blocks), and pipes the result to
readback say, which invokes Kokoro and plays the audio.
Claude Code sessions are cheap — you probably have several open at any time. Without pinning, any Claude Code session would trigger audio, including ones running in background (build watchers, long-running agents, automated workflows). Pinning lets you say "only this one specific chat reads aloud."
Pin as many or as few as you want. The hook checks membership on every fire, so the overhead is near zero for unpinned sessions.
Claude Code writes the transcript asynchronously — when the Stop hook fires, the latest assistant text may not yet be on disk. The hook retries up to 8 times (250ms apart, 2s total) waiting for the new text to appear. You can see this in the log:
grep 'extracted on attempt' /tmp/readback.log
Most turns resolve on attempt 1 or 2. If you see attempt 7 or 8 regularly, your filesystem is slow and you may want to bump the retry count in hooks/claude-code-stop.sh.
# Tail the main log (hook + kokoro + CLI all write here)
tail -f /tmp/readback.log
# Run the doctor
readback doctor
# Inspect current pinning
readback list
# Test the hook manually with a synthetic payload
echo '{"session_id":"<session-id>","cwd":"/path/to/cwd","stop_hook_active":false}' \
| ~/.claude/hooks/readback-stop-hook.shreadback uninstall-hookThis removes the Stop entry from ~/.claude/settings.json (preserving everything else) and deletes ~/.claude/hooks/readback-stop-hook.sh. Your session pins in ~/.config/readback/sessions are left alone — they're harmless without the hook registered.
cd ~/workspace/readback
git pull
readback update-hookThe hook is a copy, not a symlink, so you have to explicitly refresh it. This is intentional — a symlink means a broken commit in the repo would instantly break your live hook.