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

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.
/plugin marketplace add namtrdev/claude-mods
/plugin install danger-guard@jackie-mods
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.
You set these when you install the mod, and can change them later in the Claude Code config menu.
allowSudo, default false): Skip the confirmation for commands that use sudo.forcePush, default ask): What to do before git push --force.recursiveDelete, default ask): What to do before a recursive or forced rm outside the root and home cases, which are always refused.extraCommands, default empty): Regular expressions for commands you want to be asked about, comma separated. Example: terraform apply,docker system pruneextraPaths, default empty): Regular expressions for file paths that need confirmation before a write, comma separated. Example: /prod/,\.pem$MIT, Copyright (c) 2026 Jackie.
hooks/register.tsx 87 lines1import { 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}
87types/index.d.ts 8 lines1export 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