2.1.x · macOS / Linux
Part 1 left us with one chore: you still have to remember to name your sessions. Let’s delete that chore.
What is a hook? #
A hook is just a small command that Claude Code runs for you at certain moments: when a session starts, when it finishes a turn, when it asks permission, and so on. You tell Claude “when X happens, run this script,” and it does. That’s it.
Hooks live in your settings file:
~/.claude/settings.jsonWe’ll use the SessionStart hook, which fires right when a session begins.
That’s the perfect moment to give it a name.
How a hook talks to your script #
When the hook fires, Claude Code runs your command and sends it a little blob of
JSON on standard input describing what’s happening. For SessionStart, that
JSON looks like this:
{
"session_id": "6b603cbb-…",
"cwd": "/Users/you/projects/my-app",
"source": "startup",
"hook_event_name": "SessionStart",
"transcript_path": "/Users/you/.claude/projects/…/6b603cbb-….jsonl"
}Two fields matter to us:
cwd: the folder the session is running in (we’ll name the session after it).source: why SessionStart fired:startup(brand new),resume(you came back to one),clear, orcompact.
To set the session’s title, our script just prints a specific bit of JSON back out:
{ "hookSpecificOutput": { "hookEventName": "SessionStart", "sessionTitle": "my-app" } }sessionTitle has the same effect as typing /rename. The whole round trip is
just two JSON messages:
flowchart LR
A["Claude Code
SessionStart fires"] -->|"JSON on stdin
cwd · source · session_id"| B["your script
claude-name-session"]
B -->|"JSON on stdout
hookSpecificOutput.sessionTitle"| C["Claude Code
sets the session title"]
The script #
Save this as ~/.local/bin/claude-name-session:
#!/usr/bin/env bash
# Name each Claude Code session after its project folder (and git branch),
# so it's easy to find later in /resume.
input="$(cat)" # read the JSON Claude sent us
src="$(printf '%s' "$input" | jq -r '.source')" # startup / resume / ...
cwd="$(printf '%s' "$input" | jq -r '.cwd')"
# Only name a brand-new session, or a resumed one that has NO name yet.
# This way we never overwrite a name you set by hand with /rename.
if [ "$src" = "resume" ]; then
existing="$(printf '%s' "$input" | jq -r '.session_title // empty')"
[ -n "$existing" ] && exit 0
elif [ "$src" != "startup" ]; then
exit 0
fi
# Build the label: "<project>" or "<project>/<branch>".
root="$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)"
label="$(basename "${root:-$cwd}")"
branch="$(git -C "$cwd" symbolic-ref --quiet --short HEAD 2>/dev/null)"
case "$branch" in
"" | main | master | trunk) ;; # skip noisy/default branch names
*) label="$label/$branch" ;;
esac
# Hand the title back to Claude Code.
jq -n --arg t "$label" \
'{hookSpecificOutput: {hookEventName: "SessionStart", sessionTitle: $t}}'Make it executable:
chmod +x ~/.local/bin/claude-name-sessionThis uses jq (a tiny JSON tool) and git for the
branch name. Installing jq:
brew install jqsudo apt install jq # Debian/Ubuntu
# or: sudo dnf install jq (Fedora)On a minimal box without jq, parse the same JSON with python3 instead:
cwd="$(printf '%s' "$input" | python3 -c 'import sys,json;print(json.load(sys.stdin)["cwd"])')"Why “only name on startup (or empty resume)”? #
A subtle but important detail. SessionStart fires again every time you
resume a session. If we blindly set the title each time, we’d stomp on any
name you carefully set with /rename. So:
- startup → always name it (it has no name yet).
- resume → only name it if it’s still unnamed (handy for back-filling old sessions you created before you had this hook).
- clear / compact → leave it alone.
That rule is “safe by design”: it can only ever add a name, never clobber one.
Wire it up #
Add the hook to ~/.claude/settings.json. If you already have a hooks
section, just add the SessionStart entry alongside what’s there:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "~/.local/bin/claude-name-session" }
]
}
]
}
}Test it (safely) #
Start a session in any project, then check it got a title:
cd ~/projects/my-app
claude -p "say hi" # -p = one-shot, headless; great for a quick test
# peek at the newest session file for this folder:
ls -t ~/.claude/projects/-Users-*my-app/*.jsonl | head -1 | xargs grep -m1 custom-title
# → {"type":"custom-title","customTitle":"my-app/your-branch"}Now open claude -r, hit Ctrl+A, and watch your sessions show up with real
names.
A safety word #
Going further #
A few hardening ideas once you’re comfortable:
- Fall back to
python3ifjqisn’t installed, so it also works on minimal remote servers. - If your home directory is managed by a dotfile manager like
yadm,
gitwon’t see it as a repo, so detect that case and use your dotfile manager’s branch instead.
Next up: your sessions are named, but your tmux windows are probably still
all called zsh. Part 3 fixes that.