cc-safe-setup 2.0.7 → 2.1.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/examples/README.md +1 -0
- package/examples/block-database-wipe.sh +2 -1
- package/examples/verify-before-commit.sh +60 -0
- package/index.mjs +152 -0
- package/package.json +1 -1
package/examples/README.md
CHANGED
|
@@ -48,6 +48,7 @@ npx cc-safe-setup --examples
|
|
|
48
48
|
| **enforce-tests.sh** | Warn when source changes without tests | |
|
|
49
49
|
| **large-file-guard.sh** | Warn when Write creates files >500KB | |
|
|
50
50
|
| **todo-check.sh** | Warn when committing files with TODO/FIXME | |
|
|
51
|
+
| **verify-before-commit.sh** | Block commit unless tests passed recently | [#37818](https://github.com/anthropics/claude-code/issues/37818) |
|
|
51
52
|
|
|
52
53
|
## Recovery
|
|
53
54
|
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
# - Django: flush, sqlflush
|
|
7
7
|
# - Rails: db:drop, db:reset
|
|
8
8
|
# - Raw SQL: DROP DATABASE, TRUNCATE
|
|
9
|
+
# - Symfony/Doctrine: fixtures:load (without --append), schema:drop, database:drop
|
|
9
10
|
# - Prisma: migrate reset, db push --force-reset
|
|
10
11
|
# - PostgreSQL: dropdb
|
|
11
12
|
#
|
|
12
|
-
# Born from GitHub Issues #37405, #37439, #34729
|
|
13
|
+
# Born from GitHub Issues #37405, #37439, #34729, #37574
|
|
13
14
|
#
|
|
14
15
|
# Usage: Add to settings.json as a PreToolUse hook
|
|
15
16
|
#
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# verify-before-commit.sh — Block git commit unless tests passed recently
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude saying "fixed" and committing without actually
|
|
5
|
+
# verifying the fix works (#37818, #36970)
|
|
6
|
+
#
|
|
7
|
+
# How it works:
|
|
8
|
+
# 1. Your test runner creates a marker file on success
|
|
9
|
+
# 2. This hook checks for the marker before allowing commit
|
|
10
|
+
# 3. Marker expires after 10 minutes (stale test results don't count)
|
|
11
|
+
#
|
|
12
|
+
# Marker creation (add to your test script or PostToolUse hook):
|
|
13
|
+
# touch "/tmp/cc-tests-passed-$(pwd | md5sum | cut -c1-8)"
|
|
14
|
+
#
|
|
15
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
16
|
+
#
|
|
17
|
+
# {
|
|
18
|
+
# "hooks": {
|
|
19
|
+
# "PreToolUse": [{
|
|
20
|
+
# "matcher": "Bash",
|
|
21
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/verify-before-commit.sh" }]
|
|
22
|
+
# }]
|
|
23
|
+
# }
|
|
24
|
+
# }
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
[[ -z "$COMMAND" ]] && exit 0
|
|
30
|
+
|
|
31
|
+
# Only check git commit commands
|
|
32
|
+
if ! echo "$COMMAND" | grep -qE '^\s*git\s+commit\b'; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Must be in a git repo
|
|
37
|
+
git rev-parse --git-dir &>/dev/null || exit 0
|
|
38
|
+
|
|
39
|
+
# Check for test marker (created by test runner)
|
|
40
|
+
PROJECT_HASH=$(pwd | md5sum | cut -c1-8)
|
|
41
|
+
MARKER="/tmp/cc-tests-passed-${PROJECT_HASH}"
|
|
42
|
+
MAX_AGE=600 # 10 minutes
|
|
43
|
+
|
|
44
|
+
if [ ! -f "$MARKER" ]; then
|
|
45
|
+
echo "BLOCKED: No test evidence found. Run tests before committing." >&2
|
|
46
|
+
echo "" >&2
|
|
47
|
+
echo "Your test runner should create: $MARKER" >&2
|
|
48
|
+
echo "Example: pytest && touch $MARKER" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Check marker age
|
|
53
|
+
MARKER_AGE=$(( $(date +%s) - $(stat -c %Y "$MARKER" 2>/dev/null || echo 0) ))
|
|
54
|
+
if (( MARKER_AGE > MAX_AGE )); then
|
|
55
|
+
echo "BLOCKED: Test results are stale (${MARKER_AGE}s old, max ${MAX_AGE}s)." >&2
|
|
56
|
+
echo "Run tests again before committing." >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -68,6 +68,7 @@ const VERIFY = process.argv.includes('--verify') || process.argv.includes('-v');
|
|
|
68
68
|
const EXAMPLES = process.argv.includes('--examples') || process.argv.includes('-e');
|
|
69
69
|
const INSTALL_EXAMPLE_IDX = process.argv.findIndex(a => a === '--install-example');
|
|
70
70
|
const INSTALL_EXAMPLE = INSTALL_EXAMPLE_IDX !== -1 ? process.argv[INSTALL_EXAMPLE_IDX + 1] : null;
|
|
71
|
+
const AUDIT = process.argv.includes('--audit');
|
|
71
72
|
|
|
72
73
|
if (HELP) {
|
|
73
74
|
console.log(`
|
|
@@ -81,6 +82,7 @@ if (HELP) {
|
|
|
81
82
|
npx cc-safe-setup --uninstall Remove all installed hooks
|
|
82
83
|
npx cc-safe-setup --examples List 25 example hooks (5 categories)
|
|
83
84
|
npx cc-safe-setup --install-example <name> Install a specific example
|
|
85
|
+
npx cc-safe-setup --audit Analyze your setup and recommend missing protections
|
|
84
86
|
npx cc-safe-setup --help Show this help
|
|
85
87
|
|
|
86
88
|
Hooks installed:
|
|
@@ -304,6 +306,7 @@ function examples() {
|
|
|
304
306
|
'enforce-tests.sh': 'Warn when source files change without test files',
|
|
305
307
|
'large-file-guard.sh': 'Warn when Write creates files over 500KB',
|
|
306
308
|
'todo-check.sh': 'Warn when committing files with TODO/FIXME markers',
|
|
309
|
+
'verify-before-commit.sh': 'Block commit unless tests passed recently',
|
|
307
310
|
},
|
|
308
311
|
'Recovery': {
|
|
309
312
|
'auto-checkpoint.sh': 'Auto-commit after edits for rollback protection',
|
|
@@ -395,12 +398,161 @@ async function installExample(name) {
|
|
|
395
398
|
console.log();
|
|
396
399
|
}
|
|
397
400
|
|
|
401
|
+
function audit() {
|
|
402
|
+
console.log();
|
|
403
|
+
console.log(c.bold + ' cc-safe-setup --audit' + c.reset);
|
|
404
|
+
console.log(c.dim + ' Analyzing your Claude Code safety setup...' + c.reset);
|
|
405
|
+
console.log();
|
|
406
|
+
|
|
407
|
+
const risks = [];
|
|
408
|
+
const good = [];
|
|
409
|
+
|
|
410
|
+
// 1. Check if any PreToolUse hooks exist
|
|
411
|
+
let settings = {};
|
|
412
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
413
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch(e) {}
|
|
414
|
+
}
|
|
415
|
+
const preHooks = (settings.hooks?.PreToolUse || []);
|
|
416
|
+
const postHooks = (settings.hooks?.PostToolUse || []);
|
|
417
|
+
const stopHooks = (settings.hooks?.Stop || []);
|
|
418
|
+
|
|
419
|
+
if (preHooks.length === 0) {
|
|
420
|
+
risks.push({
|
|
421
|
+
severity: 'CRITICAL',
|
|
422
|
+
issue: 'No PreToolUse hooks — destructive commands (rm -rf, git reset --hard) can run unchecked',
|
|
423
|
+
fix: 'npx cc-safe-setup'
|
|
424
|
+
});
|
|
425
|
+
} else {
|
|
426
|
+
good.push('PreToolUse hooks installed (' + preHooks.length + ')');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 2. Check for destructive command protection
|
|
430
|
+
const allHookCommands = preHooks.map(h => h.hooks?.map(hh => hh.command || '').join(' ') || '').join(' ');
|
|
431
|
+
if (!allHookCommands.match(/destructive|guard|block|rm|reset/i)) {
|
|
432
|
+
risks.push({
|
|
433
|
+
severity: 'HIGH',
|
|
434
|
+
issue: 'No destructive command blocker detected — rm -rf /, git reset --hard could execute',
|
|
435
|
+
fix: 'npx cc-safe-setup (installs destructive-guard)'
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
good.push('Destructive command protection detected');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 3. Check for branch protection
|
|
442
|
+
if (!allHookCommands.match(/branch|push|main|master/i)) {
|
|
443
|
+
risks.push({
|
|
444
|
+
severity: 'HIGH',
|
|
445
|
+
issue: 'No branch push protection — code could be pushed directly to main/master',
|
|
446
|
+
fix: 'npx cc-safe-setup (installs branch-guard)'
|
|
447
|
+
});
|
|
448
|
+
} else {
|
|
449
|
+
good.push('Branch push protection detected');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 4. Check for secret leak protection
|
|
453
|
+
if (!allHookCommands.match(/secret|env|credential/i)) {
|
|
454
|
+
risks.push({
|
|
455
|
+
severity: 'HIGH',
|
|
456
|
+
issue: 'No secret leak protection — .env files could be committed via git add .',
|
|
457
|
+
fix: 'npx cc-safe-setup (installs secret-guard)'
|
|
458
|
+
});
|
|
459
|
+
} else {
|
|
460
|
+
good.push('Secret leak protection detected');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 5. Check for database wipe protection
|
|
464
|
+
if (!allHookCommands.match(/database|wipe|migrate|prisma/i)) {
|
|
465
|
+
risks.push({
|
|
466
|
+
severity: 'MEDIUM',
|
|
467
|
+
issue: 'No database wipe protection — migrate:fresh, prisma migrate reset could wipe data',
|
|
468
|
+
fix: 'npx cc-safe-setup --install-example block-database-wipe'
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
good.push('Database wipe protection detected');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 6. Check for syntax checking
|
|
475
|
+
if (postHooks.length === 0) {
|
|
476
|
+
risks.push({
|
|
477
|
+
severity: 'MEDIUM',
|
|
478
|
+
issue: 'No PostToolUse hooks — no automatic syntax checking after edits',
|
|
479
|
+
fix: 'npx cc-safe-setup (installs syntax-check)'
|
|
480
|
+
});
|
|
481
|
+
} else {
|
|
482
|
+
good.push('PostToolUse hooks installed (' + postHooks.length + ')');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 7. Check for CLAUDE.md
|
|
486
|
+
const CC_DIR = join(HOME, '.claude');
|
|
487
|
+
const claudeMdPaths = ['CLAUDE.md', '.claude/CLAUDE.md', join(CC_DIR, 'CLAUDE.md')];
|
|
488
|
+
const hasClaudeMd = claudeMdPaths.some(p => existsSync(p));
|
|
489
|
+
if (!hasClaudeMd) {
|
|
490
|
+
risks.push({
|
|
491
|
+
severity: 'MEDIUM',
|
|
492
|
+
issue: 'No CLAUDE.md found — Claude has no project-specific instructions',
|
|
493
|
+
fix: 'Create CLAUDE.md with project rules and conventions'
|
|
494
|
+
});
|
|
495
|
+
} else {
|
|
496
|
+
good.push('CLAUDE.md found');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 8. Check for dotfile protection
|
|
500
|
+
if (!allHookCommands.match(/dotfile|bashrc|protect/i)) {
|
|
501
|
+
risks.push({
|
|
502
|
+
severity: 'LOW',
|
|
503
|
+
issue: 'No dotfile protection — ~/.bashrc, ~/.aws/ could be modified',
|
|
504
|
+
fix: 'npx cc-safe-setup --install-example protect-dotfiles'
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 9. Check for scope guard
|
|
509
|
+
if (!allHookCommands.match(/scope|traversal|outside/i)) {
|
|
510
|
+
risks.push({
|
|
511
|
+
severity: 'LOW',
|
|
512
|
+
issue: 'No scope guard — files outside project directory could be modified',
|
|
513
|
+
fix: 'npx cc-safe-setup --install-example scope-guard'
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Display results
|
|
518
|
+
if (good.length > 0) {
|
|
519
|
+
console.log(c.bold + ' ✓ What\'s working:' + c.reset);
|
|
520
|
+
for (const g of good) {
|
|
521
|
+
console.log(' ' + c.green + '✓' + c.reset + ' ' + g);
|
|
522
|
+
}
|
|
523
|
+
console.log();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (risks.length === 0) {
|
|
527
|
+
console.log(c.green + c.bold + ' No risks detected. Your setup looks solid.' + c.reset);
|
|
528
|
+
} else {
|
|
529
|
+
console.log(c.bold + ' ⚠ Risks found (' + risks.length + '):' + c.reset);
|
|
530
|
+
console.log();
|
|
531
|
+
for (const r of risks) {
|
|
532
|
+
const severityColor = r.severity === 'CRITICAL' ? c.red : r.severity === 'HIGH' ? c.red : c.yellow;
|
|
533
|
+
console.log(' ' + severityColor + '[' + r.severity + ']' + c.reset + ' ' + r.issue);
|
|
534
|
+
console.log(' ' + c.dim + ' Fix: ' + r.fix + c.reset);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log();
|
|
539
|
+
const score = Math.max(0, 100 - risks.reduce((sum, r) => {
|
|
540
|
+
if (r.severity === 'CRITICAL') return sum + 30;
|
|
541
|
+
if (r.severity === 'HIGH') return sum + 20;
|
|
542
|
+
if (r.severity === 'MEDIUM') return sum + 10;
|
|
543
|
+
return sum + 5;
|
|
544
|
+
}, 0));
|
|
545
|
+
console.log(c.bold + ' Safety Score: ' + (score >= 80 ? c.green : score >= 50 ? c.yellow : c.red) + score + '/100' + c.reset);
|
|
546
|
+
console.log();
|
|
547
|
+
}
|
|
548
|
+
|
|
398
549
|
async function main() {
|
|
399
550
|
if (UNINSTALL) return uninstall();
|
|
400
551
|
if (VERIFY) return verify();
|
|
401
552
|
if (STATUS) return status();
|
|
402
553
|
if (EXAMPLES) return examples();
|
|
403
554
|
if (INSTALL_EXAMPLE) return installExample(INSTALL_EXAMPLE);
|
|
555
|
+
if (AUDIT) return audit();
|
|
404
556
|
|
|
405
557
|
console.log();
|
|
406
558
|
console.log(c.bold + ' cc-safe-setup' + c.reset);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 25 installable examples. Destructive blocker, branch guard, database wipe protection, dotfile guard, and more.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|