Names the tmux window after the conversation: on /rename, and when a named session starts or resumes

Personal Claude Code mods.
One mod, in one step:
/plugin install statusline --marketplace sidhantunnithan/jindan-cc-marketplace
Or add the marketplace once, then install any mod from it:
/plugin marketplace add sidhantunnithan/jindan-cc-marketplace
/plugin install <mod>@jindan
| Mod | What it does |
|---|---|
statusline | Status line in the prompt footer. At the left, the model (Opus 5.5 (1M context)) and working directory (~/Projects/foo, cut from the left past 40 characters). At the right, a context window bar (▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▒▒▒ 32%) coloured by what fills the context. simple (default): grey for system (prompt, tools, MCP, commands), blue for custom (agents, memory files, skills), yellow for messages. granular: each category in its /context colour. Free space is ▱, the autocompact buffer ▒; the percentage is green below 60%, yellow below 80%, red above. A shorthand line under the bar names each segment (sys 16k · custom 3k · msgs 92k). In /config: Colour mode, and Show model / Show directory / Show context bar. |
tmux-session-rename | Names the tmux window after the conversation. /rename <name> renames it at once; a bare /rename takes the name Claude generates; resuming a named session (--resume, --continue) names it as the session starts. When the session ends (exit or /clear) the window goes back to tmux's automatic name. Does nothing outside tmux; needs no tmux.conf changes. |
.claude-plugin/plugin.json and hooks/.plugins in .claude-plugin/marketplace.json.claude plugin validate . and claude plugin test <mod>.hooks/register.tsx 82 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// What /rename prints, saved as the command's output row. A bare /rename names the
4// session itself, and this row is the only place the plugin can read that name.
5const RENAMED = /^<local-command-stdout>Session renamed to: (.+)<\/local-command-stdout>$/s
6
7const renamedTo = (content: unknown) => {
8 const text = Array.isArray(content)
9 ? content.map(block => (block?.type === 'text' ? block.text : '')).join('')
10 : typeof content === 'string' ? content : ''
11 return RENAMED.exec(text.trim())?.[1]?.trim()
12}
13
14// Outside tmux there is no window to name, so every call is a no-op.
15async function nameWindow($: EngineInterface, title: string) {
16 const pane = await $.env.get('TMUX_PANE')
17 if (pane) await $.process.run(['tmux', 'rename-window', '-t', pane, title])
18}
19
20// The session's name as /rename last set it: the newest custom-title row of its
21// transcript. classic.SessionStart would carry it, but plugins at the user tier
22// are bypassed on that event, so the hook never runs in a real session.
23async function sessionTitle($: EngineInterface) {
24 const id = await $.session.id()
25 const { stdout } = await $.process.run([
26 'sh', '-c',
27 'cat "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/projects/*/"$1".jsonl 2>/dev/null | grep \'"type":"custom-title"\' | tail -n 1',
28 'sh', id,
29 ])
30 if (!stdout.trim()) return undefined
31 return (JSON.parse(stdout) as { customTitle?: string }).customTitle?.trim() || undefined
32}
33
34// rename-window turns automatic-rename off for the window; turning it back on
35// hands the name back to tmux (the running command) once the session is over.
36async function releaseWindow($: EngineInterface) {
37 const pane = await $.env.get('TMUX_PANE')
38 if (pane) await $.process.run(['tmux', 'set-option', '-w', '-t', pane, 'automatic-rename', 'on'])
39}
40
41export const register: Register = on => {
42 // /rename <name>: the name is in the args, so the window follows at once.
43 on('command.run', { command: 'rename' }, async ($, e, next) => {
44 const result = await next(e)
45 const title = e.args.trim()
46 if (title) await nameWindow($, title)
47 return result
48 }).catch(($, e, next) => next(e))
49
50 // Bare /rename: read the generated name off the command's output row.
51 on('session.append', { door: 'command' }, async ($, e, next) => {
52 const result = await next(e)
53 const title = renamedTo(e.message.content)
54 if (title) await nameWindow($, title)
55 return result
56 }).catch(($, e, next) => next(e))
57
58 // /resume: the session.end it raises (reason resume) has just handed the window
59 // back to tmux, so name it after the session now running.
60 on('command.run', { command: 'resume' }, async ($, e, next) => {
61 const result = await next(e)
62 const title = await sessionTitle($)
63 if (title) await nameWindow($, title)
64 return result
65 }).catch(($, e, next) => next(e))
66
67 // claude --resume / --continue: a named session names the window as it starts.
68 on('session.start', async ($, e, next) => {
69 const result = await next(e)
70 const title = await sessionTitle($)
71 if (title) await nameWindow($, title)
72 return result
73 }).catch(($, e, next) => next(e))
74
75 // Covers exit and /clear, after which the conversation has no name of its own.
76 on('session.end', async ($, e, next) => {
77 const result = await next(e)
78 await releaseWindow($)
79 return result
80 }).catch(($, e, next) => next(e))
81}
82