SLOPSHOPPER

on-me

Band: what waits on you and what Claude is doing now

newbandguard
v0.3.0MITupdated 2026-10-02abhibansal60/claude-mods/on-me
A shopper browsing a rack in a slop shop
README

claude-mods

Claude Code mods (plugins of function hooks) built from how I actually use Claude Code: two accounts rotated with cswap, many parallel sessions in herdr, and checking results rather than diffs.

on-me and twin-meter in the band above the prompt, ship-state in the side pane, on a real repo

Start here: paste this to your agent

Set up the claude-mods Claude Code plugins from https://github.com/abhibansal60/claude-mods.
1. Read README.md and each mod's hooks/register.tsx (or .ts) on GitHub so you know what each one runs.
2. Check what I have, read-only: `claude --version`, `which cswap herdr gh git`, `gh auth status`, and the plugins in ~/.claude/settings.json.
3. Tell me which mods fit my machine: twin-meter and browser-guard need cswap; herdr-fleet needs herdr; ship-state needs git and gh. Skip the ones whose tools are missing.
4. Ask me before changing anything. Then run `claude plugin marketplace add abhibansal60/claude-mods` and `claude plugin install <mod>@claude-mods --scope user` for each mod I pick.
5. Tell me to restart Claude Code, and how to see each mod: the one-line band above the prompt, `/ship-state`, `/herdr-fleet`, `/browser-guard`.

Install

claude plugin marketplace add abhibansal60/claude-mods
claude plugin install on-me@claude-mods --scope user
claude plugin install twin-meter@claude-mods --scope user
claude plugin install ship-state@claude-mods --scope user
claude plugin install browser-guard@claude-mods --scope user
claude plugin install herdr-fleet@claude-mods --scope user

Then restart Claude Code. Update later with claude plugin marketplace update claude-mods.

Needs: twin-meter and browser-guard use cswap, herdr-fleet uses herdr, ship-state uses git and gh.

The mods

ModWhereWhat it shows or does
on-meBand, leftOne line: a yellow ON YOU tag and the first item from the last answer's Blocked on me part (or "answer my question"), +N for more; a cyan CLAUDE tag and the current tool call while Claude works. Nothing when nothing waits on you.
twin-meterBand, right edgeA magenta OTHER ACCOUNT tag with #2 5h 3% ↺ 45m · 7d 91% for your other cswap accounts (the status line already shows the active one). Turns into a cyan SWITCH tag when the active account passes 60% and another has room.
ship-statePane, /ship-stateEach repo the session touched: branch, uncommitted files, unpushed commits, last CI run, commit statuses on HEAD (Vercel deploys) and PyPI against the local version. A toast when an answer says "done" but a repo is not shipped.
browser-guardHook on Chrome toolsPairs each cswap account with a Chrome browser on first use, then stops Chrome actions from the wrong profile. /browser-guard lists pairs, /browser-guard reset forgets them.
herdr-fleetPane, /herdr-fleetEvery herdr agent with status, folder, title and last words, "◉ you are here" on this pane, a focus button, a toast when another agent is blocked. Stops Bash commands that would close this pane or herdr.

Develop

Each mod is a plugin: .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.tsx, a types/index.d.ts contract for its $.state, and pure parsers in hooks/parse.ts with tests in tests/.

claude plugin validate ./<mod>
claude plugin test ./<mod>

To work on a mod live, run claude --plugin-dir ./<mod>: saving a file reloads it.

Source 3 files
hooks/register.tsx 71 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { OnMe } from '../types'
5import { endsWithQuestion, parseBlocked } from './parse'
6
7const state = atom({ plugin: 'on-me', key: 'state' } as const, { items: [], doing: '' })
8
9// Markdown marks read as noise in a one-line band.
10const plain = (text: string) => text.replace(/[`*_]/g, '').replace(/\s+/g, ' ').trim()
11
12const short = (text: string, n: number) => (text.length > n ? `${text.slice(0, n - 1)}…` : text)
13
14export const register: Register = on => {
15  on('tool.call', async ($, e, next) => {
16    if (!e.agentId) {
17      const input = e as unknown as { description?: string; command?: string; file_path?: string }
18      const doing = input.description ?? `${String(e.tool)} ${input.file_path ?? input.command ?? ''}`
19      await update($, state, (s: OnMe) => ({ ...s, doing: short(doing.trim(), 60) }))
20    }
21    return next(e)
22  })
23
24  on('turn.complete', async ($, e, next) => {
25    if (!e.agentId) {
26      const blocked = parseBlocked(e.answer)
27      const asks = endsWithQuestion(e.answer) ? ['answer my question'] : []
28      await update($, state, (s: OnMe) => ({
29        doing: '',
30        items: blocked === null ? [...new Set([...s.items.filter(i => i !== 'answer my question'), ...asks])] : [...blocked, ...asks],
31      }))
32    }
33    return next(e)
34  })
35
36  // One row shared with twin-meter: on-me on the left, whatever the band holds below it on the right.
37  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
38    const below = await next(e)
39    if (e.props.hasSurvey) return below
40    const s = await read($, state)
41    const { Box, Text } = $.ui.resolve(e)
42
43    const first = plain(s.items[0] ?? '')
44    const more = s.items.length > 1 ? `  +${s.items.length - 1}` : ''
45    // A colored tag sets the band apart from transcript rows and logs.
46    const mine =
47      e.props.isWorking && s.doing ? (
48        <Text key="on-me" wrap="truncate-end">
49          <Text backgroundColor="cyan" color="black" bold>{' CLAUDE '}</Text>
50          <Text color="cyan">{` ${plain(s.doing)}`}</Text>
51        </Text>
52      ) : first ? (
53        <Text key="on-me" wrap="truncate-end">
54          <Text backgroundColor="yellow" color="black" bold>{' ON YOU '}</Text>
55          <Text color="yellow">{` ${first}`}</Text>
56          <Text dimColor>{more}</Text>
57        </Text>
58      ) : null
59
60    if (!mine || !below) return mine ?? below
61    return (
62      // The engine refuses another hook's tree under a sized Box, so `below` sits bare in a plain row.
63      <Box flexDirection="row" gap={3}>
64        <Box flexShrink={1}>{mine}</Box>
65        <Box flexGrow={1} />
66        {below}
67      </Box>
68    )
69  })
70}
71
hooks/parse.ts 32 lines
1const LABEL = /^\s*(#{1,6}\s*|\*\*|__)?blocked on me\b/i
2const NONE = /^(nothing|none|no\b|n\/a|-|—)/i
3const NEXT_PART = /^\s*(#{1,6}\s|\*\*(changed|found)\b|(changed|found):)/i
4
5// Reads the "Blocked on me" part of an answer: an inline value
6// ("**Blocked on me:** pick the ideas") or the bullets below its heading.
7// null when the answer has no such part.
8export function parseBlocked(answer: string): string[] | null {
9  const lines = answer.split('\n')
10  // Only a heading or label that starts the line, never the words inside a sentence.
11  const at = lines.findIndex(l => LABEL.test(l))
12  if (at === -1) return null
13  const inline = lines[at]!.replace(LABEL, '').replace(/^[\s:*]+/, '').trim()
14  const items: string[] = []
15  if (inline) items.push(inline)
16  for (const line of lines.slice(at + 1)) {
17    if (NEXT_PART.test(line)) break
18    if (!line.trim()) {
19      if (items.length > 0) break
20      continue
21    }
22    items.push(line.replace(/^\s*([-*•]|\d+[.)])\s*/, '').trim())
23  }
24  return items.filter(i => i && !NONE.test(i)).map(i => i.replace(/\*\*/g, ''))
25}
26
27// The answer ends by asking something: the person must reply.
28export function endsWithQuestion(answer: string): boolean {
29  const last = answer.trim().split('\n').pop() ?? ''
30  return /\?\s*\**\s*$/.test(last)
31}
32
types/index.d.ts 13 lines
1export type OnMe = {
2  // What waits on the person, from the last answer's "Blocked on me" part
3  items: string[]
4  // What Claude does now; empty between turns
5  doing: string
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'on-me': { state: OnMe }
11  }
12}
13