diff --git a/CHANGELOG.md b/CHANGELOG.md index 305478b..b0b11f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +## [3.32.1] - 2026-03-08 + +### Added + +- **`auto-rename-session.sh` hook template** (`examples/hooks/bash/auto-rename-session.sh`) — hook SessionEnd qui génère automatiquement un titre descriptif pour chaque session. Lit le JSONL de session directement, extrait les 3 premiers messages utilisateur, appelle `claude -p --model claude-haiku-4-5-20251001` pour générer un titre 4-6 mots (format `verb + subject`), fallback sur le premier message nettoyé si Haiku indisponible. Met à jour le slug dans le JSONL natif (pour `/resume`) et dans `sessions-index.jsonl`. Output via `/dev/tty` pour bypasser le parsing JSON de Claude Code. + +### Documentation + +- **Section "Session Auto-Rename" mise à jour** (`guide/ultimate-guide.md`) — présente désormais deux approches complémentaires : Approche A (instruction CLAUDE.md, renommage mid-session via `/rename`, zéro tooling) et Approche B (hook SessionEnd, titre généré par Haiku en post-session, lecture directe du JSONL). Suppression du paragraphe "Why not a hook?" qui était incorrect depuis l'introduction de l'accès aux données de session via JSONL. + ## [3.32.0] - 2026-03-06 ### Added diff --git a/examples/hooks/bash/auto-rename-session.sh b/examples/hooks/bash/auto-rename-session.sh new file mode 100755 index 0000000..c4df0b7 --- /dev/null +++ b/examples/hooks/bash/auto-rename-session.sh @@ -0,0 +1,252 @@ +#!/bin/bash +# .claude/hooks/auto-rename-session.sh +# Event: SessionEnd +# Automatically generates a descriptive title for each session when it ends. +# +# Strategy: +# 1. Reads the session JSONL from ~/.claude/projects/ (via session_id from stdin or env) +# 2. Extracts the first 3 user messages as context +# 3. Calls claude-haiku-4-5 to generate a 4-6 word title (verb + subject) +# 4. Falls back to a sanitized version of the first message if Haiku fails +# 5. Updates sessions-index.jsonl (custom index) and the JSONL slug (for /resume) +# +# Requirements: +# - python3 (standard on macOS/Linux) +# - claude CLI on PATH (for AI title generation; fallback works without it) +# +# Wire up in .claude/settings.json: +# { +# "hooks": { +# "SessionEnd": [ +# { +# "matcher": "", +# "hooks": [{ "type": "command", "command": "~/.claude/hooks/auto-rename-session.sh" }] +# } +# ] +# } +# } +# +# Configuration: +# SESSION_AUTORENAME=0 Disable this hook for a specific session +# +# Note: Output goes to /dev/tty (or /dev/stderr as fallback) to avoid interfering +# with Claude Code's JSON parsing of hook stdout. + +set -uo pipefail + +# Route output to tty so it doesn't pollute Claude Code's JSON parsing of stdout +if (echo -n "" > /dev/tty) 2>/dev/null; then + OUT="/dev/tty" +else + OUT="/dev/stderr" +fi + +# Opt-out mechanism: set SESSION_AUTORENAME=0 in your shell or .env to skip +[[ "${SESSION_AUTORENAME:-1}" == "0" ]] && exit 0 + +# --------------------------------------------------------------------------- +# Step 1: Resolve session_id and working directory +# --------------------------------------------------------------------------- +# Claude Code passes a JSON payload on stdin with session_id and cwd. +# If stdin was already consumed by a preceding hook, fall back to env vars. + +INPUT=$(cat 2>/dev/null || true) + +SESSION_ID="" +CWD="" + +if [[ -n "$INPUT" ]]; then + SESSION_ID=$(echo "$INPUT" | python3 -c " +import sys, json +try: + d = json.load(sys.stdin) + print(d.get('session_id', '')) +except: pass +" 2>/dev/null || true) + + CWD=$(echo "$INPUT" | python3 -c " +import sys, json +try: + d = json.load(sys.stdin) + print(d.get('cwd', '')) +except: pass +" 2>/dev/null || true) +fi + +# Fall back to environment variables if stdin parsing yielded nothing +[[ -z "$SESSION_ID" ]] && SESSION_ID="${CLAUDE_SESSION_ID:-}" +[[ -z "$CWD" ]] && CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}" +[[ -z "$CWD" ]] && CWD="$(pwd)" + +# --------------------------------------------------------------------------- +# Step 2: Locate the session JSONL file +# --------------------------------------------------------------------------- +# Claude Code stores sessions under ~/.claude/projects//.jsonl + +PROJECT_SLUG=$(echo "$CWD" | sed 's|/|-|g') +PROJECTS_DIR="$HOME/.claude/projects/$PROJECT_SLUG" + +JSONL_FILE="" + +if [[ -n "$SESSION_ID" && -f "$PROJECTS_DIR/$SESSION_ID.jsonl" ]]; then + JSONL_FILE="$PROJECTS_DIR/$SESSION_ID.jsonl" +elif [[ -d "$PROJECTS_DIR" ]]; then + # No explicit session_id: pick the most recently modified JSONL (excluding agent files) + JSONL_FILE=$(ls -t "$PROJECTS_DIR"/*.jsonl 2>/dev/null | grep -v '/agent-' | head -1 || true) + [[ -n "$JSONL_FILE" ]] && SESSION_ID=$(basename "$JSONL_FILE" .jsonl) +fi + +[[ -z "$JSONL_FILE" || ! -f "$JSONL_FILE" ]] && exit 0 + +# Read the current slug so we can detect whether it changed later +CURRENT_SLUG=$(python3 -c " +import json, sys +with open(sys.argv[1]) as f: + for line in f: + try: + obj = json.loads(line) + s = obj.get('slug', '') + if s: print(s); break + except: pass +" "$JSONL_FILE" 2>/dev/null || true) + +# --------------------------------------------------------------------------- +# Step 3: Extract the first 3 user messages as context +# --------------------------------------------------------------------------- +# Skips tool result blobs (<...), slash commands (/...), and messages longer +# than 250 characters to keep the prompt tight. + +CONTEXT=$(python3 -c " +import json, sys + +jsonl_file = sys.argv[1] +messages = [] + +with open(jsonl_file) as f: + for line in f: + try: + obj = json.loads(line) + if obj.get('type') != 'user': + continue + content = obj.get('message', {}).get('content', '') + if not isinstance(content, str): + continue + content = content.strip() + if content.startswith('<') or content.startswith('/'): + continue + messages.append(content[:250]) + if len(messages) >= 3: + break + except: + continue + +print('\n---\n'.join(messages)) +" "$JSONL_FILE" 2>/dev/null || true) + +[[ -z "$CONTEXT" ]] && exit 0 + +FIRST_MSG=$(echo "$CONTEXT" | head -1 | head -c 60) + +# --------------------------------------------------------------------------- +# Step 4: Generate a title via Haiku (with plaintext fallback) +# --------------------------------------------------------------------------- + +TITLE="" +TITLE_SOURCE="fallback" + +PROMPT="Generate a 4-6 word lowercase English title summarizing this Claude Code session. Format: verb + subject (e.g. 'fix auth bug postgres', 'add stripe webhook handler', 'eval session labels tool'). Reply with ONLY the title, nothing else. + +Session messages: +$CONTEXT" + +if command -v claude &>/dev/null; then + # CLAUDECODE is unset to prevent the subprocess from inheriting hook context, + # which could cause unexpected behaviour or infinite loops. + TITLE=$(printf '%s' "$PROMPT" \ + | env -u CLAUDECODE timeout 12 claude -p --model claude-haiku-4-5-20251001 2>/dev/null \ + | head -1 \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^a-z0-9 ]//g' \ + | sed 's/ */ /g' \ + | sed 's/^ //;s/ $//' \ + | head -c 60 \ + || true) + [[ -n "$TITLE" ]] && TITLE_SOURCE="haiku" +fi + +# Fallback: sanitize the first user message into a slug-friendly string +if [[ -z "$TITLE" ]]; then + TITLE=$(echo "$FIRST_MSG" \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^a-z0-9 ]/ /g' \ + | sed 's/ */ /g' \ + | sed 's/^ //;s/ $//' \ + | head -c 55 \ + || true) +fi + +[[ -z "$TITLE" ]] && exit 0 + +SLUG=$(echo "$TITLE" | sed 's/ /-/g' | sed 's/[^a-z0-9-]//g') + +# --------------------------------------------------------------------------- +# Step 5: Persist the title +# --------------------------------------------------------------------------- + +# 5a. Update sessions-index.jsonl (used by custom session browsers / tooling) +python3 -c " +import json, os, sys + +index_path = os.path.expanduser('~/.claude/sessions-index.jsonl') +session_id = sys.argv[1] +title = sys.argv[2] + +try: + with open(index_path) as f: + entries = [json.loads(line) for line in f if line.strip()] + for entry in entries: + if entry['id'] == session_id: + entry['context'] = title + break + with open(index_path, 'w') as f: + for entry in entries: + f.write(json.dumps(entry) + '\n') +except: + pass +" "$SESSION_ID" "$TITLE" 2>/dev/null || true + +# 5b. Update the slug field inside the JSONL so /resume shows the new title +if [[ -n "$CURRENT_SLUG" && -n "$SLUG" && "$CURRENT_SLUG" != "$SLUG" ]]; then + TMPFILE=$(mktemp) + python3 -c " +import json, sys + +jsonl_file, old_slug, new_slug = sys.argv[1], sys.argv[2], sys.argv[3] + +with open(jsonl_file) as f: + lines = f.readlines() + +out = [] +for line in lines: + try: + obj = json.loads(line) + if obj.get('slug') == old_slug: + obj['slug'] = new_slug + out.append(json.dumps(obj, separators=(',', ':'))) + except: + out.append(line.rstrip()) + +print('\n'.join(out)) +" "$JSONL_FILE" "$CURRENT_SLUG" "$SLUG" > "$TMPFILE" 2>/dev/null + + [[ -s "$TMPFILE" ]] && mv "$TMPFILE" "$JSONL_FILE" || rm -f "$TMPFILE" +fi + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- +echo "" > "$OUT" +echo "Session renamed: \"$TITLE\" ($TITLE_SOURCE)" > "$OUT" +echo "" > "$OUT" + +exit 0 diff --git a/guide/ultimate-guide.md b/guide/ultimate-guide.md index 0c5d375..6f6fcc4 100644 --- a/guide/ultimate-guide.md +++ b/guide/ultimate-guide.md @@ -858,9 +858,13 @@ Claude: [Resumes with Serena's persistent project understanding] ### Session Auto-Rename -When running multiple Claude Code sessions in parallel (split terminals, WebStorm tabs, parallel workstreams), the `/resume` picker shows sessions by timestamp or truncated first prompt — impossible to distinguish. +When running multiple Claude Code sessions in parallel (split terminals, WebStorm tabs, parallel workstreams), the `/resume` picker shows sessions by timestamp or truncated first prompt — impossible to distinguish at a glance. -**Solution**: A behavioral instruction in `~/.claude/CLAUDE.md` that makes Claude rename sessions automatically after 2-3 exchanges, with no tooling required. +Two complementary approaches solve this. Use one or both together. + +#### Approach A: CLAUDE.md behavioral instruction (mid-session) + +A behavioral instruction in `~/.claude/CLAUDE.md` makes Claude call `/rename` automatically after 2-3 exchanges. No tooling required, works across all IDEs and terminals. ```markdown # Session Naming (auto-rename) @@ -887,13 +891,45 @@ When running multiple Claude Code sessions in parallel (split terminals, WebStor - Do NOT ask for confirmation on early rename (just do it) ``` -**Why not a hook?** The `Stop` event hook has no access to conversation context — it can't infer a meaningful title. The behavioral instruction approach costs zero tooling and works across all IDEs and terminals. +This works well during active sessions but depends on Claude following the instruction consistently. -**Limitation**: Terminal tab names (WebStorm, iTerm2) are not affected. JetBrains filters ANSI escape sequences. The Claude session is renamed, not the OS tab. +#### Approach B: SessionEnd hook (automatic, AI-generated) -After a session, the `/resume` picker shows `"fix auth middleware"` instead of `"2026-03-04T14:23..."`. +A `SessionEnd` hook reads the session's JSONL file directly from `~/.claude/projects/`, extracts the first few user messages as context, and calls `claude -p --model claude-haiku-4-5-20251001` to generate a 4-6 word descriptive title. If Haiku is unavailable, it falls back to a sanitized version of the first message. + +The hook updates both `sessions-index.jsonl` (for custom session browsers) and the slug field in the JSONL file (for native `/resume` compatibility). + +```json +// .claude/settings.json +{ + "hooks": { + "SessionEnd": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/auto-rename-session.sh" + } + ] + } + ] + } +} +``` + +Requirements: `claude` CLI on PATH, `python3` for JSON parsing. Set `SESSION_AUTORENAME=0` to disable for a specific session. + +After the session ends, the `/resume` picker shows `"fix auth middleware"` instead of `"2026-03-04T14:23..."`. + +#### Using both together + +The two approaches handle different moments in a session's lifecycle. Approach A renames early so the session is identifiable while it's still running. Approach B renames at the end with a title that reflects the full session scope, potentially overwriting the mid-session name with something more accurate. + +**Limitation (both approaches)**: Terminal tab names in WebStorm and iTerm2 are not affected. JetBrains filters ANSI escape sequences. The Claude session is renamed, not the OS tab. > See full template: [examples/claude-md/session-naming.md](../examples/claude-md/session-naming.md) +> See hook template: [examples/hooks/bash/auto-rename-session.sh](../examples/hooks/bash/auto-rename-session.sh) ## 1.4 Permission Modes