@jeremyy_prt/cc-config 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/agents/corriger-orthographe.md +49 -0
- package/agents/explorer-code.md +63 -0
- package/agents/explorer-docs.md +87 -0
- package/agents/recherche-web.md +46 -0
- package/cli.js +213 -0
- package/commands/commit.md +47 -0
- package/commands/corriger-orthographe.md +59 -0
- package/commands/creer-agent.md +126 -0
- package/commands/creer-commande.md +225 -0
- package/commands/liste-commande.md +103 -0
- package/commands/memoire-claude.md +190 -0
- package/commands/surveiller-ci.md +65 -0
- package/package.json +44 -0
- package/scripts/statusline/CLAUDE.md +178 -0
- package/scripts/statusline/README.md +105 -0
- package/scripts/statusline/biome.json +34 -0
- package/scripts/statusline/bun.lockb +0 -0
- package/scripts/statusline/data/.gitignore +5 -0
- package/scripts/statusline/fixtures/test-input.json +25 -0
- package/scripts/statusline/package.json +21 -0
- package/scripts/statusline/src/commands/CLAUDE.md +3 -0
- package/scripts/statusline/src/commands/spend-month.ts +60 -0
- package/scripts/statusline/src/commands/spend-today.ts +42 -0
- package/scripts/statusline/src/index.ts +199 -0
- package/scripts/statusline/src/lib/context.ts +103 -0
- package/scripts/statusline/src/lib/formatters.ts +218 -0
- package/scripts/statusline/src/lib/git.ts +100 -0
- package/scripts/statusline/src/lib/spend.ts +119 -0
- package/scripts/statusline/src/lib/types.ts +25 -0
- package/scripts/statusline/src/lib/usage-limits.ts +147 -0
- package/scripts/statusline/statusline.config.ts +125 -0
- package/scripts/statusline/test.ts +20 -0
- package/scripts/statusline/tsconfig.json +27 -0
- package/scripts/validate-command.js +707 -0
- package/scripts/validate-command.readme.md +283 -0
- package/settings.json +42 -0
- package/song/finish.mp3 +0 -0
- package/song/need-human.mp3 +0 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code "Before Tools" Hook - Command Validation Script
|
|
5
|
+
*
|
|
6
|
+
* This script validates commands before execution to prevent harmful operations.
|
|
7
|
+
* It receives command data via stdin and returns exit code 0 (allow) or 1 (block).
|
|
8
|
+
*
|
|
9
|
+
* Usage: Called automatically by Claude Code PreToolUse hook
|
|
10
|
+
* Manual test: echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bun validate-command.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Comprehensive dangerous command patterns database
|
|
14
|
+
const SECURITY_RULES = {
|
|
15
|
+
// Critical system destruction commands
|
|
16
|
+
CRITICAL_COMMANDS: [
|
|
17
|
+
"del",
|
|
18
|
+
"format",
|
|
19
|
+
"mkfs",
|
|
20
|
+
"shred",
|
|
21
|
+
"dd",
|
|
22
|
+
"fdisk",
|
|
23
|
+
"parted",
|
|
24
|
+
"gparted",
|
|
25
|
+
"cfdisk",
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
// Privilege escalation and system access
|
|
29
|
+
PRIVILEGE_COMMANDS: [
|
|
30
|
+
"sudo",
|
|
31
|
+
"su",
|
|
32
|
+
"passwd",
|
|
33
|
+
"chpasswd",
|
|
34
|
+
"usermod",
|
|
35
|
+
"chmod",
|
|
36
|
+
"chown",
|
|
37
|
+
"chgrp",
|
|
38
|
+
"setuid",
|
|
39
|
+
"setgid",
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
// Network and remote access tools
|
|
43
|
+
NETWORK_COMMANDS: [
|
|
44
|
+
"nc",
|
|
45
|
+
"netcat",
|
|
46
|
+
"nmap",
|
|
47
|
+
"telnet",
|
|
48
|
+
"ssh-keygen",
|
|
49
|
+
"iptables",
|
|
50
|
+
"ufw",
|
|
51
|
+
"firewall-cmd",
|
|
52
|
+
"ipfw",
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
// System service and process manipulation
|
|
56
|
+
SYSTEM_COMMANDS: [
|
|
57
|
+
"systemctl",
|
|
58
|
+
"service",
|
|
59
|
+
"kill",
|
|
60
|
+
"killall",
|
|
61
|
+
"pkill",
|
|
62
|
+
"mount",
|
|
63
|
+
"umount",
|
|
64
|
+
"swapon",
|
|
65
|
+
"swapoff",
|
|
66
|
+
],
|
|
67
|
+
|
|
68
|
+
// Dangerous regex patterns
|
|
69
|
+
DANGEROUS_PATTERNS: [
|
|
70
|
+
// File system destruction - block rm -rf with absolute paths (checked separately)
|
|
71
|
+
/rm\s+.*-rf\s*\/\s*$/i, // rm -rf ending at root directory
|
|
72
|
+
/rm\s+.*-rf\s*\/etc/i, // rm -rf in /etc
|
|
73
|
+
/rm\s+.*-rf\s*\/usr/i, // rm -rf in /usr
|
|
74
|
+
/rm\s+.*-rf\s*\/bin/i, // rm -rf in /bin
|
|
75
|
+
/rm\s+.*-rf\s*\/sys/i, // rm -rf in /sys
|
|
76
|
+
/rm\s+.*-rf\s*\/proc/i, // rm -rf in /proc
|
|
77
|
+
/rm\s+.*-rf\s*\/boot/i, // rm -rf in /boot
|
|
78
|
+
/rm\s+.*-rf\s*\/home\/[^\/]*\s*$/i, // rm -rf entire home directory
|
|
79
|
+
/rm\s+.*-rf\s*\.\.+\//i, // rm -rf with parent directory traversal
|
|
80
|
+
/rm\s+.*-rf\s*\*.*\*/i, // rm -rf with multiple wildcards
|
|
81
|
+
/rm\s+.*-rf\s*\$\w+/i, // rm -rf with variables (could be dangerous)
|
|
82
|
+
/>\s*\/dev\/(sda|hda|nvme)/i,
|
|
83
|
+
/dd\s+.*of=\/dev\//i,
|
|
84
|
+
/shred\s+.*\/dev\//i,
|
|
85
|
+
/mkfs\.\w+\s+\/dev\//i,
|
|
86
|
+
|
|
87
|
+
// Fork bomb and resource exhaustion
|
|
88
|
+
/:\(\)\{\s*:\|:&\s*\};:/,
|
|
89
|
+
/while\s+true\s*;\s*do.*done/i,
|
|
90
|
+
/for\s*\(\(\s*;\s*;\s*\)\)/i,
|
|
91
|
+
|
|
92
|
+
// Command injection (but allow general chaining - we'll validate each command separately)
|
|
93
|
+
// /;\s*(rm|dd|mkfs|format)/i, // Commented out - handled by individual command validation
|
|
94
|
+
// /&&\s*(rm|dd|mkfs|format)/i, // Commented out - handled by individual command validation
|
|
95
|
+
// /\|\|\s*(rm|dd|mkfs|format)/i, // Commented out - handled by individual command validation
|
|
96
|
+
|
|
97
|
+
// Remote code execution
|
|
98
|
+
/\|\s*(sh|bash|zsh|fish)$/i,
|
|
99
|
+
/(wget|curl)\s+.*\|\s*(sh|bash)/i,
|
|
100
|
+
/(wget|curl)\s+.*-O-.*\|\s*(sh|bash)/i,
|
|
101
|
+
|
|
102
|
+
// Command substitution with dangerous commands
|
|
103
|
+
/`.*rm.*`/i,
|
|
104
|
+
/\$\(.*rm.*\)/i,
|
|
105
|
+
/`.*dd.*`/i,
|
|
106
|
+
/\$\(.*dd.*\)/i,
|
|
107
|
+
|
|
108
|
+
// Sensitive file access
|
|
109
|
+
/cat\s+\/etc\/(passwd|shadow|sudoers)/i,
|
|
110
|
+
/>\s*\/etc\/(passwd|shadow|sudoers)/i,
|
|
111
|
+
/echo\s+.*>>\s*\/etc\/(passwd|shadow|sudoers)/i,
|
|
112
|
+
|
|
113
|
+
// Network exfiltration
|
|
114
|
+
/\|\s*nc\s+\S+\s+\d+/i,
|
|
115
|
+
/curl\s+.*-d.*\$\(/i,
|
|
116
|
+
/wget\s+.*--post-data.*\$\(/i,
|
|
117
|
+
|
|
118
|
+
// Log manipulation
|
|
119
|
+
/>\s*\/var\/log\//i,
|
|
120
|
+
/rm\s+\/var\/log\//i,
|
|
121
|
+
/echo\s+.*>\s*~?\/?\.bash_history/i,
|
|
122
|
+
|
|
123
|
+
// Backdoor creation
|
|
124
|
+
/nc\s+.*-l.*-e/i,
|
|
125
|
+
/nc\s+.*-e.*-l/i,
|
|
126
|
+
/ncat\s+.*--exec/i,
|
|
127
|
+
/ssh-keygen.*authorized_keys/i,
|
|
128
|
+
|
|
129
|
+
// Crypto mining and malicious downloads
|
|
130
|
+
/(wget|curl).*\.(sh|py|pl|exe|bin).*\|\s*(sh|bash|python)/i,
|
|
131
|
+
/(xmrig|ccminer|cgminer|bfgminer)/i,
|
|
132
|
+
|
|
133
|
+
// Hardware direct access
|
|
134
|
+
/cat\s+\/dev\/(mem|kmem)/i,
|
|
135
|
+
/echo\s+.*>\s*\/dev\/(mem|kmem)/i,
|
|
136
|
+
|
|
137
|
+
// Kernel module manipulation
|
|
138
|
+
/(insmod|rmmod|modprobe)\s+/i,
|
|
139
|
+
|
|
140
|
+
// Cron job manipulation
|
|
141
|
+
/crontab\s+-e/i,
|
|
142
|
+
/echo\s+.*>>\s*\/var\/spool\/cron/i,
|
|
143
|
+
|
|
144
|
+
// Environment variable exposure
|
|
145
|
+
/env\s*\|\s*grep.*PASSWORD/i,
|
|
146
|
+
/printenv.*PASSWORD/i,
|
|
147
|
+
],
|
|
148
|
+
|
|
149
|
+
// Paths that should never be written to
|
|
150
|
+
PROTECTED_PATHS: [
|
|
151
|
+
"/etc/",
|
|
152
|
+
"/usr/",
|
|
153
|
+
"/bin/",
|
|
154
|
+
"/sbin/",
|
|
155
|
+
"/boot/",
|
|
156
|
+
"/sys/",
|
|
157
|
+
"/proc/",
|
|
158
|
+
"/dev/",
|
|
159
|
+
"/root/",
|
|
160
|
+
],
|
|
161
|
+
|
|
162
|
+
// Safe paths where rm -rf is allowed
|
|
163
|
+
SAFE_RM_PATHS: [
|
|
164
|
+
"/Users/melvynx/Developer/",
|
|
165
|
+
"/tmp/",
|
|
166
|
+
"/var/tmp/",
|
|
167
|
+
process.cwd() + "/", // Current working directory
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Allowlist of safe commands (when used appropriately)
|
|
172
|
+
const SAFE_COMMANDS = [
|
|
173
|
+
"ls",
|
|
174
|
+
"dir",
|
|
175
|
+
"pwd",
|
|
176
|
+
"whoami",
|
|
177
|
+
"date",
|
|
178
|
+
"echo",
|
|
179
|
+
"cat",
|
|
180
|
+
"head",
|
|
181
|
+
"tail",
|
|
182
|
+
"grep",
|
|
183
|
+
"find",
|
|
184
|
+
"wc",
|
|
185
|
+
"sort",
|
|
186
|
+
"uniq",
|
|
187
|
+
"cut",
|
|
188
|
+
"awk",
|
|
189
|
+
"sed",
|
|
190
|
+
"git",
|
|
191
|
+
"npm",
|
|
192
|
+
"pnpm",
|
|
193
|
+
"node",
|
|
194
|
+
"bun",
|
|
195
|
+
"python",
|
|
196
|
+
"pip",
|
|
197
|
+
"source",
|
|
198
|
+
"cd",
|
|
199
|
+
"cp",
|
|
200
|
+
"mv",
|
|
201
|
+
"mkdir",
|
|
202
|
+
"touch",
|
|
203
|
+
"ln",
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
class CommandValidator {
|
|
207
|
+
constructor() {
|
|
208
|
+
this.logFile = "/Users/melvynx/.claude/security.log";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Main validation function
|
|
213
|
+
*/
|
|
214
|
+
validate(command, toolName = "Unknown") {
|
|
215
|
+
const result = {
|
|
216
|
+
isValid: true,
|
|
217
|
+
severity: "LOW",
|
|
218
|
+
violations: [],
|
|
219
|
+
sanitizedCommand: command,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (!command || typeof command !== "string") {
|
|
223
|
+
result.isValid = false;
|
|
224
|
+
result.violations.push("Invalid command format");
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Normalize command for analysis
|
|
229
|
+
const normalizedCmd = command.trim().toLowerCase();
|
|
230
|
+
const cmdParts = normalizedCmd.split(/\s+/);
|
|
231
|
+
const mainCommand = cmdParts[0];
|
|
232
|
+
|
|
233
|
+
// Allow source and python commands unconditionally
|
|
234
|
+
if (mainCommand === "source" || mainCommand === "python") {
|
|
235
|
+
return result; // Always allow
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check against critical commands
|
|
239
|
+
if (SECURITY_RULES.CRITICAL_COMMANDS.includes(mainCommand)) {
|
|
240
|
+
result.isValid = false;
|
|
241
|
+
result.severity = "CRITICAL";
|
|
242
|
+
result.violations.push(`Critical dangerous command: ${mainCommand}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check privilege escalation commands
|
|
246
|
+
if (SECURITY_RULES.PRIVILEGE_COMMANDS.includes(mainCommand)) {
|
|
247
|
+
result.isValid = false;
|
|
248
|
+
result.severity = "HIGH";
|
|
249
|
+
result.violations.push(`Privilege escalation command: ${mainCommand}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check network commands
|
|
253
|
+
if (SECURITY_RULES.NETWORK_COMMANDS.includes(mainCommand)) {
|
|
254
|
+
result.isValid = false;
|
|
255
|
+
result.severity = "HIGH";
|
|
256
|
+
result.violations.push(`Network/remote access command: ${mainCommand}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check system commands
|
|
260
|
+
if (SECURITY_RULES.SYSTEM_COMMANDS.includes(mainCommand)) {
|
|
261
|
+
result.isValid = false;
|
|
262
|
+
result.severity = "HIGH";
|
|
263
|
+
result.violations.push(`System manipulation command: ${mainCommand}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for rm -rf commands first (special handling)
|
|
267
|
+
if (/rm\s+.*-rf\s/.test(command)) {
|
|
268
|
+
const isRmRfSafe = this.isRmRfCommandSafe(command);
|
|
269
|
+
if (!isRmRfSafe) {
|
|
270
|
+
result.isValid = false;
|
|
271
|
+
result.severity = "CRITICAL";
|
|
272
|
+
result.violations.push("rm -rf command targeting unsafe path");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check dangerous patterns (skip rm -rf patterns as they're handled above)
|
|
277
|
+
for (const pattern of SECURITY_RULES.DANGEROUS_PATTERNS) {
|
|
278
|
+
if (pattern.test(command) && !/rm\s+.*-rf/.test(pattern.source)) {
|
|
279
|
+
result.isValid = false;
|
|
280
|
+
result.severity = "CRITICAL";
|
|
281
|
+
result.violations.push(`Dangerous pattern detected: ${pattern.source}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Allow && chaining for safe commands like source and python
|
|
286
|
+
if (command.includes("&&")) {
|
|
287
|
+
const chainedCommands = this.splitCommandChain(command);
|
|
288
|
+
let allSafe = true;
|
|
289
|
+
for (const chainedCmd of chainedCommands) {
|
|
290
|
+
const trimmedCmd = chainedCmd.trim();
|
|
291
|
+
const cmdParts = trimmedCmd.split(/\s+/);
|
|
292
|
+
const mainCommand = cmdParts[0];
|
|
293
|
+
|
|
294
|
+
// Allow source and python commands in && chains
|
|
295
|
+
if (
|
|
296
|
+
mainCommand === "source" ||
|
|
297
|
+
mainCommand === "python" ||
|
|
298
|
+
SAFE_COMMANDS.includes(mainCommand)
|
|
299
|
+
) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const chainResult = this.validateSingleCommand(trimmedCmd, toolName);
|
|
304
|
+
if (!chainResult.isValid) {
|
|
305
|
+
result.isValid = false;
|
|
306
|
+
result.severity = chainResult.severity;
|
|
307
|
+
result.violations.push(
|
|
308
|
+
`Chained command violation: ${trimmedCmd} - ${chainResult.violations.join(
|
|
309
|
+
", "
|
|
310
|
+
)}`
|
|
311
|
+
);
|
|
312
|
+
allSafe = false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (allSafe) {
|
|
316
|
+
return result; // Allow safe && chains
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check other command chaining (; and ||)
|
|
321
|
+
if (command.includes(";") || command.includes("||")) {
|
|
322
|
+
const chainedCommands = this.splitCommandChain(command);
|
|
323
|
+
for (const chainedCmd of chainedCommands) {
|
|
324
|
+
const chainResult = this.validateSingleCommand(
|
|
325
|
+
chainedCmd.trim(),
|
|
326
|
+
toolName
|
|
327
|
+
);
|
|
328
|
+
if (!chainResult.isValid) {
|
|
329
|
+
result.isValid = false;
|
|
330
|
+
result.severity = chainResult.severity;
|
|
331
|
+
result.violations.push(
|
|
332
|
+
`Chained command violation: ${chainedCmd.trim()} - ${chainResult.violations.join(
|
|
333
|
+
", "
|
|
334
|
+
)}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check for protected path access (but allow common redirections like /dev/null)
|
|
342
|
+
for (const path of SECURITY_RULES.PROTECTED_PATHS) {
|
|
343
|
+
if (command.includes(path)) {
|
|
344
|
+
// Allow common safe redirections
|
|
345
|
+
if (
|
|
346
|
+
path === "/dev/" &&
|
|
347
|
+
(command.includes("/dev/null") ||
|
|
348
|
+
command.includes("/dev/stderr") ||
|
|
349
|
+
command.includes("/dev/stdout"))
|
|
350
|
+
) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
result.isValid = false;
|
|
354
|
+
result.severity = "HIGH";
|
|
355
|
+
result.violations.push(`Access to protected path: ${path}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Additional safety checks
|
|
360
|
+
if (command.length > 2000) {
|
|
361
|
+
result.isValid = false;
|
|
362
|
+
result.severity = "MEDIUM";
|
|
363
|
+
result.violations.push("Command too long (potential buffer overflow)");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Check for binary/encoded content
|
|
367
|
+
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(command)) {
|
|
368
|
+
result.isValid = false;
|
|
369
|
+
result.severity = "HIGH";
|
|
370
|
+
result.violations.push("Binary or encoded content detected");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Validate a single command (without chaining logic to avoid recursion)
|
|
378
|
+
*/
|
|
379
|
+
validateSingleCommand(command, toolName = "Unknown") {
|
|
380
|
+
const result = {
|
|
381
|
+
isValid: true,
|
|
382
|
+
severity: "LOW",
|
|
383
|
+
violations: [],
|
|
384
|
+
sanitizedCommand: command,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (!command || typeof command !== "string") {
|
|
388
|
+
result.isValid = false;
|
|
389
|
+
result.violations.push("Invalid command format");
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Normalize command for analysis
|
|
394
|
+
const normalizedCmd = command.trim().toLowerCase();
|
|
395
|
+
const cmdParts = normalizedCmd.split(/\s+/);
|
|
396
|
+
const mainCommand = cmdParts[0];
|
|
397
|
+
|
|
398
|
+
// Allow source and python commands unconditionally in single command validation too
|
|
399
|
+
if (mainCommand === "source" || mainCommand === "python") {
|
|
400
|
+
return result; // Always allow
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check against critical commands
|
|
404
|
+
if (SECURITY_RULES.CRITICAL_COMMANDS.includes(mainCommand)) {
|
|
405
|
+
result.isValid = false;
|
|
406
|
+
result.severity = "CRITICAL";
|
|
407
|
+
result.violations.push(`Critical dangerous command: ${mainCommand}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Check privilege escalation commands
|
|
411
|
+
if (SECURITY_RULES.PRIVILEGE_COMMANDS.includes(mainCommand)) {
|
|
412
|
+
result.isValid = false;
|
|
413
|
+
result.severity = "HIGH";
|
|
414
|
+
result.violations.push(`Privilege escalation command: ${mainCommand}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check network commands
|
|
418
|
+
if (SECURITY_RULES.NETWORK_COMMANDS.includes(mainCommand)) {
|
|
419
|
+
result.isValid = false;
|
|
420
|
+
result.severity = "HIGH";
|
|
421
|
+
result.violations.push(`Network/remote access command: ${mainCommand}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check system commands
|
|
425
|
+
if (SECURITY_RULES.SYSTEM_COMMANDS.includes(mainCommand)) {
|
|
426
|
+
result.isValid = false;
|
|
427
|
+
result.severity = "HIGH";
|
|
428
|
+
result.violations.push(`System manipulation command: ${mainCommand}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check for rm -rf commands first (special handling)
|
|
432
|
+
if (/rm\s+.*-rf\s/.test(command)) {
|
|
433
|
+
const isRmRfSafe = this.isRmRfCommandSafe(command);
|
|
434
|
+
if (!isRmRfSafe) {
|
|
435
|
+
result.isValid = false;
|
|
436
|
+
result.severity = "CRITICAL";
|
|
437
|
+
result.violations.push("rm -rf command targeting unsafe path");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Check dangerous patterns (skip rm -rf patterns as they're handled above)
|
|
442
|
+
for (const pattern of SECURITY_RULES.DANGEROUS_PATTERNS) {
|
|
443
|
+
if (pattern.test(command) && !/rm\s+.*-rf/.test(pattern.source)) {
|
|
444
|
+
result.isValid = false;
|
|
445
|
+
result.severity = "CRITICAL";
|
|
446
|
+
result.violations.push(`Dangerous pattern detected: ${pattern.source}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check for protected path access
|
|
451
|
+
for (const path of SECURITY_RULES.PROTECTED_PATHS) {
|
|
452
|
+
if (command.includes(path)) {
|
|
453
|
+
if (
|
|
454
|
+
path === "/dev/" &&
|
|
455
|
+
(command.includes("/dev/null") ||
|
|
456
|
+
command.includes("/dev/stderr") ||
|
|
457
|
+
command.includes("/dev/stdout"))
|
|
458
|
+
) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
result.isValid = false;
|
|
462
|
+
result.severity = "HIGH";
|
|
463
|
+
result.violations.push(`Access to protected path: ${path}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Additional safety checks
|
|
468
|
+
if (command.length > 2000) {
|
|
469
|
+
result.isValid = false;
|
|
470
|
+
result.severity = "MEDIUM";
|
|
471
|
+
result.violations.push("Command too long (potential buffer overflow)");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check for binary/encoded content
|
|
475
|
+
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(command)) {
|
|
476
|
+
result.isValid = false;
|
|
477
|
+
result.severity = "HIGH";
|
|
478
|
+
result.violations.push("Binary or encoded content detected");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Split command chain into individual commands
|
|
486
|
+
*/
|
|
487
|
+
splitCommandChain(command) {
|
|
488
|
+
// Simple splitting on && ; ||
|
|
489
|
+
// This is basic - doesn't handle complex quoting, but good enough for basic validation
|
|
490
|
+
const commands = [];
|
|
491
|
+
let current = "";
|
|
492
|
+
let inQuotes = false;
|
|
493
|
+
let quoteChar = "";
|
|
494
|
+
|
|
495
|
+
for (let i = 0; i < command.length; i++) {
|
|
496
|
+
const char = command[i];
|
|
497
|
+
const nextChar = command[i + 1];
|
|
498
|
+
|
|
499
|
+
// Handle quotes
|
|
500
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
501
|
+
inQuotes = true;
|
|
502
|
+
quoteChar = char;
|
|
503
|
+
current += char;
|
|
504
|
+
} else if (char === quoteChar && inQuotes) {
|
|
505
|
+
inQuotes = false;
|
|
506
|
+
quoteChar = "";
|
|
507
|
+
current += char;
|
|
508
|
+
} else if (inQuotes) {
|
|
509
|
+
current += char;
|
|
510
|
+
} else if (char === "&" && nextChar === "&") {
|
|
511
|
+
commands.push(current.trim());
|
|
512
|
+
current = "";
|
|
513
|
+
i++; // skip next &
|
|
514
|
+
} else if (char === "|" && nextChar === "|") {
|
|
515
|
+
commands.push(current.trim());
|
|
516
|
+
current = "";
|
|
517
|
+
i++; // skip next |
|
|
518
|
+
} else if (char === ";") {
|
|
519
|
+
commands.push(current.trim());
|
|
520
|
+
current = "";
|
|
521
|
+
} else {
|
|
522
|
+
current += char;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (current.trim()) {
|
|
527
|
+
commands.push(current.trim());
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return commands.filter((cmd) => cmd.length > 0);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Log security events
|
|
535
|
+
*/
|
|
536
|
+
async logSecurityEvent(command, toolName, result, sessionId = null) {
|
|
537
|
+
const timestamp = new Date().toISOString();
|
|
538
|
+
const logEntry = {
|
|
539
|
+
timestamp,
|
|
540
|
+
sessionId,
|
|
541
|
+
toolName,
|
|
542
|
+
command: command.substring(0, 500), // Truncate for logs
|
|
543
|
+
blocked: !result.isValid,
|
|
544
|
+
severity: result.severity,
|
|
545
|
+
violations: result.violations,
|
|
546
|
+
source: "claude-code-hook",
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
// Write to log file
|
|
551
|
+
const logLine = JSON.stringify(logEntry) + "\n";
|
|
552
|
+
await Bun.write(this.logFile, logLine, { createPath: true, flag: "a" });
|
|
553
|
+
|
|
554
|
+
// Also output to stderr for immediate visibility
|
|
555
|
+
console.error(
|
|
556
|
+
`[SECURITY] ${
|
|
557
|
+
result.isValid ? "ALLOWED" : "BLOCKED"
|
|
558
|
+
}: ${command.substring(0, 100)}`
|
|
559
|
+
);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error("Failed to write security log:", error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Check if rm -rf command targets a safe path
|
|
567
|
+
*/
|
|
568
|
+
isRmRfCommandSafe(command) {
|
|
569
|
+
// Extract the path from rm -rf command
|
|
570
|
+
const rmRfMatch = command.match(/rm\s+.*-rf\s+([^\s;&|]+)/);
|
|
571
|
+
if (!rmRfMatch) {
|
|
572
|
+
return false; // Couldn't parse path, block for safety
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const targetPath = rmRfMatch[1];
|
|
576
|
+
|
|
577
|
+
// Block if targeting root or ending at root
|
|
578
|
+
if (targetPath === "/" || targetPath.endsWith("/")) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Check if path starts with any safe prefix
|
|
583
|
+
for (const safePath of SECURITY_RULES.SAFE_RM_PATHS) {
|
|
584
|
+
if (targetPath.startsWith(safePath)) {
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Check if it's a relative path (safer)
|
|
590
|
+
if (!targetPath.startsWith("/")) {
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Block all other absolute paths
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Check if command matches any allowed patterns from settings
|
|
600
|
+
*/
|
|
601
|
+
isExplicitlyAllowed(command, allowedPatterns = []) {
|
|
602
|
+
for (const pattern of allowedPatterns) {
|
|
603
|
+
// Convert Claude Code permission pattern to regex
|
|
604
|
+
// e.g., "Bash(git *)" becomes /^git\s+.*$/
|
|
605
|
+
if (pattern.startsWith("Bash(") && pattern.endsWith(")")) {
|
|
606
|
+
const cmdPattern = pattern.slice(5, -1); // Remove "Bash(" and ")"
|
|
607
|
+
const regex = new RegExp(
|
|
608
|
+
"^" + cmdPattern.replace(/\*/g, ".*") + "$",
|
|
609
|
+
"i"
|
|
610
|
+
);
|
|
611
|
+
if (regex.test(command)) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Main execution function
|
|
622
|
+
*/
|
|
623
|
+
async function main() {
|
|
624
|
+
const validator = new CommandValidator();
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
// Read hook input from stdin
|
|
628
|
+
const stdin = process.stdin;
|
|
629
|
+
const chunks = [];
|
|
630
|
+
|
|
631
|
+
for await (const chunk of stdin) {
|
|
632
|
+
chunks.push(chunk);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const input = Buffer.concat(chunks).toString();
|
|
636
|
+
|
|
637
|
+
if (!input.trim()) {
|
|
638
|
+
console.error("No input received from stdin");
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Parse Claude Code hook JSON format
|
|
643
|
+
let hookData;
|
|
644
|
+
try {
|
|
645
|
+
hookData = JSON.parse(input);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error("Invalid JSON input:", error.message);
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const toolName = hookData.tool_name || "Unknown";
|
|
652
|
+
const toolInput = hookData.tool_input || {};
|
|
653
|
+
const sessionId = hookData.session_id || null;
|
|
654
|
+
|
|
655
|
+
// Only validate Bash commands for now
|
|
656
|
+
if (toolName !== "Bash") {
|
|
657
|
+
console.log(`Skipping validation for tool: ${toolName}`);
|
|
658
|
+
process.exit(0);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const command = toolInput.command;
|
|
662
|
+
if (!command) {
|
|
663
|
+
console.error("No command found in tool input");
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Validate the command
|
|
668
|
+
const result = validator.validate(command, toolName);
|
|
669
|
+
|
|
670
|
+
// Log the security event
|
|
671
|
+
await validator.logSecurityEvent(command, toolName, result, sessionId);
|
|
672
|
+
|
|
673
|
+
// Output result and exit with appropriate code
|
|
674
|
+
if (result.isValid) {
|
|
675
|
+
console.log("Command validation passed");
|
|
676
|
+
process.exit(0); // Allow execution
|
|
677
|
+
} else {
|
|
678
|
+
// Instead of blocking, ask user for confirmation
|
|
679
|
+
const confirmationMessage = `⚠️ Potentially dangerous command detected!\n\nCommand: ${command}\nViolations: ${result.violations.join(
|
|
680
|
+
", "
|
|
681
|
+
)}\nSeverity: ${
|
|
682
|
+
result.severity
|
|
683
|
+
}\n\nDo you want to proceed with this command?`;
|
|
684
|
+
|
|
685
|
+
const hookOutput = {
|
|
686
|
+
hookSpecificOutput: {
|
|
687
|
+
hookEventName: "PreToolUse",
|
|
688
|
+
permissionDecision: "ask",
|
|
689
|
+
permissionDecisionReason: confirmationMessage,
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
console.log(JSON.stringify(hookOutput));
|
|
694
|
+
process.exit(0); // Exit with 0 to trigger user prompt
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
console.error("Validation script error:", error);
|
|
698
|
+
// Fail safe - block execution on any script error
|
|
699
|
+
process.exit(2);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Execute main function
|
|
704
|
+
main().catch((error) => {
|
|
705
|
+
console.error("Fatal error:", error);
|
|
706
|
+
process.exit(2);
|
|
707
|
+
});
|