Skip to content
NewMy Claude Code sessions now say which project they are
Nov 6, 2026·4 MIN READ
My Claude Code sessions now say which project they are

My Claude Code sessions now say which project they are

Four Claude Code sessions, one Ping sound, no idea which one needed me. A small hook script now speaks a short word per project, queues the voices and has a mute switch for meetings.

I usually have three or four Claude Code sessions open, each in a different client folder. My hooks played Blow when a session needed me and Ping when it finished. The same two sounds for every project. So every Ping meant: switch through all the terminal tabs and look.

Today I fixed that in about an hour, and most of the hour went into finding out what not to do.

First idea: a different sound per project

Claude Code passes JSON to every hook on stdin, and that JSON has the session's cwd. My projects live in ~/work/<client>/, so the client name is just the first folder after ~/work/. One script can read it and pick a sound from a small map.

macOS ships 14 system sounds in /System/Library/Sounds. I gave three clients two sounds each, one for "needs you" and one for "done". It worked. It was also useless: after ten minutes I could not tell Bottle from Frog, or remember which one was which client.

What stuck: a spoken word

The fix was to drop sounds for the projects I care about and let macOS say one short word instead. No learning, no mapping in my head.

Folder in ~/workSaysSpeed
vimo"vimo"280 wpm
mtl"MTL"280 wpm
acme"acme"280 wpm
anything elsea plain Ping

Three things I changed after hearing it in real use:

Faster speech

The default voice speed is about 175 words per minute. At 280 a word takes under half a second and still sounds clear.

No beep before the word

Beep plus word felt like two notifications. The word alone is enough, and the beep stays only for unlisted projects.

A queue

Two sessions finishing at once spoke over each other. A lock file now makes them wait their turn.

How it fits together

flowchart LR
    A["Claude Code<br/>Notification or Stop"] --> B["project-sound.sh"]
    B --> C{"muted?"}
    C -- yes --> D[silence]
    C -- no --> E{"client in map?"}
    E -- yes --> F["say word"]
    E -- no --> G["afplay Ping"]
    F --> H["lockf queue"]
    G --> H

The map is a plain text file, one line per client: the folder name, then the word. The heart of the script is a few lines:

project-sound.sh
client=${${cwd#$HOME/work/}%%/*}

if [[ -n $word ]]; then
  lockf -k $lock zsh -c 'say -r 280 "$1"; sleep 0.3' _ "$word" &!
else
  lockf -k $lock zsh -c 'afplay "$1"; sleep 0.3' _ /System/Library/Sounds/Ping.aiff &!
fi

lockf ships with macOS, so the queue needs nothing extra. The &! runs the sound in the background, so the hook returns at once and Claude Code does not wait for the voice.

Worktrees in ~/work/<client>/workdirs/ map to the same client for free, because only the first folder after ~/work/ counts.

A mute switch for meetings

A laptop that says "acme" in the middle of a call with a different client is not great. So the same script takes on, off, toggle and status. off creates a flag file, and while it exists every hook stays silent.

I can flip it two ways: /sounds off inside any Claude session, or cc-sounds off in a terminal. The flag file is in .gitignore, so muting my laptop does not mute the other Mac when I sync ~/.claude.

settings.json is not in my synced repo, so the second Mac needed a manual change to point its hooks at the script. I left that as a handoff note for the other machine.

What it took

One script, one text file, two hooks and a slash command. The code was the easy part. The real work was listening: sounds that looked fine in a table were impossible to tell apart, the first voice speed was too slow, and the beep in front of the word was noise. Next time I will start with spoken words and skip the sound palette.

Share
Mariusz Smenżyk
Written by

Mariusz Smenżyk

Builder for sport. Make. Test. Launch.

Related notes