Appearance
session-title.sh
.claude/hooks/session-title.sh
PostToolUse
Session-title management, in two modes: write — PostToolUse on the Skill tool. Records "<emoji> <skill> <args>" for the last substantive skill run into .claude/.session-title. emit — called by session-start.sh. Prints the SessionStart JSON that sets sessionTitle, from that state file, else the current work branch. WHY a state file + SessionStart rather than a live rename: Claude Code only honours sessionTitle from SessionStart output — PostToolUse / Stop output cannot set it (verified against the hooks docs, 2026-06). So the title can't change the instant a skill runs; we capture the intent on use and apply it on the next startup/resume. Issue #443 tracks the platform ask to make it live. be-* manner-of-working modes (be-caveman, be-complete, …) are deliberately NOT titled: opening a session with caveman to save tokens must never make the session read as "about caveman".
Source
bash
#!/usr/bin/env bash
#
# Session-title management, in two modes:
# write — PostToolUse on the `Skill` tool. Records "<emoji> <skill> <args>"
# for the last *substantive* skill run into .claude/.session-title.
# emit — called by session-start.sh. Prints the SessionStart JSON that sets
# `sessionTitle`, from that state file, else the current work branch.
#
# WHY a state file + SessionStart rather than a live rename: Claude Code only
# honours `sessionTitle` from SessionStart output — PostToolUse / Stop output
# cannot set it (verified against the hooks docs, 2026-06). So the title can't
# change the instant a skill runs; we capture the intent on use and apply it on
# the next startup/resume. Issue #443 tracks the platform ask to make it live.
#
# be-* manner-of-working modes (be-caveman, be-complete, …) are deliberately
# NOT titled: opening a session with `caveman` to save tokens must never make
# the session read as "about caveman".
set -euo pipefail
cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0
state=".claude/.session-title"
case "${1:-write}" in
write)
input="$(cat)"
skill="$(jq -r '.tool_input.skill // empty' <<<"$input")"
[ -n "$skill" ] || exit 0
# Only mapped, substantive skills earn a title. Everything unlisted — every
# be-* mode included — falls through and leaves the existing title untouched.
case "$skill" in
build-*) emoji="🧑💻" ;;
brainstorm) emoji="🧠" ;;
*) exit 0 ;;
esac
args="$(jq -r '.tool_input.args // empty' <<<"$input" | tr '\n\r' ' ' | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//' | cut -c1-50)"
label="$skill"
[ -n "$args" ] && label="$skill $args"
mkdir -p .claude
printf '%s %s\n' "$emoji" "$label" >"$state"
;;
emit)
title=""
if [ -s "$state" ]; then
title="$(head -n1 "$state")"
else
branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
# The web env's auto-generated claude/* branch doesn't reflect the work,
# so it's no better than the auto-summary — skip it and stay untitled.
case "$branch" in
"" | HEAD | main | claude/*) : ;;
*) title="$branch" ;;
esac
fi
[ -n "$title" ] || exit 0
jq -n --arg t "$title" \
'{hookSpecificOutput: {hookEventName: "SessionStart", sessionTitle: $t}}'
;;
esac
exit 0