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 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[@]}))

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

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

7 comments on “Claude Code Hooks for Fun and Focus

  1. Cool stuff Murphy! You could package this up in a plugin and have it be downloadable/configurable from a Claude marketplace!

    I’ve been building something similar, a Claude Code plugin that speaks out the responses. I think it’d be cool to converse with it verbally at some point! It’s using kokoro which may be good for your project as well, it is a nice natural-ish sounding TTS. https://www.kokorotts.io

    1. Oooh! Thanks for telling me about Kokoro, that sounds really nice. And it looks like it’s fully local! ✨

      Will you blog about your plugin once it’s done? I’d like to hear how it turns out.
      A fellow Automattician shared their speak-to-interact Claude plugin recently: https://github.com/kat3samsin/claude-code-plugins, that might provide some inspiration too!

      1. Yeah, that looks like what I made, using Kokoro instead of `say`. Literally made it over the weekend, so probably has some bugs but if you want to kick the tires:

        https://github.com/Primary-Vector/claude-talk

        1. Cool! Have you been using it? I haven’t found myself wanting to talk out loud to the AI mostly.

          1. Sometimes! It doesn’t feel very “conversational” yet, at least as far as human interaction typically goes, since it can take many seconds for Claude to process.

          2. Arg yeah, that’s a flow killer. I just tried out Kokoro and it sounds nice! The first voice provides an experience very similar to what I’m getting from “say” with the default Mac OS 26 voice.

Leave a Reply to Murphy Randle Cancel reply

Your email address will not be published. Required fields are marked *