Skip to main content
  1. Posts/

Your First Claude Code Hook: Auto-Name Every Session

··886 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:

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

Running Several AI Coding Agents Without Losing Track

··875 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 4 1 2 3 4 🧪 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

··623 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 4 1 2 3 4 🧪 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:

Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call

··2355 words·12 mins
Claude Code wants to run a shell command. I want to press a physical Stream Deck key, the YES key two inches to the left of my keyboard, to approve it. The hook gets exactly one HTTP response to decide allow vs deny. The key press might land in 200 milliseconds; it might land seven minutes later, after I've been pulled into a meeting and come back. The trick is that Claude Code's hook timeout is 600 seconds, which turns out to be just enough headroom to hold the HTTP response open the whole time and let a hardware button write the answer. Building ClaudeDeck · Part 6 of 10 1 2 3 4 5 6 7 8 9 10 🧪 Tested with Claude Code 2.1.x · macOS (Setup, for anyone who hasn’t seen this stack before: Claude Code is Anthropic’s terminal CLI for Claude, and one of its hook events, PreToolUse, is a script Claude spawns and waits on before running a tool like Bash or Edit. The script’s stdout decides “allow” / “deny” / “ask”. Stream Deck is Elgato’s USB grid of programmable LCD keys. The plumbing I’m describing here lives in a daemon, a background process at 127.0.0.1:9127, that the hook script POSTs to and that the Stream Deck plugin connects to over WebSocket. For the hooks docs themselves and the four other gotchas in that layer, see the hooks-reality post.)