A band above the prompt naming the computer, workspace and project a Claude Code session runs in.

A Claude Code mod that draws one line above the prompt, so you can tell at a glance which machine and project a session belongs to:
computer michaels-macbook-air workspace home project homelab herdr workspace w1 tab review agent proj-48
<x>_workspace folder in the repo's path, shown as x (- when there is none).HERDR_WORKSPACE_ID set). It always shows the herdr workspace id; it adds the tab label once the tab is renamed from its default number, and the agent name once one is set.The band refreshes at session start and after every turn.
/plugin marketplace add moneymikeMD/moneymike-plugins
/plugin install session-band@moneymike-plugins
claude plugin validate .
claude plugin test .
claude --plugin-dir .hooks/register.tsx 100 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Herdr, Where } from '../types'
5import { herdrOf, placeOf, repoOf } from './where'
6
7const where = atom({ plugin: 'session-band', key: 'where' } as const, null)
8
9async function herdrCall($: EngineInterface, bin: string, argv: string[]): Promise<string> {
10 try {
11 const { exitCode, stdout } = await $.process.run([bin, ...argv], { timeoutMs: 5000 })
12 return exitCode === 0 ? stdout : ''
13 } catch {
14 return ''
15 }
16}
17
18async function locateHerdr($: EngineInterface): Promise<Herdr | null> {
19 const [workspace, tab, pane, bin] = await Promise.all([
20 $.env.get('HERDR_WORKSPACE_ID'),
21 $.env.get('HERDR_TAB_ID'),
22 $.env.get('HERDR_PANE_ID'),
23 $.env.get('HERDR_BIN_PATH'),
24 ])
25 if (!workspace) return null
26 const herdr = bin || 'herdr'
27 const [tabGet, agentGet] = await Promise.all([
28 tab ? herdrCall($, herdr, ['tab', 'get', tab]) : '',
29 pane ? herdrCall($, herdr, ['agent', 'get', pane]) : '',
30 ])
31 return herdrOf(workspace, tabGet, agentGet)
32}
33
34async function locate($: EngineInterface): Promise<Where> {
35 const [host, root, herdr] = await Promise.all([
36 $.process.run(['hostname', '-s']),
37 $.session.root(),
38 locateHerdr($),
39 ])
40 const git = await $.process.run(
41 ['git', 'rev-parse', '--path-format=absolute', '--git-common-dir'],
42 { cwd: root },
43 )
44 const repo = git.exitCode === 0 ? repoOf(git.stdout.trim()) : root
45 return { computer: host.stdout.trim().toLowerCase(), ...placeOf(repo), herdr }
46}
47
48async function refresh($: EngineInterface): Promise<void> {
49 const found = await locate($)
50 await update($, where, () => found)
51}
52
53export const register: Register = on => {
54 on('session.start', async ($, e, next) => {
55 const started = await next(e)
56 await refresh($)
57 return started
58 })
59
60 on('turn.complete', async ($, e, next) => {
61 const done = await next(e)
62 if (e.agentId === undefined) await refresh($)
63 return done
64 })
65
66 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
67 const at = await read($, where)
68 if (e.props.hasSurvey || at === null) return next(e)
69
70 const { Box, Text } = $.ui.resolve(e)
71 const herdr = at.herdr
72
73 return (
74 <Box>
75 <Text wrap="truncate">
76 <Text dimColor>computer </Text>
77 <Text bold color="suggestion">{at.computer}</Text>
78 <Text dimColor> workspace </Text>
79 <Text bold color="warning">{at.workspace}</Text>
80 <Text dimColor> project </Text>
81 <Text bold color="success">{at.project}</Text>
82 {herdr == null ? null : (
83 <Text>
84 {' '}
85 <Text inverse color="claude">
86 <Text bold> herdr </Text>
87 {' workspace '}
88 <Text bold>{herdr.workspace}</Text>
89 {herdr.tab === undefined ? null : <Text>{' tab '}<Text bold>{herdr.tab}</Text></Text>}
90 {herdr.agent === undefined ? null : <Text>{' agent '}<Text bold>{herdr.agent}</Text></Text>}
91 {' '}
92 </Text>
93 </Text>
94 )}
95 </Text>
96 </Box>
97 )
98 })
99}
100hooks/where.ts 56 lines1import type { Herdr, Where } from '../types'
2
3/** Splits a repo path into workspace (`<x>_workspace` -> `x`) and the project directly beneath it. */
4export function placeOf(repo: string): Pick<Where, 'workspace' | 'project'> {
5 const parts = repo.split('/').filter(Boolean)
6 const at = parts.findIndex(p => /_workspace$/.test(p))
7 const root = parts[at]
8 if (root === undefined) return { workspace: '-', project: parts[parts.length - 1] ?? '/' }
9 return {
10 workspace: root.replace(/_workspace$/, ''),
11 project: parts[at + 1] ?? '-',
12 }
13}
14
15/** The main checkout a git common dir belongs to, so a worktree anywhere names its repo. */
16export function repoOf(commonDir: string): string {
17 return commonDir.replace(/\/+$/, '').replace(/\/\.git$/, '')
18}
19
20function parse(json: string): Record<string, unknown> {
21 try {
22 const value: unknown = JSON.parse(json)
23 return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {}
24 } catch {
25 return {}
26 }
27}
28
29function field(json: string, kind: string, key: string): unknown {
30 const result = parse(json).result
31 if (typeof result !== 'object' || result === null) return undefined
32 const item = (result as Record<string, unknown>)[kind]
33 if (typeof item !== 'object' || item === null) return undefined
34 return (item as Record<string, unknown>)[key]
35}
36
37/** A tab's label from `herdr tab get`, or undefined while it is herdr's default (the tab's own number). */
38export function tabLabelOf(tabGet: string): string | undefined {
39 const label = field(tabGet, 'tab', 'label')
40 const number = field(tabGet, 'tab', 'number')
41 if (typeof label !== 'string' || label === '' || label === String(number)) return undefined
42 return label
43}
44
45/** The agent's name from `herdr agent get`, or undefined when it has none. */
46export function agentNameOf(agentGet: string): string | undefined {
47 const name = field(agentGet, 'agent', 'name')
48 return typeof name === 'string' && name !== '' ? name : undefined
49}
50
51export function herdrOf(workspace: string, tabGet: string, agentGet: string): Herdr {
52 const tab = tabLabelOf(tabGet)
53 const agent = agentNameOf(agentGet)
54 return { workspace, ...(tab === undefined ? {} : { tab }), ...(agent === undefined ? {} : { agent }) }
55}
56types/index.d.ts 10 lines1export type Herdr = { workspace: string; tab?: string; agent?: string }
2
3export type Where = { computer: string; workspace: string; project: string; herdr: Herdr | null }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'session-band': { where: Where | null }
8 }
9}
10