Blocks risky actions before they run: recursive deletes of root or home, force pushes, pipe-to-shell installs, and writes to secret or key files.

Small, independent mods for Claude Code: a live progress bar, a usage dashboard, a context-window meter, a safety guard, a git status band, and your own notes and todos.
Each mod is a plugin of function hooks. Install only the ones you want; none depends on another.
| Mod | Command | What it gives you |
|---|---|---|
| task-progress | /progress | A progress band above the prompt that follows the todo list, with a working timer and your usage. |
| usage-dashboard | /usage-dashboard | A pane with 5h and weekly limits, models, tasks, time left and a 7-day chart. |
| context-meter | /ctxmeter | Context-window percentage in the status line, with warnings at 70% and 90%. |
| safety-guard | /safeguard | Blocks destructive commands and writes to secret or key files before they run. |
| git-band | /gitband | Branch, ahead/behind and change counts on one line above the prompt. |
| notes-todos | /mytodo, /mynote | Your own todo list and notes that persist across sessions. |
Run these in a terminal Claude Code session. The desktop app's Code tab does not run /plugin install.
/plugin install task-progress --marketplace wablevaibhav/claude-mods
Answer y to add the marketplace the first time, then pick a scope. Repeat with any other mod name from the table:
/plugin install usage-dashboard --marketplace wablevaibhav/claude-mods
/plugin install context-meter --marketplace wablevaibhav/claude-mods
/plugin install safety-guard --marketplace wablevaibhav/claude-mods
/plugin install git-band --marketplace wablevaibhav/claude-mods
/plugin install notes-todos --marketplace wablevaibhav/claude-mods
Clone the repo and point Claude Code at the mod folders:
git clone https://github.com/wablevaibhav/claude-mods.git
cd claude-mods
claude --plugin-dir ./task-progress --plugin-dir ./usage-dashboard
Add one --plugin-dir per mod.
task-progressis a real screenshot. The other previews are illustrative text with sample numbers; real colours (green / amber / red, orange accents) follow your terminal theme.
task-progress: the band above the prompt

usage-dashboard: the /usage-dashboard pane
Claude usage updated 12s ago
╭──────────────────────────────────────────────────────────────────╮
│ 5-hour session resets in 2h 10m │
│ ██████████████▌░░░░░░░░░░░░░░░░░░░░░░░░░ 35% │
│ burning 0.40%/min · full in ~2h 5m │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Weekly resets in 4d 3h │
│ █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 12% │
╰──────────────────────────────────────────────────────────────────╯
● Limit risk green session cost ~$1.42
╭──────────────────────────────────────────────────────────────────╮
│ Models active: opus-5-5 (3m ago) │
│ ★ opus-5-5 ████████░░ 78% 1.2M 34 msgs ~$0.91 │
│ sonnet-5-5 ██░░░░░░░░ 22% 340k 12 msgs ~$0.18 │
│ tokens: in / out / cache = 80k / 190k / 1.3M │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Tasks ▸ 1 in progress ○ 3 pending ✓ 2 today │
│ ▸ Wire the charts 1m 20s │
│ ○ Add the tasks board │
│ ○ Polish and test │
│ Time left 25m (best case 14m) · confidence medium, 4 finished │
│ Should finish within the current 5h window. │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Last 7 days │
│ Wed 07 ██████████████████░░░░░░░░░░░░░░░░░░░░░░ 420k │
│ Thu 08 ███████████████████████████████░░░░░░░░░ 740k │
│ Fri 09 ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 240k │
│ Sat 10 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 │
│ Sun 11 ██████████████████████░░░░░░░░░░░░░░░░░░ 530k │
│ Mon 12 ████████████████████████████████████████ 1.0M │
╰──────────────────────────────────────────────────────────────────╯
context-meter and git-band each add one line above the prompt:
▲ Context 78% ███████████████████░░░░░ 156k / 200k Getting full: consider /compact soon. [Hide]
⎇ feature/login ↑2 ↓1 ● 3 staged ✎ 2 changed ? 2 untracked
A progress band above the prompt, visible while you type.
progress tool. Claude can report title, total, done, note, items and finished explicitly (the tool is mcp__task-progress__progress), for work that doesn't use a todo list.⠹ Working 12s) whenever a turn is running and no job is shown.Collapse / Expand hides the step list, Hide or /progress hides the band. A finished job clears when you send your next prompt.Open it with /usage-dashboard. (/usage is Claude Code's own built-in panel; this mod uses a different name so the two don't collide.)
5h 34% · wk 12% · opus-5-5 after your first reply.ctx 43%, the share of the context window in use./ctxmeter hides or shows the band.Stops a call before it runs and tells Claude why, so it asks you first.
| Blocks | Examples | ||
|---|---|---|---|
| Recursive deletes of root, home or wildcards | rm -rf /, rm -r ~, rm -rf *, rm -rf /etc | ||
| Force pushes without a lease | git push --force, git push -f (--force-with-lease is allowed) | ||
| Piping a download into a shell | `curl … \ | sh, wget … \ | sudo bash` |
| Disk and filesystem damage | dd … of=/dev/sda, mkfs.*, > /dev/sda, fork bombs, chmod -R 777 | ||
| Writing secrets | .env, .env.production (not .env.example), *.pem, *.key, id_rsa, service-account JSON, .npmrc, .aws/credentials, .git/ internals | ||
| Reading private keys | id_rsa, *.pem, *.key, .ssh/, .aws/credentials |
/safeguard shows status and how many calls it blocked. /safeguard pause and /safeguard resume turn it off and on for the session. If the guard itself errors, it blocks the call rather than letting it through.
detached HEAD), ↑ahead ↓behind against the upstream, staged, changed and untracked counts, conflicts, or ✓ clean./gitband hides or shows it.Your own lists, separate from Claude's todo list, stored locally and kept across sessions. The open-todo count shows in the status line.
/mytodo add Renew the domain add a todo (a bare /mytodo <text> also adds)
/mytodo list todos
/mytodo done 2 /mytodo undo 2 /mytodo rm 2 /mytodo clear
/mynote Wifi password is on the fridge save a note
/mynote list notes
/mynote find wifi /mynote rm 3
five_hour, seven_day). Accounts without those windows (some API-key setups) show "No reading yet".TodoWrite list. If your session uses another task tool, task-progress can still be driven through its progress tool./usage, /context and /todos.git-band runs git status.| Problem | What to try |
|---|---|
/usage shows "Session limit / Breakdown" | That is Claude Code's built-in panel. Use /usage-dashboard. |
| A mod's command isn't found | The mod isn't loaded. Install it, or start Claude with --plugin-dir ./<mod>. |
| Gauges say "No reading yet" | Send one message; the reading arrives with the first reply. |
| Band shows the old layout after an update | Close the pane and reopen it, or restart Claude. |
| A command was blocked unexpectedly | Run /safeguard to see why, /safeguard pause if you really want it. |
/plugin install isn't available | Use a terminal Claude Code session, not the desktop app's Code tab. |
claude plugin validate . # the marketplace
claude plugin validate ./<mod> # one mod
claude plugin test ./<mod> # its tests
Each mod is a folder with .claude-plugin/plugin.json, hooks/hooks.json, a hooks module, pure logic with *.test.ts files, and (where it keeps state) a types/index.d.ts contract. The marketplace file is .claude-plugin/marketplace.json.
Releases are git tags: v0.2.0 added context-meter, safety-guard, git-band and notes-todos.
MIT, see LICENSE.
hooks/register.tsx 86 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { checkBash, checkRead, checkWrite } from './rules'
5
6const isPaused = atom({ plugin: 'safety-guard', key: 'isPaused' } as const, false)
7const blocked = atom({ plugin: 'safety-guard', key: 'blocked' } as const, 0)
8
9export const register: Register = on => {
10 on('session.start', async ($, e, next) => {
11 await $.command.register({
12 name: 'safeguard',
13 description: 'Safety guard: show status, or pause / resume it for this session',
14 })
15
16 return next(e)
17 })
18
19 on('command.run', { command: 'safeguard' }, async ($, e) => {
20 const arg = e.args.trim().toLowerCase()
21 if (arg === 'pause') await update($, isPaused, () => true)
22 if (arg === 'resume') await update($, isPaused, () => false)
23
24 const paused = await read($, isPaused)
25 const count = await read($, blocked)
26
27 return {
28 text:
29 `Safety guard is ${paused ? 'PAUSED' : 'active'}; ${count} call(s) blocked this session.\n` +
30 'Blocks: recursive delete of / ~ . * or system dirs, git push --force, curl|sh, disk writes, chmod -R 777,\n' +
31 'and writes to .env, key files, service-account JSON, .npmrc, .aws/credentials, .git internals.\n' +
32 'Use /safeguard pause or /safeguard resume.',
33 }
34 }).catch(() => ({ text: 'Safety guard status unavailable.' }))
35
36 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
37 if (await read($, isPaused)) return next(e)
38
39 const reason = checkBash(e.command)
40 if (reason === null) return next(e)
41
42 await update($, blocked, n => n + 1)
43 $.ui.toast(`Blocked: ${reason}`)
44
45 return {
46 deny: `${$.plugin.name}: blocked (${reason}). Ask the user first; they can run /safeguard pause to allow it.`,
47 }
48 }).catch(($, e, next) =>
49 next.called ? next(e) : { deny: `${$.plugin.name}: the guard failed, so this call was blocked.` },
50 )
51
52 for (const tool of ['Edit', 'Write'] as const) {
53 on('tool.call', { tool }, async ($, e, next) => {
54 if (await read($, isPaused)) return next(e)
55
56 const reason = checkWrite(e.file_path)
57 if (reason === null) return next(e)
58
59 await update($, blocked, n => n + 1)
60 $.ui.toast(`Blocked write: ${reason}`)
61
62 return {
63 deny: `${$.plugin.name}: blocked (${reason}). Ask the user first; they can run /safeguard pause to allow it.`,
64 }
65 }).catch(($, e, next) =>
66 next.called ? next(e) : { deny: `${$.plugin.name}: the guard failed, so this call was blocked.` },
67 )
68 }
69
70 on('tool.call', { tool: 'Read' }, async ($, e, next) => {
71 if (await read($, isPaused)) return next(e)
72
73 const reason = checkRead(e.file_path)
74 if (reason === null) return next(e)
75
76 await update($, blocked, n => n + 1)
77 $.ui.toast(`Blocked read: ${reason}`)
78
79 return {
80 deny: `${$.plugin.name}: blocked (${reason}). Ask the user first; they can run /safeguard pause to allow it.`,
81 }
82 }).catch(($, e, next) =>
83 next.called ? next(e) : { deny: `${$.plugin.name}: the guard failed, so this call was blocked.` },
84 )
85}
86hooks/rules.ts 108 lines1/** Pure rules: each check returns the reason a call is blocked, or null when it may run. */
2
3const SEPARATORS = /&&|\|\||[;|\n]/
4
5const DANGEROUS_TARGETS = new Set(['/', '/*', '~', '~/', '$HOME', '$HOME/', '.', '..', '*', './*', '~/*'])
6const SYSTEM_DIRS = /^\/(bin|boot|dev|etc|lib|lib64|opt|proc|root|sbin|sys|usr|var|home)\/?$/
7
8const words = (segment: string): string[] =>
9 segment
10 .trim()
11 .split(/\s+/)
12 .filter(w => w.length > 0)
13 .map(w => w.replace(/^["']|["']$/g, ''))
14
15const stripPrefix = (ws: string[]): string[] => {
16 let i = 0
17 while (i < ws.length && (ws[i] === 'sudo' || ws[i] === 'command' || ws[i] === 'time' || /^\w+=/.test(ws[i]))) i++
18 return ws.slice(i)
19}
20
21const isRecursive = (flags: string[]): boolean =>
22 flags.some(f => f === '--recursive' || /^-[a-zA-Z]*[rR][a-zA-Z]*$/.test(f))
23
24const isForce = (flags: string[]): boolean =>
25 flags.some(f => f === '--force' || /^-[a-zA-Z]*f[a-zA-Z]*$/.test(f))
26
27export const checkBash = (command: string): string | null => {
28 if (/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(command)) {
29 return 'fork bomb'
30 }
31 if (/\b(curl|wget)\b[^|;&\n]*\|\s*(sudo\s+)?(ba|z|da)?sh\b/.test(command)) {
32 return 'piping a download straight into a shell'
33 }
34 if (/\bdd\b[^;&|\n]*\bof=\/dev\//.test(command) || />\s*\/dev\/(sd|nvme|hd)/.test(command)) {
35 return 'writing directly to a disk device'
36 }
37 if (/\bmkfs(\.\w+)?\b/.test(command)) {
38 return 'formatting a filesystem'
39 }
40
41 for (const segment of command.split(SEPARATORS)) {
42 const ws = stripPrefix(words(segment))
43 if (ws.length === 0) continue
44 const [program, ...rest] = ws
45 const flags = rest.filter(w => w.startsWith('-'))
46 const args = rest.filter(w => !w.startsWith('-'))
47
48 if (program === 'rm' && isRecursive(flags)) {
49 const target = args.find(a => DANGEROUS_TARGETS.has(a) || SYSTEM_DIRS.test(a))
50 if (target !== undefined) return `recursive delete of ${target}`
51 }
52
53 if (program === 'git' && args[0] === 'push') {
54 const lease = flags.some(f => f.startsWith('--force-with-lease'))
55 if (isForce(flags) && !lease) return 'git push --force (use --force-with-lease)'
56 }
57
58 if (program === 'chmod' && isRecursive(flags) && args.some(a => a === '777' || a === 'a+rwx')) {
59 return 'recursive chmod 777'
60 }
61 }
62
63 return null
64}
65
66const baseName = (path: string): string => {
67 const normalized = path.replace(/\\/g, '/')
68 return normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase()
69}
70
71const isEnvFile = (name: string): boolean =>
72 (name === '.env' || name.startsWith('.env.')) && !/\.(example|sample|template|dist)$/.test(name)
73
74const isKeyFile = (name: string): boolean =>
75 name === 'id_rsa' ||
76 name === 'id_ed25519' ||
77 name === 'id_ecdsa' ||
78 name.endsWith('.pem') ||
79 name.endsWith('.key') ||
80 name.endsWith('.p12') ||
81 name.endsWith('.pfx')
82
83const isServiceAccount = (name: string): boolean =>
84 /serviceaccount.*\.json$/.test(name) || /firebase-adminsdk.*\.json$/.test(name)
85
86/** Writing or editing: secrets, keys, credentials and git internals are off limits. */
87export const checkWrite = (path: string): string | null => {
88 const normalized = path.replace(/\\/g, '/').toLowerCase()
89 const name = baseName(path)
90 if (isEnvFile(name)) return `${name} holds secrets`
91 if (isKeyFile(name)) return `${name} is a private key or certificate`
92 if (isServiceAccount(name)) return `${name} is a service-account key`
93 if (name === '.npmrc' || name === '.netrc') return `${name} can hold access tokens`
94 if (normalized.includes('/.aws/credentials')) return 'AWS credentials file'
95 if (normalized.includes('/.git/') && !normalized.endsWith('/.git/info/exclude')) return 'git internals'
96 return null
97}
98
99/** Reading: only private keys and cloud credentials. Reading .env is left to the user. */
100export const checkRead = (path: string): string | null => {
101 const normalized = path.replace(/\\/g, '/').toLowerCase()
102 const name = baseName(path)
103 if (isKeyFile(name)) return `${name} is a private key or certificate`
104 if (isServiceAccount(name)) return `${name} is a service-account key`
105 if (normalized.includes('/.aws/credentials') || normalized.includes('/.ssh/')) return 'credentials folder'
106 return null
107}
108types/index.d.ts 11 lines1export type GuardState = {
2 isPaused: boolean
3 blocked: number
4}
5
6declare module 'claude-code' {
7 interface PluginState {
8 'safety-guard': { isPaused: boolean; blocked: number }
9 }
10}
11