Catches risky shell commands (rm -rf, git reset --hard, force push, ...) before they run, forces a permission prompt, and shows what they would touch in a side…

A Claude Code plugin marketplace, and the single home for Claude customizations that stay the same on every device: ten mods, the Claritymaxx skill, the sync-home skill, and the karpathy-guidelines and python-standards coding skills. To add something, ask Claude to put it "on all my devices". The sync-home skill and CLAUDE.md tell it how.
| Plugin | Command | What it does |
|---|---|---|
| usage-forecast | /forecast, /forecast hide, /forecast show | A band above the prompt. 5-hour and Weekly plan limits as coloured bars (green, then yellow from 70%, red from 90%) with the percent used, a reset countdown and the reset time (↻ resets in 1h 12m · 15:40), and ⚠ at this pace, out in 40m when you would run out before the reset. Below it, the context window as weather with a 12-turn sparkline and turns left. Replaces token-weather. |
| blast-radius | /blast-radius, /blast-radius <command> (dry run) | Catches rm -rf, git reset --hard, force pushes, git clean -f, git checkout ., git branch -D, git stash drop/clear, DROP/TRUNCATE, mkfs, dd of=/dev/.... It shows what the command would touch in a pane and forces a permission prompt. A deny from your settings is never loosened. |
| replay-theater | /replay, /replay 2 | Records each turn's Edit and Write calls. In the pane, n/p step through the diffs, o/w change turn, and Esc closes it. |
| changed-files | /changed-files | A live sidebar of every file touched this session, with +/- counts, edit counts and a new tag. It opens by itself on the first edit. |
| session-meter | /meter | The status line shows elapsed time, tool calls and files edited. A toast appears when a turn takes 60 s or more; change it with /plugin configure session-meter@harshit-mods. |
| claritymaxx | /claritymaxx:explain <topic>, or ask "explain..." | Third-party skill from v60samurai/claritymaxx. Builds a mental model first, then explains it as text, a diagram or a small HTML page. |
| fork-explorer | /fork <what if...>, or type in the pane | Runs a side question over the current session and shows 2-3 alternative approaches as cards side by side, with pros, cons and effort. "use this" puts the choice in your prompt box to edit and send. Nothing is added to your conversation. |
| mission-control | /mission | A live pane of what Claude is executing: each tool call with its input, status badge (✓ ✗ ⊘ ●), duration and output; subagents in their own lanes with tool and token counts; a network-tab style waterfall; time spent in the model between tools. v switches between the timeline and the outputs. Observe-only; it never changes a call. |
| diff-minimap | automatic | A thin strip beside each Edit and Write row in the transcript. Green, red and yellow ticks show where in the file the change sits, with the line range and the file length. |
| codebase-atlas | /atlas, /atlas changes, /atlas components <file>, /atlas calls <name>, /atlas decisions, /atlas rescan | One pane with six tabs. Map: folders as boxes in dependency layers (entry points on top, foundations at the bottom), lit yellow when Claude edits and cyan when it reads; select one for what it imports and what uses it. Changes: every uncommitted change mapped to the functions and classes it added, modified or removed, with call-site counts, the layers crossed and the files that import what changed. Components: a file's classes, functions and methods with size bars, what each calls, and which changed. Calls: callers and callees of any function; type a name or select one in the transcript, then walk the graph. Decisions: a timeline of decisions a small model pulls from each turn (turn off with extractDecisions). Explain: side-question explanations of a folder, a function or the session's changes that never enter your conversation. Works with or without git: in a git repo, changes are measured against the last commit; in a plain folder, against the files as Atlas first read them that session (dependencies, virtual environments and build output are skipped). Reads Python and JS/TS. |
| workbench | /wb, /wb changes [file], /wb preview <file>, /wb artifacts, /wb map, /wb usage, /wb calls <name>, /wb forecast, /wb workbench, /wb hud off | One workspace. A one-line band above the prompt with two slides, switched with ◀ ▶: Workbench (session time, tool calls, files changed with +/−, new files, plan limits and context on wide terminals, then the running tool or the last edit and its functions) and Forecast (5-hour and weekly limits with bars, reset times and pace, plus the context forecast; it shows the usage-forecast mod's band when that mod is on). A pane with six tabs on one row: Now, the turn's tool calls with status and timing, failures with their error, and subagents nested under the call that started them; Changes, every file with a change bar and the functions it touched, then a file's changed functions and highlighted diff since Claude first touched it, then each edit (replay with n/p); Preview, a live render of the file being edited (Markdown, CSV table, JSON tree, HTML screenshot, PNG, code) with recent files one press away; Artifacts, files created this session by Claude ✦ or its commands, with preview and copy path; Map, architecture layers with folder sizes and imports, a file's components, and the call graph; Usage, limits with reset times and pace, context, and cost per turn. Works with or without git. Claude Code's own diff panel sits above mod panes: run /diff to hide it if it covers the workbench. |
| crew | /crew, /crew auto off, /crew close | An Agents side pane, like a mission board for subagents. Three cards on top total the session's cost, tokens and time. Each subagent is a little pixel crew member whose hat shows its effort (heavy, careful, medium, light), with its model and effort level, context used with a bar, tokens, its share of the cost and how long it ran; it walks while it works and shows the tool it is running. Sections: Running, Completed (✓ done, ✗ failed, ⊘ stopped; folds) and Planned, the tasks from Claude's task or todo list with what each still waits for ("after 1, 3"). Click an agent for its task, tool count and answer. Opens by itself when the first subagent starts; /crew auto off stops that. |
| explainer-page | /explainer-page:explainer-page, or ask for a visual or interactive explainer page | A skill for textbook-style HTML pages that teach one concept: each idea in words, maths, a figure and code, with colour-coded notation (inputs blue, outputs orange, parameters green, functions violet, probabilities pink) kept identical everywhere; tables and figures (3D bars, node diagrams) drawn from one data source and highlighted together on hover; sliders where a parameter is the point; code cells with their real output; margin notes, callouts and check-yourself questions; light and dark themes and a contents sidebar. Publishes as a claude.ai artifact when that tool is available, otherwise writes an .html file. Triggers only on an explicit request for such a page. |
| savvy-flow | /savvy-flow:savvy-flow <task> | A copy of savvy-flow from johnnyvizz/claude-kit with one change: the top tier, savvy-fable, runs on Opus at max effort instead of the Fable model, so it works without Fable access. The session model plans the task, delegates pieces to five tiered worker agents (savvy-fable, savvy-heavy, savvy-careful, savvy-medium, savvy-light) and reviews what they return. tools/sync-savvy-flow.sh refreshes the copy from upstream and reapplies the change; the Sync savvy-flow GitHub Action runs it daily and commits when upstream changed. |
| savvy-progress | /agents-info | From the same repo, following its main branch the same way. A progress bar above the prompt driven by savvy-flow, and an agents panel with model, effort, step progress, context, estimated cost and time. Animated crabs draw in the desktop app; the terminal shows text rows (crew draws its sprites in both). |
| sync-home | (automatic) or ask "add X to all my devices" | A skill that tells Claude this repository is where plugins, mods, skills and other customizations go so they sync to every device. Claude adds them under plugins/, validates, pushes and tells you what to turn on. |
| karpathy-guidelines | /karpathy-guidelines:clean-code, or automatic on any coding task | Guidelines from Andrej Karpathy's notes on LLM coding mistakes: think before coding, simplicity first, surgical changes, goal-driven execution, with worked examples in EXAMPLES.md. |
| python-standards | /python-standards:python-standards, or automatic on Python work | Python standards: SOLID, DRY, KISS and YAGNI; FastAPI project layout; naming; OWASP-aligned secure coding; linting and testing setup. |
Add this marketplace to your claude.ai account once:
harshitmywork17/claude-mods. If the repository is private, your GitHub account must be connected to Claude.sync-home too, so every session knows to put new synced customizations here.Plugins on your account sync at session start, on every machine where you sign in to Claude Code with that account. Mods need Claude Code v2.1.287 or later (claude --version, then claude update). After you push a change here, press Check for updates on the marketplace in claude.ai, and turn on any new plugin. Then start a new session.
If a command such as /wb is missing on a machine:
claude --version must be 2.1.287 or later.claude plugin list should show workbench@synced. If it doesn't, the sync hasn't picked it up: check for updates in claude.ai, or install on that machine as below.claude plugin test run in an empty folder should say no hooks module to load. Any other message means mods are turned off there.Mods run in Claude Code only: the terminal, the desktop app's Code tab, IDE extensions and cloud sessions set up as below. Claude chat on the web, desktop and mobile shows no mod UI. Claritymaxx is a skill, so it also works in Claude chat and Cowork.
Cloud sessions at claude.ai/code don't load account plugins or the plugins a repository lists in .claude/settings.json. They do load plugin folders named in CLAUDE_CODE_PLUGIN_DIRS. Set it once on the cloud environment:
CLAUDE_CODE_PLUGIN_DIRS=/home/user/claude-mods/plugins ``harshitmywork17/claude-mods alongside the repository you're working on. It is cloned to /home/user/claude-mods, and every plugin in plugins/ loads, including new ones.A cloud session can only clone the repositories selected for it, because this repository is private. In a session without it, the variable points at a missing folder, which Claude Code skips.
claude plugin marketplace add harshitmywork17/claude-mods
claude plugin install workbench@harshit-mods # repeat for each plugin
Or run every plugin from a local clone, picking up changes with git pull: add "env": { "CLAUDE_CODE_PLUGIN_DIRS": "<path to clone>/plugins" } to ~/.claude/settings.json. Don't combine this with account sync on the same machine: when two copies share a name, only the first one loads.
Each mod has tests: claude plugin test plugins/<mod>. Check a mod with claude plugin validate plugins/<mod>. Increase version in a mod's plugin.json when you change it, because installed copies are cached by version.
hooks/register.tsx 168 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Finding, Report, ReportStatus } from '../types'
5import { assess, summarize } from './risk'
6import type { Risk } from './risk'
7
8const PANE = 'blast-radius'
9const MAX_LINES = 15
10const MAX_REPORTS = 10
11const reports = atom({ plugin: 'blast-radius', key: 'reports' } as const, [])
12
13const STATUS_TEXT: Record<ReportStatus, string> = {
14 checking: 'measuring…',
15 awaiting: 'waiting for your approval',
16 ran: 'ran',
17 stopped: 'did not run (denied or failed)',
18 'dry-run': 'dry run, nothing executed',
19}
20
21async function probe($: EngineInterface, risks: readonly Risk[]): Promise<Finding[]> {
22 const findings: Finding[] = []
23 for (const risk of risks) {
24 for (const { label, argv } of risk.probes) {
25 try {
26 const run = await $.process.run(argv, { cwd: risk.cwd, timeoutMs: 5000 })
27 const text = (run.exitCode === 0 ? run.stdout : run.stderr || run.stdout).trimEnd()
28 const lines = text === '' ? ['(nothing)'] : text.split('\n')
29 findings.push({ label, lines: lines.slice(0, MAX_LINES), more: Math.max(0, lines.length - MAX_LINES) })
30 } catch (error) {
31 findings.push({ label, lines: [`(could not measure: ${String(error)})`], more: 0 })
32 }
33 }
34 }
35 return findings
36}
37
38function severityOf(risks: readonly Risk[]): Report['severity'] {
39 return risks.some(risk => risk.severity === 'critical') ? 'critical' : 'high'
40}
41
42async function setStatus($: EngineInterface, id: string, change: Partial<Report>): Promise<void> {
43 await update($, reports, list => list.map(one => (one.id === id ? { ...one, ...change } : one)))
44}
45
46async function record($: EngineInterface, id: string, command: string, risks: readonly Risk[]): Promise<void> {
47 const report: Report = {
48 id,
49 command,
50 label: summarize(risks),
51 severity: severityOf(risks),
52 findings: [],
53 status: 'checking',
54 }
55 await update($, reports, list => [report, ...list.filter(one => one.id !== id)].slice(0, MAX_REPORTS))
56}
57
58export const register: Register = on => {
59 on('session.start', async ($, e, next) => {
60 await $.command.register({
61 name: 'blast-radius',
62 description: 'Blast Radius: open the pane, or dry-run a command (/blast-radius <command>)',
63 argumentHint: '[command to analyze]',
64 immediate: true,
65 })
66
67 return next(e)
68 })
69
70 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
71 const risks = assess(e.command)
72 if (risks.length === 0) return next(e)
73
74 await record($, e.tool_use_id, e.command, risks)
75 void $.ui.open({ id: PANE, title: 'Blast Radius' })
76 $.ui.toast(`Blast Radius: ${summarize(risks)}. Check the pane before approving.`)
77 await setStatus($, e.tool_use_id, { findings: await probe($, risks), status: 'awaiting' })
78
79 const ran = await next(e)
80 const didRun = ran.deny === undefined && ran.isError !== true
81 await setStatus($, e.tool_use_id, { status: didRun ? 'ran' : 'stopped' })
82
83 return ran
84 })
85
86 on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
87 const verdict = await next(e)
88 const input = e.input as { command?: unknown }
89 if (verdict.decision === 'deny' || typeof input.command !== 'string') return verdict
90
91 const risks = assess(input.command)
92 if (risks.length === 0) return verdict
93
94 return {
95 decision: 'ask',
96 reason: `Blast Radius: ${summarize(risks)}. The Blast Radius pane shows what it would touch.`,
97 }
98 })
99
100 on('command.run', { command: 'blast-radius' }, async ($, e) => {
101 const command = e.args.trim()
102 if (command === '') {
103 await $.ui.open({ id: PANE, title: 'Blast Radius' })
104 return { text: 'Blast Radius pane opened.' }
105 }
106
107 const risks = assess(command)
108 if (risks.length === 0) return { text: `Blast Radius: nothing risky found in: ${command}` }
109
110 const id = `dry-${await $.clock.now()}`
111 await record($, id, command, risks)
112 await setStatus($, id, { findings: await probe($, risks), status: 'dry-run' })
113 await $.ui.open({ id: PANE, title: 'Blast Radius' })
114
115 return { text: `Blast Radius (dry run): ${summarize(risks)}. See the pane for what it would touch.` }
116 })
117
118 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
119 const { Box, Button, Text } = $.ui.resolve(e)
120 const [latest, ...earlier] = await read($, reports)
121
122 if (latest === undefined) {
123 return (
124 <Box flexDirection="column">
125 <Text dimColor>No risky commands caught yet.</Text>
126 <Text dimColor>Try: /blast-radius git reset --hard</Text>
127 </Box>
128 )
129 }
130
131 const color = latest.severity === 'critical' ? 'red' : 'yellow'
132
133 return (
134 <Box flexDirection="column">
135 <Text color={color} bold>
136 {latest.severity === 'critical' ? '✖ CRITICAL' : '⚠ RISKY'} {latest.label}
137 </Text>
138 <Text dimColor>
139 {STATUS_TEXT[latest.status]}
140 </Text>
141 <Text wrap="truncate-end">$ {latest.command}</Text>
142 {latest.findings.map(finding => (
143 <Box flexDirection="column" marginTop={1}>
144 <Text bold>{finding.label}</Text>
145 {finding.lines.map(line => (
146 <Text wrap="truncate-end"> {line}</Text>
147 ))}
148 {finding.more > 0 && <Text dimColor> … {finding.more} more</Text>}
149 </Box>
150 ))}
151 {earlier.length > 0 && (
152 <Box flexDirection="column" marginTop={1}>
153 <Text bold>Earlier</Text>
154 {earlier.map(report => (
155 <Text dimColor wrap="truncate-end">
156 {report.status === 'ran' ? '✓' : '·'} {report.label} ({STATUS_TEXT[report.status]})
157 </Text>
158 ))}
159 </Box>
160 )}
161 <Box marginTop={1}>
162 <Button key="clear" label="clear history" onPress={() => update($, reports, () => [])} />
163 </Box>
164 </Box>
165 )
166 })
167}
168hooks/risk.ts 189 lines1import type { Severity } from '../types'
2
3export type Probe = { label: string; argv: string[] }
4
5export type Risk = {
6 label: string
7 severity: Severity
8 /** Where the probes run: the session's directory, or the one a leading `cd` moved to. */
9 cwd?: string
10 probes: Probe[]
11}
12
13const CATASTROPHIC_TARGETS = new Set(['/', '/*', '~', '~/', '$HOME', '.', './', '*', '..', '../'])
14const FORCE_PUSH_FLAGS = new Set(['-f', '--force', '--force-with-lease', '--force-if-includes'])
15const SQL_DESTRUCTION = /\b(drop\s+(table|database|schema)|truncate(\s+table)?\s+\w)/i
16const MAX_TARGETS = 3
17
18/** Splits a shell line on `&&`, `||`, `;`, `|` and newlines (quotes respected). */
19export function segments(command: string): string[] {
20 const parts: string[] = []
21 let current = ''
22 let quote: string | null = null
23 for (let i = 0; i < command.length; i += 1) {
24 const char = command.charAt(i)
25 if (quote !== null) {
26 if (char === quote) quote = null
27 current += char
28 continue
29 }
30 if (char === '"' || char === "'") {
31 quote = char
32 current += char
33 continue
34 }
35 const pair = command.slice(i, i + 2)
36 if (pair === '&&' || pair === '||') {
37 parts.push(current)
38 current = ''
39 i += 1
40 continue
41 }
42 if (char === ';' || char === '|' || char === '\n') {
43 parts.push(current)
44 current = ''
45 continue
46 }
47 current += char
48 }
49 parts.push(current)
50 return parts.map(part => part.trim()).filter(part => part.length > 0)
51}
52
53/** Whitespace tokens with quotes stripped; leading `sudo` and `VAR=value` dropped. */
54export function tokens(segment: string): string[] {
55 const found = segment.match(/"[^"]*"|'[^']*'|\S+/g) ?? []
56 const words = found.map(word => word.replace(/^(["'])(.*)\1$/, '$2'))
57 while (words.length > 0 && (words[0] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0] ?? ''))) {
58 words.shift()
59 }
60 return words
61}
62
63function shortFlags(words: readonly string[]): string {
64 return words
65 .filter(word => /^-[A-Za-z]+$/.test(word))
66 .map(word => word.slice(1))
67 .join('')
68}
69
70function rmRisk(args: readonly string[]): Risk | null {
71 const flags = shortFlags(args)
72 const isRecursive = /[rR]/.test(flags) || args.includes('--recursive')
73 const isForced = flags.includes('f') || args.includes('--force')
74 if (!isRecursive || !isForced) return null
75
76 const targets = args.filter(arg => !arg.startsWith('-'))
77 const isCatastrophic = targets.some(target => CATASTROPHIC_TARGETS.has(target))
78 const probed = targets.filter(target => !/[*?$]/.test(target)).slice(0, MAX_TARGETS)
79
80 return {
81 label: `rm -rf ${targets.join(' ') || '(no target)'}`,
82 severity: isCatastrophic ? 'critical' : 'high',
83 probes: probed.flatMap(target => [
84 { label: `size of ${target}`, argv: ['du', '-sh', '--', target] },
85 { label: `contents of ${target}`, argv: ['find', target, '-maxdepth', '2'] },
86 ]),
87 }
88}
89
90const WORKTREE_PROBES: Probe[] = [
91 { label: 'uncommitted changes that would be lost', argv: ['git', 'status', '--short'] },
92 { label: 'diff against HEAD', argv: ['git', 'diff', '--stat', 'HEAD'] },
93]
94
95function gitRisk(args: readonly string[]): Risk | null {
96 const rest = [...args]
97 while (rest[0] === '-C' || rest[0] === '-c') rest.splice(0, 2)
98 const [sub, ...params] = rest
99
100 if (sub === 'reset' && params.includes('--hard')) {
101 return { label: 'git reset --hard', severity: 'high', probes: WORKTREE_PROBES }
102 }
103 if (sub === 'push' && params.some(arg => FORCE_PUSH_FLAGS.has(arg) || /^\+/.test(arg))) {
104 return {
105 label: 'git push --force',
106 severity: 'high',
107 probes: [
108 { label: 'branch', argv: ['git', 'rev-parse', '--abbrev-ref', 'HEAD'] },
109 {
110 label: 'remote commits that would be overwritten',
111 argv: ['git', 'log', '--oneline', '-n', '15', 'HEAD..@{upstream}'],
112 },
113 { label: 'local commits being pushed', argv: ['git', 'log', '--oneline', '-n', '15', '@{upstream}..HEAD'] },
114 ],
115 }
116 }
117 if (sub === 'clean' && (shortFlags(params).includes('f') || params.includes('--force'))) {
118 const extra = shortFlags(params).includes('x') ? ['-x'] : shortFlags(params).includes('X') ? ['-X'] : []
119 return {
120 label: 'git clean -f',
121 severity: 'high',
122 probes: [{ label: 'files that would be deleted', argv: ['git', 'clean', '-n', '-d', ...extra] }],
123 }
124 }
125 if ((sub === 'checkout' || sub === 'restore') && params.includes('.') && !params.includes('--staged')) {
126 return { label: `git ${sub} .`, severity: 'high', probes: WORKTREE_PROBES }
127 }
128 if (sub === 'branch' && (params.includes('-D') || (params.includes('--delete') && params.includes('--force')))) {
129 const branch = params.find(arg => !arg.startsWith('-'))
130 return {
131 label: `git branch -D ${branch ?? ''}`.trim(),
132 severity: 'high',
133 probes:
134 branch === undefined
135 ? []
136 : [
137 {
138 label: `commits only on ${branch}`,
139 argv: ['git', 'log', '--oneline', '-n', '15', branch, '--not', '--remotes'],
140 },
141 ],
142 }
143 }
144 if (sub === 'stash' && (params[0] === 'drop' || params[0] === 'clear')) {
145 return {
146 label: `git stash ${params[0]}`,
147 severity: 'high',
148 probes: [{ label: 'stashes', argv: ['git', 'stash', 'list'] }],
149 }
150 }
151 return null
152}
153
154function segmentRisk(segment: string): Risk | null {
155 if (SQL_DESTRUCTION.test(segment)) {
156 return { label: 'destructive SQL (DROP / TRUNCATE)', severity: 'critical', probes: [] }
157 }
158 const [program, ...args] = tokens(segment)
159 if (program === 'rm') return rmRisk(args)
160 if (program === 'git') return gitRisk(args)
161 if (program === 'mkfs' || program?.startsWith('mkfs.')) {
162 return { label: program, severity: 'critical', probes: [] }
163 }
164 if (program === 'dd' && args.some(arg => arg.startsWith('of=/dev/'))) {
165 return { label: 'dd to a device', severity: 'critical', probes: [] }
166 }
167 return null
168}
169
170/** Every risky segment of a shell command, each with the probes that size it. */
171export function assess(command: string): Risk[] {
172 const risks: Risk[] = []
173 let cwd: string | undefined
174 for (const segment of segments(command)) {
175 const [program, target] = tokens(segment)
176 if (program === 'cd' && target !== undefined) {
177 cwd = target
178 continue
179 }
180 const risk = segmentRisk(segment)
181 if (risk !== null) risks.push(cwd === undefined ? risk : { ...risk, cwd })
182 }
183 return risks
184}
185
186export function summarize(risks: readonly Risk[]): string {
187 return risks.map(risk => risk.label).join('; ')
188}
189types/index.d.ts 21 lines1export type Severity = 'high' | 'critical'
2
3export type ReportStatus = 'checking' | 'awaiting' | 'ran' | 'stopped' | 'dry-run'
4
5export type Finding = { label: string; lines: string[]; more: number }
6
7export type Report = {
8 id: string
9 command: string
10 label: string
11 severity: Severity
12 findings: Finding[]
13 status: ReportStatus
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'blast-radius': { reports: Report[] }
19 }
20}
21