SLOPSHOPPER

session-band

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

newbandprocess
v0.2.0MITupdated 2026-10-09moneymikeMD/session-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · session-band
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM computer dev workspace - project a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
computer dev workspace - project a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
README

session-band

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
  • computer is the short hostname, lowercased.
  • workspace is the <x>_workspace folder in the repo's path, shown as x (- when there is none).
  • project is the folder directly beneath the workspace, or the repo's own name outside one. A git worktree resolves to its main checkout, so a worktree stored anywhere still names its repo.
  • herdr appears, highlighted, only inside a herdr pane (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.

Install

/plugin marketplace add moneymikeMD/moneymike-plugins
/plugin install session-band@moneymike-plugins

Develop

claude plugin validate .
claude plugin test .
claude --plugin-dir .
Source 3 files
hooks/register.tsx 100 lines
1import { 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}
100
hooks/where.ts 56 lines
1import 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}
56
types/index.d.ts 10 lines
1export 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