Skip to main content
  1. Posts/

Your First Claude Code Hook: Auto-Name Every Session

··915 words·5 mins·
Nick Liu
Author
Nick Liu
Building infrastructure for Facebook Feed Ranking at Meta. Previously at Walmart, Twitter, AWS, and eBay. MS in Computer Science at Georgia Tech.
Table of Contents
Taming Claude Code Sessions - This article is part of a series.
Part 2: This Article
In Part 1 we learned that named sessions are easy to find. Now let's make naming automatic with a hook. This is also a perfect first hook project, so I'll explain the whole idea from scratch.
Tested with Claude Code 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.json

We’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, or compact.

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:

JSON on stdin
cwd · source · session_id

JSON on stdout
hookSpecificOutput.sessionTitle

Claude Code
SessionStart fires

your script
claude-name-session

Claude Code
sets the session title

JSON on stdin
cwd · source · session_id

JSON on stdout
hookSpecificOutput.sessionTitle

Claude Code
SessionStart fires

your script
claude-name-session

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-session

This uses jq (a tiny JSON tool) and git for the branch name. Installing jq:

brew install jq
sudo 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
#

Hooks run real commands on your machine on Claude’s lifecycle events, so keep them small, readable, and trusted, exactly like the one above. Don’t paste in hook scripts you don’t understand. (Same caution as any shell snippet from the internet.)

Going further
#

A few hardening ideas once you’re comfortable:

  • Fall back to python3 if jq isn’t installed, so it also works on minimal remote servers.
  • If your home directory is managed by a dotfile manager like yadm, git won’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.

Taming Claude Code Sessions - This article is part of a series.
Part 2: This Article

Related

Hooks are guarantees, skills are knowledge, subagents are other people.

··1101 words·6 mins
My Claude Code config now holds two hooks, ten skills, and three custom subagents, and most of them started life in the wrong layer. The instruction the model followed nine times out of ten lived in a prompt until I accepted that nine out of ten is a coin I lose every day. The workflow I pasted into chats became a skill. The bulk work that was draining my priciest model's quota became a fleet of cheaper agents. Same features, different failure modes. Taming Claude Code Sessions · Part 6 of 6 1 2 3 4 5 6 🧪 Tested with Claude Code 2.1.x · macOS The four layers # Claude Code has four extension points, and they answer four different questions:

Running Several AI Coding Agents Without Losing Track

··867 words·5 mins
Once you're comfortable with AI coding agents, you start running several at once: one refactoring here, one writing tests there, one stuck waiting for your approval. Keeping them straight is its own little skill. Taming Claude Code Sessions · Part 4 of 6 1 2 3 4 5 6 🧪 Tested with Claude Code 2.1.x · macOS / Linux Here are two ways to do it: a lightweight tmux plugin, and (briefly) dedicated “AI terminal” apps.

Stop Losing Your Claude Code Conversations

··625 words·3 mins
You're deep in a great Claude Code conversation. You close the terminal. The next day you want to pick up where you left off… and you can't find it. Sound familiar? Let's fix that. Taming Claude Code Sessions · Part 1 of 6 1 2 3 4 5 6 🧪 Tested with Claude Code 2.1.x · macOS / Linux What is a “session,” and where does it go? # Every time you run claude, you start a session, one conversation, with its full history. When you quit, that history doesn’t vanish. Claude Code saves it to disk, organized per project folder, here: