Ben Newton - Commerce Frontend Specialist

I Gave Claude Code the Voice of Rocky From Project Hail Mary

When a deploy is actually live or a stubborn bug finally dies, Claude Code says one short line out loud in Rocky's voice. Everything on screen stays normal English.

I Gave Claude Code the Voice of Rocky From Project Hail Mary

When Claude Code finishes something big for me now, like a deploy confirmed live or a stubborn bug finally dead, it says one short line out loud in Rocky's voice. Rocky is the Eridian engineer from Project Hail Mary. Everything on screen stays normal English. The voice only shows up for the big stuff, about once a session.

Here's a 26 second demo. The cheer at 0:12 is the real audio from Fish Audio.

That line was "Deploy live! Amaze amaze amaze. Fist my bump, friend." I did not get here on the first try. The first version read every single reply aloud and talked to me in Rocky-speak in the chat, and I turned most of it off by the end of the day.

It Started With Pedram's Gist

I saw Pedram Amini's gist rocky_say and wanted it immediately. It does two things.

First, a rule-based text transform that turns normal English into Rocky-speak. It drops articles and helper verbs, adds "question?" to the end of questions, and triples words for emphasis. So you get "good good good", "amaze amaze amaze", "no understand".

Second, a local voice clone using Coqui TTS. YourTTS is the default and XTTS v2 is an option. The clone is built from about 2 minutes 10 seconds of scrubbed film audio, and everything runs on your own machine.

The gist is also upfront that Rocky's voice design is the film production's IP and XTTS is under Coqui's non-commercial license. Personal use only. Same goes for everything in this post.

Why I Did Not Just Run It

I installed the voice clone in ~/.rocky_say. Python 3.12 venv, coqui-tts, torch 2.5.1. The gist setup also missed the click package, so the install needed that too.

Then I wrote my own speak.py instead of calling the gist script directly. The gist pastes your text into generated Python source and only escapes double quotes. That's fine when you type the line yourself. I was about to pipe Claude's replies into it, and a reply containing certain quote characters could break it. Claude writes a lot of quotes. So speak.py takes the text as data and never builds code out of it.

Every Reply, Out Loud

The first version had two parts.

The first part was a Claude Code output style. That's a markdown file in ~/.claude/output-styles/ that changes how Claude talks in chat. Mine made Claude talk in Rocky-speak but kept code, commits, PRs and file contents in normal English. Safety confirmations stayed plain too, because I did not want to be squinting at "Delete table, question?" before approving something. A typical reply looked like this:

Find bug. Migration no filter by site_id. Can read other tenant rows. Bad bad bad. I fix. Tests pass, good good good. I open PR, question?

The second part was read-aloud. Claude Code has a Stop hook, which runs when Claude finishes a reply. Mine pulled out the last reply, stripped code blocks, links, tables and markdown, capped it around 600 characters, and handed it to speak.py in the background. Running it in the background mattered. The speech never blocks Claude, so the next turn is not waiting on audio.

I wanted to be able to kill it fast, so there's a switch file plus a small script:

rocky-voice on rocky-voice off rocky-voice hush rocky-voice status

And a /rocky-voice slash command that calls the same thing from inside a session.

It worked. It also was not good. The local voice sounded rough, and each line took roughly 15 to 30 seconds because the model loads fresh every time. By the time Rocky finished telling me about the last reply I was usually two replies further along.

Casey's Desk Robot Had a Better Voice

I had seen a Rocky desk robot on Instagram by Casey Grothaus (@casey.aicreates). It's a great build. A Seeed XIAO ESP32-S3 Sense with an OLED face and pan/tilt servos, talking to a local Python server. The project page walks through it and the code is on GitHub.

Casey's robot sounded a lot more like Rocky than mine did. The voice comes from Fish Audio, a hosted TTS service, using a community-uploaded fan clone of Rocky's voice. Casey's repo has the voice ID and the settings, temperature 0.4 and top_p 0.6. The README also says the voice is a fan upload, it may disappear, and it's for personal use. All credit to Casey here. I would not have found the voice without that repo.

So I switched speak.py to call Fish Audio's /v1/tts endpoint with that voice ID and the same settings. It's stdlib only, no SDK:

body = json.dumps({ "text": text, "reference_id": "6dd07916890445e59c5f019ad0fc7879", "format": "mp3", "latency": "balanced", "temperature": 0.4, "top_p": 0.6, }).encode() req = urllib.request.Request( "https://api.fish.audio/v1/tts", data=body, headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, )

It comes back as mp3 and plays with afplay.

Fish needs an API key and paid credit. My first call got HTTP 402 Payment Required, which was fair, I had not paid them anything. I added $5 of credit and tried again. A test line took about 7 seconds end to end including playback, and it sounded much better.

If Fish fails for any reason, no credit, no network, the voice got removed, speak.py falls back to the local clone. It sounds worse and it's slower, but it still speaks.

Too Much, So I Cut It Back

Reading every reply aloud was cool for a while. Then it was too much. A voice talking over you all afternoon wears you out, even when the voice is Rocky.

The Rocky-speak in chat was the other problem. I could not work with it. Reading "Migration no filter by site_id" when I'm trying to figure out what actually went wrong is a tax on every reply.

So I turned read-every-reply off and deleted the output style. What was left was the part I actually liked, which was hearing Rocky get excited when something good happened.

What I Kept

Now there's a short section in my global ~/.claude/CLAUDE.md. That file loads in every Claude Code session in every project, so this works everywhere without any per-repo setup. Here's the whole section:

# Rocky cheers When something genuinely big lands, have Rocky (the Eridian engineer from Project Hail Mary) say one short excited line out loud: ~/.rocky_say/bin/rocky-voice cheer "<line>" - Big means: a deploy confirmed live in production, a long or stubborn bug finally fixed, a large feature finished end to end, tests green after a real fight. Not routine edits, not every passing test run, not intermediate steps. At most about once per session unless several big things happen. - Cheer the verified result, not the action. A merge is not a win yet. Wait until the production build is Ready for that exact commit and production is serving it, and, if there is a health check or smoke test, until it passes. A failed build, a rollback, or a deploy you could not verify gets no cheer. - The line is Rocky-speak about what happened: under 20 words, no articles, "question?" on questions, tripled words for emphasis. - It runs in the background and returns instantly. - Only the spoken line is Rocky-speak. Everything written stays in normal English. - If Ben says "mute Rocky", "quiet", or similar, run rocky-voice mute.

Claude writes the line itself, about what just happened, and runs the command once. The cheer runs in the background and returns instantly, so Claude keeps going. If I want quiet, rocky-voice mute silences cheers and rocky-voice unmute turns them back on.

The first version of this rule counted a merged PR as big, and on the first night Claude had Rocky cheer a merge while the production build was still running. It was fine, the build went green four minutes later. But when I posted this on X, @cloneisjun replied that a post-deploy health check would save Rocky from celebrating a rollback. That's right, so the rule now waits for the verified result. A merge alone gets nothing. Another reply suggested a silent timeout so a stuck TTS call can't hang around forever, and cheer now gives up after 45 seconds.

The Stop hook is switched off. It fired on every reply, which is exactly what I wanted to get rid of. Deciding what counts as big is a judgment call, and Claude already has the context for that, so the rule lives in CLAUDE.md where Claude reads it.

How to Build Your Own

If you want this on your machine, roughly:

  1. Get a Fish Audio account, an API key, and some credit. $5 was enough to get me going.
  2. Write a small speak.py that posts to https://api.fish.audio/v1/tts with the voice ID from Casey's repo, saves the mp3, and plays it. On a Mac that's afplay.
  3. Optional, set up the local clone from Pedram's gist as a fallback. Pass text in as data, don't paste it into generated code.
  4. Wrap it in a rocky-voice script with cheer, mute and unmute. Make cheer run in the background so it returns right away, and give it a timeout so a stuck call dies quietly.
  5. Add the Rocky cheers section above to your global ~/.claude/CLAUDE.md.

Then go ship something and wait for the deploy.

The Tradeoffs

The hosted voice costs money. Not much, but it's paid credit, and the local clone is free.

It also means the cheer text goes to a hosted service. With cheers only, that's a short line Claude wrote about a deploy or a fix. In the read-every-reply version it was my actual replies, stripped down but still my work. The local clone never sends anything off the machine, which is the one thing it has going for it.

The voice is a fan upload. Casey's README says it may disappear, and if it does, my setup quietly drops back to the local clone.

And the voice belongs to the film. This is a personal toy on my own machine. I would not ship it in a product or put it in anything commercial, and you shouldn't either.

Now the voice shows up about once a session, when a deploy is actually live, and that's the amount I actually want.

Join the conversation

Have thoughts about this post? Reply on 𝕏 — I read every one.

Discuss on 𝕏
Loading tweet...

I wrote this post inside BlackOps, my content operating system for thinking, drafting, and refining ideas — with AI assistance.

If you want the behind-the-scenes updates and weekly insights, subscribe to the newsletter.

Related Posts