SLOPSHOPPER

inbox-band

Shows N new reports above the prompt from a shared inbox file other chats append to

newbandtimer
v1.0.0NOASSERTIONupdated 2026-10-05soyakaai-studio/claude-herdr-mods/mods/inbox-band
A shopper browsing a rack in a slop shop
README

Claude mods for Herdr — a starter kit

Claude Code now supports mods: small plugins that run inside every Claude chat. They can block a command before it runs, put a line above the prompt, show a status line, or pop a toast. If you run many Claude chats in Herdr, a few small mods make the whole setup safer and calmer.

This kit has three, each a few dozen lines you can read and edit.

ModWhat it doesWhy we built it
guardBlocks risky shell commands before Claude runs them, and strips secrets you paste by accidentA cleanup job once emptied a phone folder; pkill -f killed Claude's own shell twice; a sign-in code landed in a chat
status-lineBottom line in every chat: local time · running jobs · API creditStop asking "status?" — jobs write a small progress file, every chat shows it
inbox-band"📥 3 new reports · ✋ 1 may need you" above the promptMany chats report to one inbox file; you see it without reading it

Rules written in a prompt can be forgotten. A mod is code, so it is enforced every time.


Install (2 minutes)

git clone <this repo> ~/.claude/mods/claude-herdr-mods

Load them in every Claude chat by adding this to ~/.claude/settings.json (merge with what's there):

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/mods/claude-herdr-mods/mods/guard:~/.claude/mods/claude-herdr-mods/mods/status-line"
  }
}

Add :~/.claude/mods/claude-herdr-mods/mods/inbox-band if you use the inbox idea below. New chats pick them up; restart open ones.

Try it: ask Claude to run pkill -f something. You should see guard: blocked: ….

Check or test a mod yourself:

claude plugin validate ~/.claude/mods/claude-herdr-mods/mods/guard
claude plugin test     ~/.claude/mods/claude-herdr-mods/mods/guard

Make them yours

  • guard → mods/guard/hooks/register.ts, the RULES list. Each rule is a pattern and a reason. Add the mistakes your agents make.
  • status-line → set TZ_OFFSET_HOURS. Any job can show up by writing ~/.cache/progress/<name>.json:
  { "status": "running", "done": 120, "total": 400, "eta": "14:30" }

Optional credit: write {"left": 12.5} to ~/.cache/api-credit.json from a small cron job.

  • inbox-band → other chats append one line per report to ~/.config/herdr/inbox.md (- 2026-10-06T10:00Z · Chat name · what happened). You (or your main chat) write the line number you've read to ~/.config/herdr/inbox.read. The band counts what's new.

Want to change one live? In a Claude chat, ask Claude to load the plugin-authoring skill and edit the mod — with hot reloading on, changes apply as soon as the turn ends.


How we use it with Herdr

  • One "Help Desk" chat we talk to; it dispatches work to other chats in panes and never does long work itself.
  • Other chats report into the inbox file instead of typing into the Help Desk pane (so they never land in the middle of what you're typing). The inbox band shows the count.
  • Long jobs write progress files; the status line shows them in every chat, and a small pane in Herdr can show the same file.
  • The guard runs in every chat, so a rule learned once protects all of them.

Small, boring, and it removed a whole class of "wait, what just happened?" moments.


Built by Sonat Yalcinkaya (SoyakaAI). MIT licence — use, change, share.

Source 2 files
hooks/register.tsx 49 lines
1import { atom, read, update } from 'claude-code'
2import type { Engine, Register } from 'claude-code'
3
4import type { Inbox } from '../types'
5
6const INBOX = '/.config/herdr/inbox.md'
7const POINTER = '/.config/herdr/inbox.read'
8const NOISE = /^$/  // add patterns for report lines you never want counted
9const NEEDS = /✋|needs you|❌|⚠️|blocked/i
10
11const inbox = atom({ plugin: 'inbox-band', key: 'inbox' } as const, { fresh: 0, needsYou: 0 })
12
13async function refresh($: Engine) {
14  const home = (await $.env.get('HOME')) || ''
15  try {
16    const text = (await $.fs.read(home + INBOX)) as string
17    const seen = parseInt(((await $.fs.read(home + POINTER)) as string).trim(), 10) || 0
18    const lines = text.split('\n').slice(seen).filter(l => l.startsWith('- 20') && !NOISE.test(l))
19    const fresh: Inbox = { fresh: lines.length, needsYou: lines.filter(l => NEEDS.test(l)).length }
20    await update($, inbox, () => fresh)
21  } catch {}
22}
23
24export const register: Register = on => {
25  on('session.start', async ($, e, next) => {
26    await refresh($)
27    $.clock.every(30000, () => refresh($))
28    return next(e)
29  })
30
31  on('turn.complete', async ($, e, next) => {
32    await refresh($)
33    return next(e)
34  })
35
36  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
37    const box = await read($, inbox)
38    if (e.props.hasSurvey || box.fresh === 0) return next(e)
39    const { Box, Text } = $.ui.resolve(e)
40    return (
41      <Box>
42        <Text color={box.needsYou ? 'yellow' : undefined} dimColor={!box.needsYou}>
43          📥 {box.fresh} new report{box.fresh === 1 ? '' : 's'}{box.needsYou ? ` · ✋ ${box.needsYou} may need you` : ''}
44        </Text>
45      </Box>
46    )
47  })
48}
49
types/index.d.ts 8 lines
1export type Inbox = { fresh: number; needsYou: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'inbox-band': { inbox: Inbox }
6  }
7}
8