Skip to content

Claude Code Hooks for Fun and Focus

My attention wanders. A lot. I’ll kick off a Claude Code session, switch to check on something else, and 20 minutes later realize I’ve got a forlorn Claude tab that’s been waiting for approval for …19 minutes. A notification beep doesn’t help much—it starts a quick game of “which tab was that?”. Not a huge...

Murphy R
Jan 10, 20265 min read

y attention wanders. A lot. I’ll kick off a Claude Code session, switch to check on something else, and 20 minutes later realize I’ve got a forlorn Claude tab that’s been waiting for approval for …19 minutes. A notification beep doesn’t help much—it starts a quick game of “which tab was that?”. Not a huge time sink, since it takes only a moment to find the tab. But those moments build up over the day, not only in time spent, but in mental fatigue.

So, inspired by this nice blog post, I set up Claude Code hooks to tell me what’s happening, with a fun twist.

The Setup

Claude Code supports hooks—shell scripts that run on events like “needs permission” or “task finished.” Hooks receive JSON on stdin with context about the session.

On macOS, the say command does text-to-speech, and the voices have come a long way in quality from where they were years ago.

Combine the hooks and the voices:

say "Claude needs your permission to edit server.ts"

Now I hear what needs attention, not just that something does.

The Fun Part: Session Identities

I have multiple terminals. Sometimes too many. Sometimes way too many. I wanted a way to hear which is calling me. Rather than try to announce paths (since my sessions are often in the same project directory) or worse, listen to session UUIDs being read aloud, I employed a little hashing logic for the sake of cuteness.

My hook script hashes the session ID (stable across hook calls) to reliably map to one of a set of emoji identities:

EMOJIS=("🍌:Banana" "🦊:Fox" "🌮:Taco" "🚀:Rocket" ...)

# MD5 hash the session ID for uniform randomness
HASH=$(echo -n "$SESSION_ID" | md5 -q)
# Map the hash to an emoji index:
# - Take first 8 hex chars and convert to a number
# - Mod by emoji count to get index 0-24
# - MD5 distributes inputs uniformly, so each emoji
# has roughly equal chance of being selected
INDEX=$((16#${HASH:0:8} % ${#EMOJIS[@]}))
</div> </div>

Now I hear “Banana needs your permission” or “Octopus is done.” Each session gets a consistent identity. We’ll see if naturally build a mental map: “Right, Banana is the API refactor, Octopus is the docs.” I don’t intend on doing this consciously, but letting my second brain do it.

Marking the tab

It’s not enough to hear the sound. I’ve got to be able to see which session is which, so the hook sets the terminal title via /dev/tty:

echo -ne "\033]0;🍌 Banana - Claude Code\007" > /dev/tty 2>/dev/null
</div> </div>

Here’s a bit from Claude on what /dev/tty is, in case you (like me) didn’t know:

/dev/tty is a special file that always points to the actual terminal the process is connected to, bypassing any stdout redirection.

This means that, even though the hook scripts are run in a sub-process, they can send a command to change the title of the terminal directly to the tab that called them.

The Scripts

speak-notification.sh (runs when Claude needs input):

  • Extracts the message and session ID from stdin JSON
  • Hashes session → emoji identity
  • Sets terminal title
  • Speaks the message passed into the hook, prefixed by the session emoji: “Banana needs your permission to run npm install”

speak-done.sh (runs when Claude finishes):

  • Same identity mapping
  • Says: “Banana is done”

Does It Help?

I just set this up today, so the jury’s out. But the first few times hearing “Cupcake is “done pulled me back to the right terminal instantly. That’s promising.

The code is simple—two ~50 line bash scripts. If your attention wanders like mine, might be worth a try.

Installing

1. Create the hooks directory

mkdir -p ~/.claude/hooks

2. Create ~/.claude/hooks/speak-notification.sh

#!/bin/bash # Speaks notifications with a fun session identity (emoji + name) # 25 fun emoji identities EMOJIS=( "🍌:Banana" "🦊:Fox" "🌮:Taco" "🚀:Rocket" "🦄:Unicorn" "🐙:Octopus" "🍕:Pizza" "🌈:Rainbow" "🦋:Butterfly" "🍩:Donut" "🐳:Whale" "🌻:Sunflower" "🎸:Guitar" "🦖:T-Rex" "🍪:Cookie" "🐨:Koala" "🌶️:Pepper" "🦜:Parrot" "🧁:Cupcake" "🐸:Frog" "🍋:Lemon" "🦉:Owl" "🎪:Circus" "🐝:Bee" "🍄:Mushroom" ) # Read JSON from stdin INPUT=$(cat) MESSAGE=$(echo "$INPUT" | jq -r '.message // "needs attention"') SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"') # MD5 hash the session ID for uniform randomness HASH=$(echo -n "$SESSION_ID" | md5 -q) # Map the hash to an emoji index: # - Take first 8 hex chars and convert to a number # - Mod by emoji count to get index 0-24 # - MD5 distributes inputs uniformly, so each emoji # has roughly equal chance of being selected HASH_NUM=$((16#${HASH:0:8})) INDEX=$((HASH_NUM % ${#EMOJIS[@]})) # Split emoji and name EMOJI="${EMOJIS[$INDEX]%%:*}" NAME="${EMOJIS[$INDEX]##*:}" # Set terminal title (write directly to tty to bypass stdout capture) echo -ne "\033]0;${EMOJI} ${NAME} - Claude Code\007" > /dev/tty 2>/dev/null # Speak with the identity name # Transform message: "Claude needs your permission" -> "Banana needs your permission" SPOKEN_MESSAGE="${MESSAGE/Claude/$NAME}" say "$SPOKEN_MESSAGE"

3. Create ~/.claude/hooks/speak-done.sh

#!/bin/bash # Speaks "done" with fun session identity EMOJIS=( "🍌:Banana" "🦊:Fox" "🌮:Taco" "🚀:Rocket" "🦄:Unicorn" "🐙:Octopus" "🍕:Pizza" "🌈:Rainbow" "🦋:Butterfly" "🍩:Donut" "🐳:Whale" "🌻:Sunflower" "🎸:Guitar" "🦖:T-Rex" "🍪:Cookie" "🐨:Koala" "🌶️:Pepper" "🦜:Parrot" "🧁:Cupcake" "🐸:Frog" "🍋:Lemon" "🦉:Owl" "🎪:Circus" "🐝:Bee" "🍄:Mushroom" ) INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"') # MD5 hash the session ID for uniform randomness HASH=$(echo -n "$SESSION_ID" | md5 -q) # Map the hash to an emoji index HASH_NUM=$((16#${HASH:0:8})) INDEX=$((HASH_NUM % ${#EMOJIS[@]})) EMOJI="${EMOJIS[$INDEX]%%:*}" NAME="${EMOJIS[$INDEX]##*:}" # Update terminal title (write directly to tty to bypass stdout capture) echo -ne "\033]0;${EMOJI} ${NAME} - Done!\007" > /dev/tty 2>/dev/null say "${NAME} is done"

### 4. Make them executable

chmod +x ~/.claude/hooks/speak-notification.sh chmod +x ~/.claude/hooks/speak-done.sh

### 5. Add hooks to ~/.claude/settings.json

{ "hooks": { "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "~/.claude/hooks/speak-notification.sh" } ] } ], "Stop": [ { "matcher": "", "hooks": [ { "type": "command", "command": "~/.claude/hooks/speak-done.sh" } ] } ] } }

If you already have a settings.json, merge the hooks section into it.

Requirements

  • macOS (uses say and md5 -q)
  • jq for JSON parsing (brew install jq if needed)

Attribution

Authored in collaboration with Claude Sonnet 4.5

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions