I built a way for Claude Code to report progress through my home Alexa
Introduction When I hand a long-running job to Claude Code, I can't tell what's happening unless I'm watching the screen. So I built a skill that has the Echo Dot beside me read out "I'm about to do this" and "done" in Japanese. It looks simple, but the implementation hits a string of small traps:

Posted on Nov 1 • Originally published at wada-dev.com
When I hand a long-running job to Claude Code, I can't tell what's happening unless I'm watching the screen. So I built a skill that has the Echo Dot beside me read out "I'm about to do this" and "done" in Japanese. It looks simple, but the implementation hits a string of small traps: the HA SSH add-on's sshd constraints, the Amazon-side _speak APIs going silent, and /tmp being a tmpfs that wipes the script. Here's the configuration I landed on after stepping on all of them.
For what it's worth, this very post had its in-progress status ("writing the Japanese version," "verifying," "merging") read aloud from my study's Echo Dot by exactly this mechanism, while I was writing it.
When I hand Claude Code a multi-step job (edit files → wait for CI → open a PR, etc.), I lose track of progress the moment I switch to something else. Having "about to do X" and "X is done" arrive through my ears, without going to look at the CLI, lets me do other things with peace of mind.
There are several Echo Dots in the house, and HA can fire TTS. So this should have been "just call a script over ssh homeassistant and make the Echo talk." In practice it snags about three times.
Claude Code skill (alexa-report) ↓ ssh homeassistant HA SSH add-on ↓ python3 /homeassistant/scripts/ha_speak.py HA Core REST API (auth: JWT mint from.storage/auth) ↓ POST /api/services/notify/send_message Alexa Devices integration ↓ notify.<device>_announce Echo Dot speaks (chime + message) Enter fullscreen mode Exit fullscreen mode Three design decisions:
Hand-appending to ~/.ssh/authorized_keys doesn't stick. The add-on regenerates authorized_keys from its Configuration at startup, so a manual edit is wiped on add-on restart.
HA UI → Settings → Add-ons → Advanced SSH & Web Terminal → Configuration → add your public key to authorized_keys: → Save → Restart.
Host homeassistant HostName 192.168.1.10 User wada IdentityFile ~/.ssh/id_rsa Enter fullscreen mode Exit fullscreen mode The HA SSH add-on's sshd has the SFTP subsystem disabled. scp on OpenSSH 9+ assumes SFTP, so it fails with subsystem request failed on channel 0. Pipe it in with ssh + cat instead:
ssh homeassistant 'cat > /homeassistant/scripts/ha_speak.py' < /path/to/ha_speak.py # or the legacy SCP protocol scp -O /path/to/ha_speak.py homeassistant:/homeassistant/scripts/ Enter fullscreen mode Exit fullscreen mode 3. The JWT-mint script Using the refresh_token's jwt_key (a 128-char hex) in.storage/auth, mint a short-lived JWT with HS256. PyJWT isn't installed in the SSH add-on, so build it by hand with the standard library:
import json, base64, hmac, hashlib, time, urllib. request, sys CLIENT_NAME = " claude-code " # key to select the refresh_token in.storage/auth API = " http://homeassistant.local.hass.io:8123 " with open ( " /homeassistant/.storage/auth " ) as f: d = json. load ( f ) tok = next ( t for t in d [ " data " ][ " refresh_tokens " ] if t. get ( " client_name " ) == CLIENT_NAME ) now = int ( time. time ()) b64 = lambda o: base64. urlsafe_b64encode ( json. dumps ( o, separators = ( ", ", ": " )). encode ()). rstrip ( b " = " ) h = b64 ({ " alg ": " HS256 ", " typ ": " JWT " }) p = b64 ({ " iss ": tok [ " id " ], " iat ": now, " exp ": now + 300 }) sig = base64. urlsafe_b64encode ( hmac. new ( tok [ " jwt_key " ]. encode (), h + b ". " + p, hashlib. sha256 ). digest () ). rstrip ( b " = " ) token = ( h + b ". " + p + b ".
" + sig ). decode () def post ( path, data ): req = urllib. request. Request ( API + path, data = json. dumps ( data ). encode (), headers = { " Authorization ": f " Bearer { token } ", " Content-Type ": " application/json " }, method = " POST ", ) return urllib. request. urlopen ( req, timeout = 5 ). status if sys. argv [ 1 ] == " speak ": print ( post ( " /api/services/notify/send_message ", { " entity_id ": sys. argv [ 3 ], " message ": sys. argv [ 2 ]})) Enter fullscreen mode Exit fullscreen mode 4. The Claude Code skill itself ssh homeassistant 'sudo python3 /homeassistant/scripts/ha_speak.py speak "writing the blog draft" notify.koheisannoecho_dot_max_announce' Enter fullscreen mode Exit fullscreen mode If you put a wrapper that calls this shell at each Before / After step inside the skill, Claude Code automatically speaks up on every long-running task.
A Japanese group name like "全部の部屋" (all rooms) gets pinyin-slugified on the HA side ( quan_bu_nobu_wu ). Renaming the group changes the slug, so when adding a new one, the reliable move is to hit GET /api/services for the notify domain and check.
This post was written with AI assistance and reviewed by the author.