SLOPSHOPPER

status-line

Status line: local time, running jobs from ~/.cache/progress, optional API credit

newstatustimer
v1.0.0NOASSERTIONupdated 2026-10-05soyakaai-studio/claude-herdr-mods/mods/status-line
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · status-line
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ status-line: 🕐 11:53
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 1 files
hooks/register.ts 37 lines
1import type { Engine, Register } from 'claude-code'
2
3const PROGRESS = '/.cache/progress'
4const CREDIT = '/.cache/api-credit.json'
5
6async function refresh($: Engine) {
7  const home = (await $.env.get('HOME')) || ''
8  const parts: string[] = []
9  try {
10    for (const f of await $.fs.list(home + PROGRESS)) {
11      if (!f.name.endsWith('.json')) continue
12      try {
13        const d = JSON.parse((await $.fs.read(`${home + PROGRESS}/${f.name}`)) as string)
14        if (d.status !== 'running') continue
15        const n = d.total ? `${Number(d.done || 0).toLocaleString('en')}/${Number(d.total).toLocaleString('en')}` : ''
16        parts.push(`⏳ ${f.name.replace(/\.json$/, '')} ${n}${d.eta ? ` · ${d.eta}` : ''}`.trim())
17      } catch {}
18    }
19  } catch {}
20  try {
21    const c = JSON.parse((await $.fs.read(home + CREDIT)) as string)
22    parts.push(`${c.left < 5 ? '⚠️ ' : ''}credit $${c.left}`)
23  } catch {}
24  const TZ_OFFSET_HOURS = 3  // your UTC offset
25  const riyadh = new Date((await $.clock.now()) + TZ_OFFSET_HOURS * 3600 * 1000)
26  const hhmm = `${String(riyadh.getUTCHours()).padStart(2, '0')}:${String(riyadh.getUTCMinutes()).padStart(2, '0')}`
27  $.ui.status([`🕐 ${hhmm}`, ...parts.slice(0, 3)].join('  ·  '))
28}
29
30export const register: Register = on => {
31  on('session.start', async ($, e, next) => {
32    await refresh($)
33    $.clock.every(30000, () => refresh($))
34    return next(e)
35  })
36}
37