SLOPSHOPPER

danger-guard

Asks or blocks before destructive commands and writes to sensitive paths.

newguardtoast
v0.1.0MITupdated 2026-10-26namtrdev/claude-mods/danger-guard
A shopper browsing a rack in a slop shop
README

Danger Guard

Asks for confirmation before destructive commands such as recursive deletes, force pushes, hard resets, sudo and DROP TABLE, refuses catastrophic ones outright, and asks before writes to sensitive paths like ~/.ssh.

Install

/plugin marketplace add namtrdev/claude-mods
/plugin install danger-guard@jackie-mods

Use

Commands: runs automatically.

Built for Claude Code mods (hook modules). Nothing here runs outside your machine: the mod reads session events and draws its own interface.

Options

You set these when you install the mod, and can change them later in the Claude Code config menu.

  • Allow sudo without asking (allowSudo, default false): Skip the confirmation for commands that use sudo.
  • Force push (forcePush, default ask): What to do before git push --force.
  • Recursive delete (rm -rf) (recursiveDelete, default ask): What to do before a recursive or forced rm outside the root and home cases, which are always refused.
  • Extra commands to confirm (extraCommands, default empty): Regular expressions for commands you want to be asked about, comma separated. Example: terraform apply,docker system prune
  • Extra protected paths (extraPaths, default empty): Regular expressions for file paths that need confirmation before a write, comma separated. Example: /prod/,\.pem$

License

MIT, Copyright (c) 2026 Jackie.

Source 2 files
hooks/register.tsx 87 lines
1import { atom, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { GuardEvent } from '../types'
5
6const events = atom({ plugin: 'danger-guard', key: 'events' } as const, [])
7
8// deny: no sensible reason to ever do this; ask: legitimate but irreversible.
9const RULES: { re: RegExp; verdict: 'ask' | 'deny'; reason: string }[] = [
10  { re: /\brm\s+(-\w*\s+)*-\w*[rf]\w*\s+(-\w+\s+)*(\/|~|\$HOME|\/\*)(\s|$)/, verdict: 'deny', reason: 'recursive delete of root or home' },
11  { re: /:\(\)\s*\{\s*:\|:&\s*\};:/, verdict: 'deny', reason: 'fork bomb' },
12  { re: />\s*\/dev\/(sd|nvme|disk)\w*/, verdict: 'deny', reason: 'writes to a raw disk' },
13  { re: /\bmkfs\b|\bdd\s+.*\bof=\/dev\//, verdict: 'deny', reason: 'formats or overwrites a disk' },
14  { re: /\brm\s+(-\w*\s+)*-\w*[rf]\w*\b/, verdict: 'ask', reason: 'recursive or forced delete' },
15  { re: /\bgit\s+push\b.*(--force\b|\s-f\b)/, verdict: 'ask', reason: 'force push rewrites remote history' },
16  { re: /\bgit\s+(reset\s+--hard|clean\s+-\w*f|checkout\s+--\s|restore\s+\.)/, verdict: 'ask', reason: 'discards uncommitted work' },
17  { re: /\bgit\s+branch\s+-D\b/, verdict: 'ask', reason: 'deletes a branch with unmerged work' },
18  { re: /\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(ba|z)?sh\b/, verdict: 'ask', reason: 'runs a remote script' },
19  { re: /\bchmod\s+(-R\s+)?[0-7]*777\b/, verdict: 'ask', reason: 'world-writable permissions' },
20  { re: /\bsudo\b/, verdict: 'ask', reason: 'runs with root privileges' },
21  { re: /\bdrop\s+(table|database|schema)\b|\btruncate\s+table\b/i, verdict: 'ask', reason: 'drops database data' },
22  { re: /\b(kubectl|helm)\s+(delete|uninstall)\b|\bterraform\s+destroy\b/, verdict: 'ask', reason: 'deletes infrastructure' },
23]
24
25const SENSITIVE_PATH = /(^|\/)(\.ssh|\.aws|\.gnupg|\.kube)\/|(^|\/)\.(bashrc|zshrc|zprofile|profile)$|^\/etc\//
26
27const parse = (raw: unknown) =>
28  String(raw ?? '')
29    .split(',')
30    .map(one => one.trim())
31    .filter(one => one !== '')
32    .flatMap(one => {
33      try {
34        return [new RegExp(one)]
35      } catch {
36        return []
37      }
38    })
39
40export const register: Register = (on, options) => {
41  const rules = RULES.flatMap(rule => {
42    if (options.allowSudo === true && rule.reason === 'runs with root privileges') return []
43    if (rule.reason === 'force push rewrites remote history' && options.forcePush === 'allow') return []
44    if (rule.reason === 'recursive or forced delete' && options.recursiveDelete === 'allow') return []
45    if (rule.reason === 'force push rewrites remote history' && options.forcePush === 'deny') return [{ ...rule, verdict: 'deny' as const }]
46    if (rule.reason === 'recursive or forced delete' && options.recursiveDelete === 'deny') return [{ ...rule, verdict: 'deny' as const }]
47    return [rule]
48  })
49  const extraCommands = parse(options.extraCommands)
50  const extraPaths = parse(options.extraPaths)
51
52  on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
53    const command = String((e.input as { command?: unknown }).command ?? '')
54    const rule =
55      rules.find(one => one.re.test(command)) ??
56      (extraCommands.some(re => re.test(command))
57        ? { verdict: 'ask' as const, reason: 'matches one of your commands to confirm' }
58        : undefined)
59    if (rule === undefined) {
60      return next(e)
61    }
62    const entry: GuardEvent = {
63      at: await $.clock.now(),
64      verdict: rule.verdict,
65      reason: rule.reason,
66      what: command.slice(0, 120),
67    }
68    await update($, events, list => [...list, entry].slice(-50))
69    $.ui.toast(`danger-guard: ${rule.verdict} · ${rule.reason}`)
70
71    return { decision: rule.verdict, reason: `danger-guard: ${rule.reason}` }
72  })
73
74  for (const tool of ['Write', 'Edit', 'MultiEdit'] as const) {
75    on('tool.check', { tool }, async ($, e, next) => {
76      const path = String((e.input as { file_path?: unknown }).file_path ?? '')
77      if (!SENSITIVE_PATH.test(path) && !extraPaths.some(re => re.test(path))) {
78        return next(e)
79      }
80      const entry: GuardEvent = { at: await $.clock.now(), verdict: 'ask', reason: 'sensitive path', what: path }
81      await update($, events, list => [...list, entry].slice(-50))
82
83      return { decision: 'ask', reason: `danger-guard: ${path} is a sensitive path` }
84    })
85  }
86}
87
types/index.d.ts 8 lines
1export type GuardEvent = { at: number; verdict: string; reason: string; what: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'danger-guard': { events: GuardEvent[] }
6  }
7}
8