SLOPSHOPPER

twin-meter

5h and 7d limits of both cswap accounts in the band above the prompt

newbandguardprocesstimer
v0.3.1MITupdated 2026-10-05abhibansal60/claude-mods/twin-meter
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 74 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Account, Meter } from '../types'
5import { otherText, parseCswap, switchTo } from './parse'
6
7const meter = atom({ plugin: 'twin-meter', key: 'meter' } as const, null)
8
9let lastRun = 0
10
11async function refresh($: EngineInterface) {
12  lastRun = await $.clock.now()
13  const listed = await $.process.run(['cswap', 'list'], { timeoutMs: 20000 }).catch(() => null)
14  if (!listed || listed.exitCode !== 0) return
15  const { stdout } = listed
16  const accounts = parseCswap(stdout)
17  if (accounts.length === 0) return
18  await update($, meter, (): Meter => ({ accounts }))
19}
20
21export const register: Register = on => {
22  on('session.start', async ($, e, next) => {
23    void refresh($)
24    $.clock.every(120_000, () => refresh($))
25    return next(e)
26  })
27
28  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
29    const ran = await next(e)
30    if (/\bcswap\b/.test(e.command)) void refresh($)
31    return ran
32  })
33
34  on('turn.complete', async ($, e, next) => {
35    if (!e.agentId && (await $.clock.now()) - lastRun > 60_000) void refresh($)
36    return next(e)
37  })
38
39  // One row shared with on-me: whatever the band holds on the left, the other accounts on the right.
40  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
41    const below = await next(e)
42    const m = await read($, meter)
43    if (e.props.hasSurvey || !m) return below
44
45    const { Box, Text } = $.ui.resolve(e)
46    const to = switchTo(m.accounts)
47    const others = otherText(m.accounts)
48    if (!others && !to) return below
49
50    // The status line shows this session's account; the tag says this is the other one.
51    const mine = to ? (
52      <Text key="twin-meter">
53        <Text backgroundColor="cyan" color="black" bold>{' SWITCH '}</Text>
54        <Text color="cyan">{` to #${to.n} (5h ${to.five}%): cswap switch ${to.n}`}</Text>
55      </Text>
56    ) : (
57      <Text key="twin-meter">
58        <Text backgroundColor="magenta" color="black" bold>{' OTHER ACCOUNT '}</Text>
59        <Text color="magenta">{` ${others}`}</Text>
60      </Text>
61    )
62    if (!below) return mine
63    return (
64      // The engine refuses another hook's tree under a sized Box, so `below` sits bare in a plain row.
65      <Box flexDirection="row" gap={3}>
66        {below}
67        <Box flexGrow={1} />
68        {/* Room for the band's own [-] control at the right edge. */}
69        <Box flexShrink={0} paddingRight={4}>{mine}</Box>
70      </Box>
71    )
72  })
73}
74
hooks/parse.ts 42 lines
1import type { Account } from '../types'
2
3// Parses `cswap list`:
4//   1: a@b.com [a@b.com's Organization] (active)
5//      ├ 5h:  87%   resets 22:19         in 3h 42m
6//      └ 7d:  10%   resets Oct 5 13:29   in 2d 18h
7export function parseCswap(text: string): Account[] {
8  const accounts: Account[] = []
9  for (const block of text.split(/\n(?=\s*\d+: )/)) {
10    const head = block.match(/^\s*(\d+): (\S+)/m)
11    const five = block.match(/5h:\s*(\d+)%(?:.*?\bin\s+(\d+[dhm])(?:\s+\d+[hm])?)?/)
12    const week = block.match(/7d:\s*(\d+)%/)
13    if (!head || !five || !week) continue
14    const email = head[2]!
15    accounts.push({
16      n: Number(head[1]),
17      email,
18      isActive: /\(active\)/.test(block.split('\n').find(l => l.includes(email)) ?? ''),
19      five: Number(five[1]),
20      fiveReset: five[2] ?? null,
21      week: Number(week[1]),
22    })
23  }
24  return accounts
25}
26
27// The account to switch to, when the active one is getting full and another has room.
28export function switchTo(accounts: Account[]): Account | null {
29  const active = accounts.find(a => a.isActive)
30  if (!active || active.five < 60) return null
31  const best = accounts.filter(a => !a.isActive && a.week < 95).sort((a, b) => a.five - b.five)[0]
32  return best && best.five <= active.five - 20 ? best : null
33}
34
35// The status line already shows the active account, so the band shows only the others.
36export function otherText(accounts: Account[]): string {
37  return accounts
38    .filter(a => !a.isActive)
39    .map(a => `#${a.n}  5h ${a.five}%${a.fiveReset ? ` ↺ ${a.fiveReset}` : ''} · 7d ${a.week}%`)
40    .join('   ')
41}
42
types/index.d.ts 20 lines
1export type Account = {
2  n: number
3  email: string
4  isActive: boolean
5  five: number
6  // time until the 5h window resets, like "3h"
7  fiveReset: string | null
8  week: number
9}
10
11export type Meter = {
12  accounts: Account[]
13}
14
15declare module 'claude-code' {
16  interface PluginState {
17    'twin-meter': { meter: Meter | null }
18  }
19}
20