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

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.
| Mod | What it does | Why we built it |
|---|---|---|
| guard | Blocks risky shell commands before Claude runs them, and strips secrets you paste by accident | A cleanup job once emptied a phone folder; pkill -f killed Claude's own shell twice; a sign-in code landed in a chat |
| status-line | Bottom line in every chat: local time · running jobs · API credit | Stop asking "status?" — jobs write a small progress file, every chat shows it |
| inbox-band | "📥 3 new reports · ✋ 1 may need you" above the prompt | Many 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.
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
mods/guard/hooks/register.ts, the RULES list. Each rule is a pattern and a reason. Add the mistakes your agents make.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.
~/.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.
Small, boring, and it removed a whole class of "wait, what just happened?" moments.
Built by Sonat Yalcinkaya (SoyakaAI). MIT licence — use, change, share.
hooks/register.ts 37 lines1import 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