Holds risky shell commands for a yes/no with a dry-run report, blocks Python heredocs carrying backslash escapes, and caps concurrent subagents at 4.

Two Claude Code mods — plugins of function hooks that run inside Claude Code (terminal and the Desktop app's Code tab). Needs Claude Code 2.1.287 or later (claude --version).
| Mod | What it does |
|---|---|
| blast-guard | Holds risky shell commands (rm -r, Remove-Item -Recurse, force push, git reset --hard, git clean -f, git checkout ., git branch -D) and asks Cancel / Run it, with a dry-run report of what would be lost. Blocks Python heredocs carrying \1 / \x.. escapes (they write control characters into files). Refuses a 5th concurrent subagent. |
| cache-meter | Status line: prompt-cache hit rate and the 1-hour TTL countdown (context size lives in usage-bar). A toast whenever a request rebuilds the cache instead of reading it, with the token count, the extra cost, and what changed just before (model switch, effort change, idle past the TTL, or a prompt-prefix change). /cache-meter prints the session summary. |
| usage-bar | One slim row above the prompt with solid bars for context fill and your 5-hour and 7-day plan limits (percent used, time to reset), plus the session cost. Bars are green under 60%, amber from 60%, red from 85%; at 90% the label turns into a bold red ⚠ warning and a toast fires once per reset window. more adds plain sentences (used, left, resets); × hides it. While subagents run, the row adds agent dots (pulsing while running, ✓ when done) and an agents button for savvy-progress's /agents-info. /usage-bar hides/shows, /usage-bar details expands and refreshes. |
In a Claude Code terminal session:
/plugin install blast-guard --marketplace satyamk4517/claude-mods
/plugin install cache-meter --marketplace satyamk4517/claude-mods
/plugin install usage-bar --marketplace satyamk4517/claude-mods
Answer y to add the marketplace, then pick a scope (user = every project).
Mods run with your permissions and are not sandboxed. Read the two hook files first, or list what they do without running them:
claude plugin validate ./blast-guard
claude plugin validate ./cache-meter
claude plugin validate ./usage-bar
blast-guard calls $.process.run (git dry runs only), $.ui.ask and $.agent.list. cache-meter calls the clock, status line, toasts and registers one command; it makes no network or file calls. usage-bar calls $.session.usage (the status line's own figures, no network request), $.agent.list, the clock, its store and toasts, and runs /agents-info only when you press agents.
PRICES in cache-meter/hooks/register.ts if your rates differ.$(...) or a script file walks past it. Pair it with deny permission rules for anything that must never run./cost totals, at API prices; on a subscription treat it as relative weight.claude plugin test ./blast-guard, claude plugin test ./cache-meter and claude plugin test ./usage-bar.hooks/register.ts 102 lines1import type { Register } from 'claude-code'
2
3// Concurrent subagent cap: many parallel agents can exhaust a usage window
4// fast; waves of four keep the work flowing without that spike.
5const MAX_AGENTS = 4
6
7// Each risky pattern, what it does, and which dry run shows its reach.
8type Risk = { pattern: RegExp; what: string; probe?: 'status' | 'clean' | 'push' }
9const RISKS: Risk[] = [
10 { pattern: /\brm\s+(-\w+\s+)*-\w*[rR]/, what: 'recursive delete (rm -r)' },
11 { pattern: /\bRemove-Item\b[^|;\n]*-Recurse/i, what: 'recursive delete (Remove-Item -Recurse)' },
12 { pattern: /\b(rd|rmdir)\s+\/s\b|\bdel\s+\/[sq]\b/i, what: 'recursive delete (cmd)' },
13 { pattern: /\bgit\s+push\b[^|;&\n]*\s(--force(-with-lease)?|-f)\b/, what: 'force push', probe: 'push' },
14 { pattern: /\bgit\s+reset\b[^|;&\n]*--hard\b/, what: 'git reset --hard (drops uncommitted work)', probe: 'status' },
15 { pattern: /\bgit\s+clean\b[^|;&\n]*\s-\w*f/, what: 'git clean -f (deletes untracked files)', probe: 'clean' },
16 { pattern: /\bgit\s+(checkout|restore)\b[^|;&\n]*\s(--\s+)?\.(\s|$)/, what: 'discard all working-tree changes', probe: 'status' },
17 { pattern: /\bgit\s+branch\s+(-\w+\s+)*-D\b/, what: 'force-delete a branch' },
18]
19
20// A Python heredoc with a \1 or \x.. escape lands control
21// characters in files. Plain \n is harmless and passes.
22const HEREDOC = /python[^\n]*<</
23const BAD_ESCAPE = /\\(\d|x[0-9a-fA-F])/
24
25const firstLines = (text: string, n: number) => {
26 const lines = text.trim().split('\n').filter(Boolean)
27 if (lines.length === 0) return ' (nothing)'
28 const shown = lines.slice(0, n).map(l => ' ' + l)
29 return shown.join('\n') + (lines.length > n ? `\n … and ${lines.length - n} more` : '')
30}
31
32type Git = (args: string[]) => Promise<string>
33
34// What the dry run says the command would touch.
35const report = async (git: Git, probe: Risk['probe']) => {
36 if (probe === 'status') return 'Uncommitted changes that would be lost:\n' + firstLines(await git(['status', '--porcelain']), 10)
37 if (probe === 'clean') return 'Files git clean would delete:\n' + firstLines(await git(['clean', '-n', '-d']), 10)
38 if (probe === 'push') {
39 const branch = (await git(['rev-parse', '--abbrev-ref', 'HEAD'])).trim()
40 return `Branch: ${branch}\nRemote commits that would be overwritten:\n` + firstLines(await git(['log', '--oneline', 'HEAD..@{u}']), 10)
41 }
42 return ''
43}
44
45const commandOf = (e: unknown) => String((e as { command?: unknown }).command ?? '')
46
47export const register: Register = on => {
48 for (const tool of ['Bash', 'PowerShell'] as const) {
49 on('tool.call', { tool }, async ($, e, next) => {
50 const command = commandOf(e)
51
52 if (HEREDOC.test(command) && BAD_ESCAPE.test(command)) {
53 return {
54 deny: 'blast-guard: Python heredoc contains a backslash escape (\\1 or \\x..). Write the script with the Write tool and run the file instead.',
55 }
56 }
57
58 const hits = RISKS.filter(r => r.pattern.test(command))
59 if (hits.length === 0) return next(e)
60
61 const git: Git = async args => {
62 try {
63 const r = await $.process.run(['git', ...args], { timeoutMs: 10_000 })
64 return r.exitCode === 0 ? r.stdout : `(git ${args[0]} failed: ${r.stderr.trim().slice(0, 120)})`
65 } catch {
66 return '(git not reachable here)'
67 }
68 }
69 const reach = (await Promise.all([...new Set(hits.map(h => h.probe))].map(p => report(git, p)))).filter(Boolean)
70 const question = [
71 `Risky ${tool} command: ${hits.map(h => h.what).join('; ')}.`,
72 ` ${command.length > 200 ? command.slice(0, 200) + '…' : command}`,
73 ...reach,
74 'Run it?',
75 ].join('\n')
76
77 let answer = ''
78 try {
79 answer = await $.ui.ask(question, { header: 'Blast radius', options: ['Cancel', 'Run it'] })
80 } catch {
81 return { deny: 'blast-guard: no one could confirm this risky command, so it was not run.' }
82 }
83 return answer === 'Run it'
84 ? next(e)
85 : { deny: `blast-guard: the user declined (${hits.map(h => h.what).join('; ')}). Ask them how to proceed.` }
86 }).catch(($, e, next) =>
87 next.called ? next(e) : { deny: 'blast-guard: the guard failed, so the command was held. Ask the user.' },
88 )
89 }
90
91 on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
92 const live = (await $.agent.list()).filter(a => a.status === 'running' || a.status === 'pending')
93 if (live.length >= MAX_AGENTS) {
94 return {
95 deny: `blast-guard: ${live.length} agents are already running (cap ${MAX_AGENTS}). Wait for one to finish, or run the rest as a second wave.`,
96 }
97 }
98 return next(e)
99 // The cap is about spend, not safety: if the count fails, let the agent run.
100 }).catch(($, e, next) => next(e))
101}
102