DEV Community

Cover image for Giving Claude Code a Voice (and a Face)
Yuuki Yamashita
Yuuki Yamashita

Posted on

Giving Claude Code a Voice (and a Face)

I spend a lot of time waiting on Claude Code. Not idle waiting — I tab away, work on something else, and come back later. The problem is "later" is a guess. A run that finishes in 40 seconds and one that's still going 20 minutes later look identical from another window: nothing happens until I check.

So I gave it a voice. Specifically, Zundamon's voice — a free, widely used Japanese character voice from VOICEVOX — plus a small character that shows up in the corner of my screen when there's something to say.

What it actually does

Claude Code has hooks that fire on specific events: when a turn ends (Stop), when a subagent finishes (SubagentStop), and when the CLI is waiting on me for longer than a few seconds (Notification). Each of those now runs a small Python script that sends a short line of text to a background daemon sitting in my menu bar. The daemon asks VOICEVOX to synthesize it, plays the result, and pops up a borderless little window with Zundamon and a speech bubble.

The daemon also remembers which of four display modes I picked: always on screen, only visible while actually talking, voice-only with no window at all, or fully muted. That choice is saved to a config file so it survives a restart.

None of this touches the network beyond localhost — the hook script, the daemon, and VOICEVOX Engine all talk to each other over 127.0.0.1.

The part that actually took the time

Wiring the pipeline together was the easy afternoon. Making it not sound broken took a lot longer, and most of the bugs were the kind you only notice by actually listening or actually looking.

English text came out mangled. The first version just piped my last message straight into VOICEVOX. It's a Japanese-only engine, so anything in English got sounded out phonetically and wrong — SubagentStop came out as something closer to "sa-ba-jento-sutoppu" than anything recognizable. I ended up stripping Markdown, keeping only the first sentence of a message (Claude's replies tend to open with a clean summary), and running the rest through a small hand-built glossary that swaps known English terms for their katakana reading before anything unknown gets dropped. That second part — silently dropping unknown English rather than mangling it — was a deliberate trade-off. It's less informative than reading everything, but it sounds like a voice instead of a glitch.

That fix had a side effect I didn't catch right away: the same regex that stripped stray English letters was also eating plain digits, since [A-Za-z0-9] was too broad. Task counts and port numbers just silently disappeared from what got spoken. Tightening the pattern to only match tokens that start with a letter fixed it without touching how English gets filtered.

A single kanji read wrong. The word 角 (corner) is genuinely ambiguous in isolation — it can be read kado or kaku depending on context, and VOICEVOX's default analysis picked the wrong one for how I was using it. VOICEVOX Engine exposes a real dictionary API for exactly this (/user_dict_word), so I registered the correct reading. It didn't take effect. Turned out the default word type is "proper noun," and the built-in dictionary entry for a plain corner apparently wins over a proper-noun override in that grammatical position. Re-registering it as a common noun with a higher priority fixed it immediately.

Failures were completely silent. At some point during testing, VOICEVOX had quietly crashed. The hook still fired, the bubble still popped up with the right caption — and nothing played. No error anywhere, because the exception was being caught and swallowed. I added a log line and a recovery path: if synthesis fails, relaunch the engine, wait for it to come back up, and retry once. A crash now costs a few seconds of silence instead of going unnoticed for the rest of the day.

The window was flush with the corner. The character wasn't. I pinned the floating window to (0, 0) — the literal bottom-right corner of the screen — and the character's feet still hovered above the edge with a visible gap. The window position was correct; I checked. What wasn't correct was the source artwork: the PNG had transparent padding baked in below the feet, so the visible pixels stopped short of the canvas edge. Cropping the artwork to its actual bounding box before resizing fixed it — no code change to the window logic at all.

Where it ended up

A menu-bar daemon, four display modes, a self-healing connection to VOICEVOX, a growing glossary for English terms, and a corrected dictionary entry for one very specific kanji. It's a small tool, and I don't think any single piece of it was hard. What was hard was that every failure mode was invisible by default — wrong pronunciation, dropped digits, a dead engine, a mispositioned character — and the only way to catch any of it was to actually sit there and listen, or take a screenshot and zoom in.

If you're building something similar: budget real time for the boring verification loop, not just the integration. That's where all of this actually lived.

Top comments (0)