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

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.tsx 49 lines1import { 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}
49types/index.d.ts 8 lines1export type Inbox = { fresh: number; needsYou: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'inbox-band': { inbox: Inbox }
6 }
7}
8