cc-safe-setup 2.3.0 → 2.5.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.
@@ -0,0 +1,145 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # case-sensitive-guard.sh — Case-Insensitive Filesystem Safety Guard
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Detects case-insensitive filesystems (exFAT, NTFS, HFS+, APFS
7
+ # case-insensitive) and warns before mkdir/rm that would collide
8
+ # due to case folding.
9
+ #
10
+ # Real incident: GitHub #37875 — Claude created "Content" dir on
11
+ # exFAT drive where "content" already existed. Both resolved to
12
+ # the same path. Claude then ran rm -rf on "content", destroying
13
+ # all user data.
14
+ #
15
+ # TRIGGER: PreToolUse
16
+ # MATCHER: "Bash"
17
+ #
18
+ # WHAT IT BLOCKS (exit 2):
19
+ # - rm -rf on case-insensitive FS when a case-variant directory
20
+ # exists that resolves to the same inode
21
+ # - mkdir that would silently collide with existing dir on
22
+ # case-insensitive FS
23
+ #
24
+ # WHAT IT ALLOWS (exit 0):
25
+ # - All commands on case-sensitive filesystems
26
+ # - rm/mkdir where no case collision exists
27
+ # - Commands that don't involve mkdir or rm
28
+ #
29
+ # HOW IT WORKS:
30
+ # 1. Extract target path from mkdir/rm commands
31
+ # 2. Check if filesystem is case-insensitive (create temp file,
32
+ # check if uppercase variant exists)
33
+ # 3. If case-insensitive, check for case-variant collisions
34
+ # 4. Block if collision detected
35
+ # ================================================================
36
+
37
+ INPUT=$(cat)
38
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
39
+
40
+ if [[ -z "$COMMAND" ]]; then
41
+ exit 0
42
+ fi
43
+
44
+ # Only check mkdir and rm commands
45
+ if ! echo "$COMMAND" | grep -qE '^\s*(mkdir|rm)\s'; then
46
+ exit 0
47
+ fi
48
+
49
+ # Extract the target path
50
+ TARGET=""
51
+ if echo "$COMMAND" | grep -qE '^\s*mkdir'; then
52
+ TARGET=$(echo "$COMMAND" | grep -oP 'mkdir\s+(-p\s+)?\K\S+' | tail -1)
53
+ elif echo "$COMMAND" | grep -qE '^\s*rm\s'; then
54
+ TARGET=$(echo "$COMMAND" | grep -oP 'rm\s+(-[rf]+\s+)*\K\S+' | tail -1)
55
+ fi
56
+
57
+ if [[ -z "$TARGET" ]]; then
58
+ exit 0
59
+ fi
60
+
61
+ # Resolve the parent directory
62
+ PARENT_DIR=$(dirname "$TARGET" 2>/dev/null)
63
+ BASE_NAME=$(basename "$TARGET" 2>/dev/null)
64
+
65
+ if [[ -z "$PARENT_DIR" ]] || [[ ! -d "$PARENT_DIR" ]]; then
66
+ exit 0
67
+ fi
68
+
69
+ # --- Check if filesystem is case-insensitive ---
70
+ is_case_insensitive() {
71
+ local dir="$1"
72
+ local test_file="${dir}/.cc_case_test_$$"
73
+ local test_upper="${dir}/.CC_CASE_TEST_$$"
74
+
75
+ # Create lowercase test file
76
+ if ! touch "$test_file" 2>/dev/null; then
77
+ return 1 # Can't test, assume case-sensitive (safe default)
78
+ fi
79
+
80
+ # Check if uppercase variant resolves to the same file
81
+ if [[ -f "$test_upper" ]]; then
82
+ rm -f "$test_file" 2>/dev/null
83
+ return 0 # Case-insensitive
84
+ else
85
+ rm -f "$test_file" 2>/dev/null
86
+ return 1 # Case-sensitive
87
+ fi
88
+ }
89
+
90
+ # --- Check for case-variant collisions ---
91
+ find_case_collision() {
92
+ local dir="$1"
93
+ local name="$2"
94
+ local name_lower
95
+ name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]')
96
+
97
+ # List directory entries and check for case variants
98
+ while IFS= read -r entry; do
99
+ local entry_lower
100
+ entry_lower=$(echo "$entry" | tr '[:upper:]' '[:lower:]')
101
+ if [[ "$entry_lower" == "$name_lower" ]] && [[ "$entry" != "$name" ]]; then
102
+ echo "$entry"
103
+ return 0
104
+ fi
105
+ done < <(ls -1 "$dir" 2>/dev/null)
106
+
107
+ return 1
108
+ }
109
+
110
+ # Only proceed if filesystem is case-insensitive
111
+ if ! is_case_insensitive "$PARENT_DIR"; then
112
+ exit 0
113
+ fi
114
+
115
+ # Check for collision
116
+ COLLISION=$(find_case_collision "$PARENT_DIR" "$BASE_NAME")
117
+
118
+ if [[ -n "$COLLISION" ]]; then
119
+ if echo "$COMMAND" | grep -qE '^\s*rm\s'; then
120
+ echo "BLOCKED: Case-insensitive filesystem collision detected." >&2
121
+ echo "" >&2
122
+ echo "Command: $COMMAND" >&2
123
+ echo "" >&2
124
+ echo "Target: $TARGET" >&2
125
+ echo "Collides with: $PARENT_DIR/$COLLISION" >&2
126
+ echo "" >&2
127
+ echo "This filesystem is case-insensitive (exFAT, NTFS, HFS+, etc.)." >&2
128
+ echo "'$BASE_NAME' and '$COLLISION' resolve to the SAME path." >&2
129
+ echo "rm -rf would destroy the data you think you're keeping." >&2
130
+ echo "" >&2
131
+ echo "Verify with: ls -la \"$PARENT_DIR\" | grep -i \"$BASE_NAME\"" >&2
132
+ exit 2
133
+ elif echo "$COMMAND" | grep -qE '^\s*mkdir'; then
134
+ echo "WARNING: Case-insensitive filesystem — directory already exists." >&2
135
+ echo "" >&2
136
+ echo "Command: $COMMAND" >&2
137
+ echo "Existing: $PARENT_DIR/$COLLISION" >&2
138
+ echo "" >&2
139
+ echo "On this filesystem, '$BASE_NAME' and '$COLLISION' are the same path." >&2
140
+ echo "mkdir will either fail or silently use the existing directory." >&2
141
+ exit 2
142
+ fi
143
+ fi
144
+
145
+ exit 0
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # session-checkpoint.sh — Auto-save session state on every stop
3
+ #
4
+ # Solves: Session crashes/disconnects causing expensive re-analysis
5
+ # of the entire codebase on next start (#37866)
6
+ #
7
+ # Saves: git state, recent commits, modified files, working directory.
8
+ # Next session reads the checkpoint instead of re-analyzing everything.
9
+ #
10
+ # Usage: Add to settings.json as a Stop hook
11
+ #
12
+ # {
13
+ # "hooks": {
14
+ # "Stop": [{
15
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-checkpoint.sh" }]
16
+ # }]
17
+ # }
18
+ # }
19
+ #
20
+ # Recovery: Add to CLAUDE.md:
21
+ # "If ~/.claude/checkpoints/ has a file for this project, read it first."
22
+
23
+ INPUT=$(cat)
24
+ REASON=$(echo "$INPUT" | jq -r '.stop_reason // empty' 2>/dev/null)
25
+
26
+ CHECKPOINT_DIR="$HOME/.claude/checkpoints"
27
+ mkdir -p "$CHECKPOINT_DIR"
28
+
29
+ PROJECT_NAME=$(basename "$(pwd)")
30
+ CHECKPOINT="$CHECKPOINT_DIR/${PROJECT_NAME}-latest.md"
31
+
32
+ {
33
+ echo "# Session Checkpoint"
34
+ echo "Saved: $(date -Iseconds)"
35
+ echo "Directory: $(pwd)"
36
+ echo "Stop reason: ${REASON:-unknown}"
37
+ echo ""
38
+ echo "## Recent Commits"
39
+ git log --oneline -10 2>/dev/null || echo "(not a git repo)"
40
+ echo ""
41
+ echo "## Uncommitted Changes"
42
+ git diff --stat 2>/dev/null || echo "(none)"
43
+ echo ""
44
+ echo "## Staged Files"
45
+ git diff --cached --name-only 2>/dev/null || echo "(none)"
46
+ echo ""
47
+ echo "## Current Branch"
48
+ git branch --show-current 2>/dev/null || echo "(unknown)"
49
+ } > "$CHECKPOINT" 2>/dev/null
50
+
51
+ # Cleanup: keep only last 10 checkpoints
52
+ ls -t "$CHECKPOINT_DIR"/*.md 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null
53
+
54
+ exit 0
package/index.mjs CHANGED
@@ -71,6 +71,7 @@ const INSTALL_EXAMPLE = INSTALL_EXAMPLE_IDX !== -1 ? process.argv[INSTALL_EXAMPL
71
71
  const AUDIT = process.argv.includes('--audit');
72
72
  const LEARN = process.argv.includes('--learn');
73
73
  const SCAN = process.argv.includes('--scan');
74
+ const FULL = process.argv.includes('--full');
74
75
 
75
76
  if (HELP) {
76
77
  console.log(`
@@ -84,7 +85,11 @@ if (HELP) {
84
85
  npx cc-safe-setup --uninstall Remove all installed hooks
85
86
  npx cc-safe-setup --examples List 25 example hooks (5 categories)
86
87
  npx cc-safe-setup --install-example <name> Install a specific example
87
- npx cc-safe-setup --audit Analyze your setup and recommend missing protections
88
+ npx cc-safe-setup --full Complete setup: hooks + scan + audit + badge
89
+ npx cc-safe-setup --audit Safety score (0-100) with fixes
90
+ npx cc-safe-setup --audit --fix Auto-fix missing protections
91
+ npx cc-safe-setup --scan Detect tech stack, recommend hooks
92
+ npx cc-safe-setup --learn Learn from your block history
88
93
  npx cc-safe-setup --help Show this help
89
94
 
90
95
  Hooks installed:
@@ -568,6 +573,18 @@ async function audit() {
568
573
  console.log();
569
574
  console.log(c.dim + ' Run with --fix to auto-apply: npx cc-safe-setup --audit --fix' + c.reset);
570
575
  }
576
+
577
+ // Badge output
578
+ if (process.argv.includes('--badge')) {
579
+ const color = score >= 80 ? 'brightgreen' : score >= 50 ? 'yellow' : 'red';
580
+ const badge = `![Claude Code Safety](https://img.shields.io/badge/Claude_Code_Safety-${score}%2F100-${color})`;
581
+ console.log();
582
+ console.log(c.bold + ' README Badge:' + c.reset);
583
+ console.log(' ' + badge);
584
+ console.log();
585
+ console.log(c.dim + ' Paste this into your README.md' + c.reset);
586
+ }
587
+
571
588
  console.log();
572
589
  }
573
590
 
@@ -683,6 +700,44 @@ exit 0`;
683
700
  console.log();
684
701
  }
685
702
 
703
+ async function fullSetup() {
704
+ console.log();
705
+ console.log(c.bold + c.green + ' cc-safe-setup --full' + c.reset);
706
+ console.log(c.dim + ' Complete safety setup in one command' + c.reset);
707
+ console.log();
708
+
709
+ const { execSync } = await import('child_process');
710
+ const self = process.argv[1];
711
+
712
+ // Step 1: Install 8 built-in hooks
713
+ console.log(c.bold + ' Step 1: Installing 8 built-in safety hooks...' + c.reset);
714
+ try {
715
+ execSync('node ' + self + ' --yes', { stdio: 'inherit' });
716
+ } catch(e) {
717
+ // --yes doesn't exist, run normal install
718
+ execSync('node ' + self, { stdio: 'inherit', input: 'y\n' });
719
+ }
720
+
721
+ // Step 2: Scan project and create CLAUDE.md
722
+ console.log();
723
+ console.log(c.bold + ' Step 2: Scanning project and creating CLAUDE.md...' + c.reset);
724
+ scan();
725
+
726
+ // Step 3: Audit and show results
727
+ console.log();
728
+ console.log(c.bold + ' Step 3: Running safety audit...' + c.reset);
729
+ // Inject --badge into argv temporarily
730
+ process.argv.push('--badge');
731
+ await audit();
732
+
733
+ console.log(c.bold + c.green + ' ✓ Full setup complete!' + c.reset);
734
+ console.log(c.dim + ' Your project now has:' + c.reset);
735
+ console.log(c.dim + ' • 8 built-in safety hooks' + c.reset);
736
+ console.log(c.dim + ' • Project-specific hook recommendations' + c.reset);
737
+ console.log(c.dim + ' • Safety score and README badge' + c.reset);
738
+ console.log();
739
+ }
740
+
686
741
  function scan() {
687
742
  console.log();
688
743
  console.log(c.bold + ' cc-safe-setup --scan' + c.reset);
@@ -832,6 +887,7 @@ async function main() {
832
887
  if (AUDIT) return audit();
833
888
  if (LEARN) return learn();
834
889
  if (SCAN) return scan();
890
+ if (FULL) return fullSetup();
835
891
 
836
892
  console.log();
837
893
  console.log(c.bold + ' cc-safe-setup' + c.reset);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "2.3.0",
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.",
3
+ "version": "2.5.0",
4
+ "description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 26 installable examples. Destructive blocker, branch guard, database wipe protection, case-insensitive FS guard, and more.",
5
5
  "main": "index.mjs",
6
6
  "bin": {
7
7
  "cc-safe-setup": "index.mjs"