-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathblock-dangerous-commands.js
More file actions
executable file
·116 lines (103 loc) · 6.67 KB
/
block-dangerous-commands.js
File metadata and controls
executable file
·116 lines (103 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/env node
/**
* Block Dangerous Commands - PreToolUse Hook for Bash
* Blocks dangerous patterns before execution. Logs to: ~/.claude/hooks-logs/
*
* SAFETY_LEVEL: 'critical' | 'high' | 'strict'
* critical - Only catastrophic: rm -rf ~, dd to disk, fork bombs
* high - + risky: force push main, secrets exposure, git reset --hard
* strict - + cautionary: any force push, sudo rm, docker prune
*
* Setup in .claude/settings.json:
* {
* "hooks": {
* "PreToolUse": [{
* "matcher": "Bash",
* "hooks": [{ "type": "command", "command": "node /path/to/block-dangerous-commands.js" }]
* }]
* }
* }
*/
const fs = require('fs');
const path = require('path');
const SAFETY_LEVEL = 'high';
const PATTERNS = [
// CRITICAL - Catastrophic, unrecoverable
{ level: 'critical', id: 'rm-home', regex: /\brm\s+(-.+\s+)*["']?~\/?["']?(\s|$|[;&|])/, reason: 'rm targeting home directory' },
{ level: 'critical', id: 'rm-home-var', regex: /\brm\s+(-.+\s+)*["']?\$HOME["']?(\s|$|[;&|])/, reason: 'rm targeting $HOME' },
{ level: 'critical', id: 'rm-home-trailing', regex: /\brm\s+.+\s+["']?(~\/?|\$HOME)["']?(\s*$|[;&|])/, reason: 'rm with trailing ~/ or $HOME' },
{ level: 'critical', id: 'rm-root', regex: /\brm\s+(-.+\s+)*\/(\*|\s|$|[;&|])/, reason: 'rm targeting root filesystem' },
{ level: 'critical', id: 'rm-system', regex: /\brm\s+(-.+\s+)*\/(etc|usr|var|bin|sbin|lib|boot|dev|proc|sys)(\/|\s|$)/, reason: 'rm targeting system directory' },
{ level: 'critical', id: 'rm-cwd', regex: /\brm\s+(-.+\s+)*(\.\/?|\*|\.\/\*)(\s|$|[;&|])/, reason: 'rm deleting current directory contents' },
{ level: 'critical', id: 'dd-disk', regex: /\bdd\b.+of=\/dev\/(sd[a-z]|nvme|hd[a-z]|vd[a-z]|xvd[a-z])/, reason: 'dd writing to disk device' },
{ level: 'critical', id: 'mkfs', regex: /\bmkfs(\.\w+)?\s+\/dev\/(sd[a-z]|nvme|hd[a-z]|vd[a-z])/, reason: 'mkfs formatting disk' },
{ level: 'critical', id: 'fork-bomb', regex: /:\(\)\s*\{.*:\s*\|\s*:.*&/, reason: 'fork bomb detected' },
// HIGH - Significant risk, data loss, security
{ level: 'high', id: 'curl-pipe-sh', regex: /\b(curl|wget)\b.+\|\s*(ba)?sh\b/, reason: 'piping URL to shell (RCE risk)' },
{ level: 'high', id: 'git-force-main', regex: /\bgit\s+push\b(?!.+--force-with-lease).+(--force|-f)\b.+\b(main|master)\b/, reason: 'force push to main/master' },
{ level: 'high', id: 'git-reset-hard', regex: /\bgit\s+reset\s+--hard/, reason: 'git reset --hard loses uncommitted work' },
{ level: 'high', id: 'git-clean-f', regex: /\bgit\s+clean\s+(-\w*f|-f)/, reason: 'git clean -f deletes untracked files' },
{ level: 'high', id: 'chmod-777', regex: /\bchmod\b.+\b777\b/, reason: 'chmod 777 is a security risk' },
{ level: 'high', id: 'cat-env', regex: /\b(cat|less|head|tail|more)\s+\.env\b/, reason: 'reading .env file exposes secrets' },
{ level: 'high', id: 'cat-secrets', regex: /\b(cat|less|head|tail|more)\b.+(credentials|secrets?|\.pem|\.key|id_rsa|id_ed25519)/i, reason: 'reading secrets file' },
{ level: 'high', id: 'env-dump', regex: /\b(printenv|^env)\s*([;&|]|$)/, reason: 'env dump may expose secrets' },
{ level: 'high', id: 'echo-secret', regex: /\becho\b.+\$\w*(SECRET|KEY|TOKEN|PASSWORD|API_|PRIVATE)/i, reason: 'echoing secret variable' },
{ level: 'high', id: 'docker-vol-rm', regex: /\bdocker\s+volume\s+(rm|prune)/, reason: 'docker volume deletion loses data' },
{ level: 'high', id: 'rm-ssh', regex: /\brm\b.+\.ssh\/(id_|authorized_keys|known_hosts)/, reason: 'deleting SSH keys' },
// STRICT - Cautionary, context-dependent
{ level: 'strict', id: 'git-force-any', regex: /\bgit\s+push\b(?!.+--force-with-lease).+(--force|-f)\b/, reason: 'force push (use --force-with-lease)' },
{ level: 'strict', id: 'git-checkout-dot', regex: /\bgit\s+checkout\s+\./, reason: 'git checkout . discards changes' },
{ level: 'strict', id: 'sudo-rm', regex: /\bsudo\s+rm\b/, reason: 'sudo rm has elevated privileges' },
{ level: 'strict', id: 'docker-prune', regex: /\bdocker\s+(system|image)\s+prune/, reason: 'docker prune removes images' },
{ level: 'strict', id: 'crontab-r', regex: /\bcrontab\s+-r/, reason: 'removes all cron jobs' },
];
const LEVELS = { critical: 1, high: 2, strict: 3 };
const EMOJIS = { critical: '🚨', high: '⛔', strict: '⚠️' };
const LOG_DIR = path.join(process.env.HOME, '.claude', 'hooks-logs');
function log(data) {
try {
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
const file = path.join(LOG_DIR, `${new Date().toISOString().slice(0, 10)}.jsonl`);
fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), ...data }) + '\n');
} catch {}
}
function checkCommand(cmd, safetyLevel = SAFETY_LEVEL) {
const threshold = LEVELS[safetyLevel] || 2;
for (const p of PATTERNS) {
if (LEVELS[p.level] <= threshold && p.regex.test(cmd)) {
return { blocked: true, pattern: p };
}
}
return { blocked: false, pattern: null };
}
async function main() {
let input = '';
for await (const chunk of process.stdin) input += chunk;
try {
const data = JSON.parse(input);
const { tool_name, tool_input, session_id, cwd, permission_mode } = data;
if (tool_name !== 'Bash') return console.log('{}');
const cmd = tool_input?.command || '';
const result = checkCommand(cmd);
if (result.blocked) {
const p = result.pattern;
log({ level: 'BLOCKED', id: p.id, priority: p.level, cmd, session_id, cwd, permission_mode });
return console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: `${EMOJIS[p.level]} [${p.id}] ${p.reason}`
}
}));
}
console.log('{}');
} catch (e) {
log({ level: 'ERROR', error: e.message });
console.log('{}');
}
}
if (require.main === module) {
main();
} else {
module.exports = { PATTERNS, LEVELS, SAFETY_LEVEL, checkCommand };
}