Stops risky shell commands (recursive delete, git clean, hard reset) and shows exactly which files would be affected before a human presses Proceed.

Nine mods for Claude Code (2.1.287 or newer), written from the walkthrough video. Each lives in mods/<name>.
Install one, or all, from a terminal session:
/plugin install <name> --marketplace KianaVega/KianaVega
| Mod | What it does | Extra model usage |
|---|---|---|
| savvy-progress | Progress bar above the prompt and a live list of subagents with model, tokens and rough cost | none |
| tool-skins | Icons and colors for tool calls in seven themes, switch with /skin <name> | none |
| you-should-know | A second, small model reads long answers and flags anything important you might miss | one small call per long answer |
| file-tree | Pane showing files Claude reads and edits, green once committed (/file-tree) | none |
| cache-tax | Holds a prompt that would reload a cold cache and shows an estimate. /keepwarm pings before the cache expires | none, except keep-warm pings |
| blast-radius | Stops recursive deletes, git clean, hard resets and shows the affected files before you press Proceed | none |
| reflect | Offers to save corrections as a rule in CLAUDE.md | one small call when you save |
| terminal-browser | /browse <url or file> opens a pane with a screenshot (kitty or Ghostty) and the page text | none |
| replay-theater | Replay button after a turn that steps through each edit as a diff (/replay) | none |
hooks/register.tsx. Edit and reload.hooks/register.tsx 85 lines1import type { Register } from 'claude-code'
2
3const MAX_LISTED = 25
4
5type Risk = { kind: string; preview: readonly string[] | undefined; targets: string[] }
6
7const unquote = (s: string) => s.replace(/^['"]|['"]$/g, '')
8
9// Split a simple command into words. Good enough to spot the dangerous forms;
10// anything cleverer than this is shown to the person as the raw command.
11const words = (segment: string) => segment.trim().split(/\s+/).map(unquote).filter(Boolean)
12
13const classify = (command: string): Risk | undefined => {
14 for (const segment of command.split(/&&|\|\||;|\|/)) {
15 const w = words(segment.replace(/^\s*sudo\s+/, ''))
16 const [cmd, ...rest] = w
17 const flags = rest.filter(a => a.startsWith('-'))
18 const args = rest.filter(a => !a.startsWith('-'))
19
20 if (cmd === 'rm' && flags.some(f => /^-[a-zA-Z]*[rR]/.test(f) || f === '--recursive')) {
21 return { kind: 'recursive delete', preview: undefined, targets: args }
22 }
23 if (cmd === 'rmdir' || cmd === 'shred') {
24 return { kind: cmd, preview: undefined, targets: args }
25 }
26 if (cmd === 'find' && rest.includes('-delete')) {
27 return { kind: 'find -delete', preview: ['find', ...rest.filter(a => a !== '-delete'), '-type', 'f'], targets: [] }
28 }
29 if (cmd === 'git' && rest[0] === 'clean' && flags.some(f => /f/.test(f))) {
30 return { kind: 'git clean', preview: ['git', 'clean', '-n', '-d', '-x'], targets: [] }
31 }
32 if (cmd === 'git' && rest[0] === 'reset' && rest.includes('--hard')) {
33 return { kind: 'git reset --hard', preview: ['git', 'diff', '--stat', 'HEAD'], targets: [] }
34 }
35 if (cmd === 'git' && rest[0] === 'checkout' && (rest.includes('.') || rest.includes('--'))) {
36 return { kind: 'git checkout (discards edits)', preview: ['git', 'diff', '--stat'], targets: [] }
37 }
38 }
39
40 return undefined
41}
42
43export const register: Register = on => {
44 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
45 const risk = classify(e.command)
46 if (risk === undefined) {
47 return next(e)
48 }
49
50 let listing: string[] = []
51 let note = ''
52 try {
53 if (risk.preview !== undefined) {
54 const ran = await $.process.run(risk.preview, { timeoutMs: 10000 })
55 listing = ran.stdout.split('\n').filter(Boolean)
56 } else if (risk.targets.some(t => t.includes('$') || t.includes('`'))) {
57 note = 'The paths use variables, so the file list cannot be shown here.'
58 } else {
59 // Unquoted $t expands globs but never runs substitutions in the text.
60 const ran = await $.process.run(
61 ['sh', '-c', 'for t; do find $t -type f 2>/dev/null; done', 'sh', ...risk.targets],
62 { timeoutMs: 10000 },
63 )
64 listing = ran.stdout.split('\n').filter(Boolean)
65 }
66 } catch {
67 note = 'The preview could not run.'
68 }
69
70 const shown = listing.slice(0, MAX_LISTED).map(f => ` ${f}`).join('\n')
71 const more = listing.length > MAX_LISTED ? `\n ... and ${listing.length - MAX_LISTED} more` : ''
72 const question =
73 `Blast radius: ${risk.kind}\n${e.command}\n\n` +
74 (listing.length > 0 ? `${listing.length} file(s) affected:\n${shown}${more}` : note || 'No files found to list.')
75
76 const answer = await $.ui.ask(question, { options: ['Cancel', 'Proceed'], header: 'Blast radius' })
77
78 return answer === 'Proceed'
79 ? next(e)
80 : { deny: `blast-radius: the person cancelled "${risk.kind}". Nothing was changed.` }
81 }).catch(($, e, next) =>
82 next.called ? next(e) : { deny: 'blast-radius: could not check this command, so it was held back.' },
83 )
84}
85