Blocks risky shell commands (phone deletes, printing secrets, self-killing pkill) and strips secrets pasted into messages

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 30 lines1import type { Register } from 'claude-code'
2
3// Rules that block a shell command before Claude runs it. Edit freely: each rule is a pattern and a reason.
4// Ours came from real mistakes; keep the ones that fit you and add your own.
5const RULES: { test: RegExp; why: string }[] = [
6 // Phones: never delete anything on a connected Android phone.
7 { test: /\badb\b[^|;&]*\bshell\b[^|;&]*\b(rm|unlink|delete)\b/, why: 'never delete anything on a phone' },
8 // Secrets: never print key files to the screen (using them inside $(cat file) is fine).
9 { test: /(?<!\$\()\b(cat|head|tail|less|more|xxd|base64|strings|od|grep|bat)\b[^|;&]*(\.env(\.local)?\b|api[-_]?key|secret|token|password|credentials)/i, why: 'never print a key or password file; use it as $(cat file) inside the command' },
10 // Self-harm: `pkill -f name` also matches the shell running it and kills Claude's own command.
11 { test: /\bpkill\s+-f\b/, why: 'pkill -f matches your own shell; use pgrep -x then kill <pid>' },
12]
13
14// Secrets pasted into a message by accident are removed before Claude sees them.
15const SECRET = /\b(sk-[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{30,}|ya29\.[A-Za-z0-9_.-]{20,}|ghp_[A-Za-z0-9]{30,}|glpat-[A-Za-z0-9_-]{20,}|xox[bpa]-[A-Za-z0-9-]{20,})\b|-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
16
17export const register: Register = on => {
18 on('prompt.submit', ($, e, next) => {
19 if (!SECRET.test(e.text)) return next(e)
20 SECRET.lastIndex = 0
21 $.ui.toast('A secret was removed from your message.')
22 return next({ ...e, text: e.text.replace(SECRET, '[secret removed]') })
23 })
24
25 on('tool.call', { tool: 'Bash' }, ($, e, next) => {
26 const hit = RULES.find(r => r.test.test(e.command))
27 return hit ? { deny: `${$.plugin.name}: blocked: ${hit.why}.` } : next(e)
28 })
29}
30