cc-safe-setup 3.3.0 → 3.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.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **One command to make Claude Code safe for autonomous operation.**
8
8
 
9
- Not just a destructive command blocker 8 hooks covering safety, quality, monitoring, and developer experience.
9
+ 8 built-in hooks + 32 installable examples. Audit, create, lint, diff, watch, and learn. [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) · [Web Tool](https://yurukusa.github.io/cc-safe-setup/) · [Troubleshooting](TROUBLESHOOTING.md)
10
10
 
11
11
  ```bash
12
12
  npx cc-safe-setup
@@ -218,11 +218,17 @@ Or browse all available examples in [`examples/`](examples/):
218
218
  - **tmp-cleanup.sh** — Clean up accumulated `/tmp/claude-*-cwd` files on session end ([#8856](https://github.com/anthropics/claude-code/issues/8856))
219
219
  - **session-checkpoint.sh** — Save session state to mission file before context compaction ([#37866](https://github.com/anthropics/claude-code/issues/37866))
220
220
  - **verify-before-commit.sh** — Block git commit when lint/test commands haven't been run ([#37818](https://github.com/anthropics/claude-code/issues/37818))
221
+ - **hook-debug-wrapper.sh** — Wrap any hook to log input/output/exit code/timing to `~/.claude/hook-debug.log`
222
+ - **loop-detector.sh** — Detect and break command repetition loops (warn at 3, block at 5 repeats)
221
223
 
222
224
  ## Safety Checklist
223
225
 
224
226
  **[SAFETY_CHECKLIST.md](SAFETY_CHECKLIST.md)** — Copy-paste checklist for before/during/after autonomous sessions.
225
227
 
228
+ ## Troubleshooting
229
+
230
+ **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** — "Hook doesn't work" → step-by-step diagnosis. Covers every common failure pattern.
231
+
226
232
  ## settings.json Reference
227
233
 
228
234
  **[SETTINGS_REFERENCE.md](SETTINGS_REFERENCE.md)** — Complete reference for permissions, hooks, modes, and common configurations. Includes known limitations and workarounds.
@@ -0,0 +1,212 @@
1
+ # Troubleshooting Claude Code Hooks
2
+
3
+ Your hook isn't working. Here's how to fix it, starting with the most common causes.
4
+
5
+ ## Quick Diagnosis
6
+
7
+ ```bash
8
+ npx cc-safe-setup --doctor
9
+ ```
10
+
11
+ This checks jq, settings.json, file permissions, shebangs, and common misconfigurations. If it says "All checks passed" but hooks still don't fire, read on.
12
+
13
+ ## "Hook doesn't block anything"
14
+
15
+ ### 1. Did you restart Claude Code?
16
+
17
+ Hooks are loaded on startup. After installing or modifying hooks, close Claude Code completely and reopen it.
18
+
19
+ ### 2. Is the hook registered in settings.json?
20
+
21
+ ```bash
22
+ cat ~/.claude/settings.json | jq '.hooks'
23
+ ```
24
+
25
+ You should see your hook's path under the correct trigger. If not:
26
+
27
+ ```bash
28
+ npx cc-safe-setup # Re-registers all hooks
29
+ ```
30
+
31
+ ### 3. Is the hook file executable?
32
+
33
+ ```bash
34
+ ls -la ~/.claude/hooks/your-hook.sh
35
+ # Should show -rwxr-xr-x
36
+ ```
37
+
38
+ Fix: `chmod +x ~/.claude/hooks/your-hook.sh`
39
+
40
+ ### 4. Is jq installed?
41
+
42
+ Most hooks use jq to parse JSON input.
43
+
44
+ ```bash
45
+ jq --version
46
+ # Should print: jq-1.x
47
+ ```
48
+
49
+ Install: `brew install jq` (macOS) / `apt install jq` (Linux/WSL)
50
+
51
+ ### 5. Does the hook work manually?
52
+
53
+ Test it outside Claude Code:
54
+
55
+ ```bash
56
+ echo '{"tool_input":{"command":"rm -rf /"}}' | bash ~/.claude/hooks/destructive-guard.sh
57
+ echo $?
58
+ # Should print: 2 (blocked)
59
+ ```
60
+
61
+ If exit code is 0, the hook isn't matching the pattern.
62
+
63
+ ### 6. Wrong exit code
64
+
65
+ | Exit Code | Meaning |
66
+ |-----------|---------|
67
+ | **0** | Allow (or no opinion) |
68
+ | **2** | Block — the only code that stops execution |
69
+ | **1** | Error (treated as allow, not block!) |
70
+
71
+ Common mistake: using `exit 1` instead of `exit 2` to block. Only exit 2 blocks.
72
+
73
+ ## "Hook blocks everything"
74
+
75
+ ### 1. Overly broad grep pattern
76
+
77
+ ```bash
78
+ # BAD: matches ANY command containing "rm"
79
+ grep -q 'rm'
80
+
81
+ # GOOD: matches only rm with -rf flags
82
+ grep -qE 'rm\s+(-[rf]+\s+)*/'
83
+ ```
84
+
85
+ ### 2. Missing empty-input guard
86
+
87
+ Every hook should handle empty input:
88
+
89
+ ```bash
90
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
91
+ [ -z "$COMMAND" ] && exit 0 # ← This line is critical
92
+ ```
93
+
94
+ Without this, the hook may exit 2 on tools that don't have `.tool_input.command` (like Read or Glob).
95
+
96
+ ### 3. Wrong matcher
97
+
98
+ If your hook is for Bash commands but the matcher is empty, it runs on every tool call:
99
+
100
+ ```json
101
+ {"matcher": "Bash"} ← Correct: only Bash commands
102
+ {"matcher": ""} ← Runs on EVERY tool (Read, Edit, Glob, etc.)
103
+ ```
104
+
105
+ ## "Hook fires but doesn't auto-approve"
106
+
107
+ ### 1. JSON output format is wrong
108
+
109
+ Auto-approve requires exact JSON structure:
110
+
111
+ ```json
112
+ {
113
+ "hookSpecificOutput": {
114
+ "hookEventName": "PreToolUse",
115
+ "permissionDecision": "allow",
116
+ "permissionDecisionReason": "your reason"
117
+ }
118
+ }
119
+ ```
120
+
121
+ Missing any field = permission system ignores it.
122
+
123
+ ### 2. jq output is going to stderr
124
+
125
+ ```bash
126
+ # BAD: output goes to stderr
127
+ jq -n '...' >&2
128
+
129
+ # GOOD: output goes to stdout
130
+ jq -n '...'
131
+ ```
132
+
133
+ Auto-approve JSON must go to stdout.
134
+
135
+ ## "Permission prompts still appear for compound commands"
136
+
137
+ This is a known Claude Code limitation, not a hook issue. `Bash(git:*)` doesn't match `cd /path && git log`.
138
+
139
+ Fix:
140
+
141
+ ```bash
142
+ npx cc-safe-setup --install-example compound-command-approver
143
+ ```
144
+
145
+ ## "Hooks slow down Claude Code"
146
+
147
+ ### 1. Check execution time
148
+
149
+ ```bash
150
+ npx cc-safe-setup --install-example hook-debug-wrapper
151
+ # Then wrap your slow hook to see timing
152
+ ```
153
+
154
+ Hooks should complete in <50ms. If a hook takes >200ms, it's noticeable.
155
+
156
+ ### 2. Too many hooks on empty matcher
157
+
158
+ Hooks with `"matcher": ""` run on every single tool call. Move heavy checks to specific matchers:
159
+
160
+ ```json
161
+ {"matcher": "Bash"} ← Only when Bash runs
162
+ {"matcher": "Edit|Write"} ← Only when files are edited
163
+ ```
164
+
165
+ ### 3. Use --lint to find issues
166
+
167
+ ```bash
168
+ npx cc-safe-setup --lint
169
+ ```
170
+
171
+ Reports performance warnings and configuration issues.
172
+
173
+ ## "Hooks work locally but not for teammates"
174
+
175
+ ### 1. Compare settings
176
+
177
+ ```bash
178
+ npx cc-safe-setup --diff teammate-settings.json
179
+ ```
180
+
181
+ Shows exactly what's different between your setups.
182
+
183
+ ### 2. Export and share
184
+
185
+ ```bash
186
+ npx cc-safe-setup --export # Creates cc-safe-setup-export.json
187
+ # Send to teammate
188
+ npx cc-safe-setup --import cc-safe-setup-export.json
189
+ ```
190
+
191
+ ### 3. Different jq versions
192
+
193
+ Some hooks use jq features not available in older versions. Check: `jq --version`
194
+
195
+ ## "Hooks run but don't log"
196
+
197
+ Hooks write to stderr for user-visible messages. For persistent logging:
198
+
199
+ ```bash
200
+ # Add to your hook
201
+ LOG="$HOME/.claude/blocked-commands.log"
202
+ echo "[$(date -Iseconds)] BLOCKED: reason | cmd: $COMMAND" >> "$LOG"
203
+ ```
204
+
205
+ Then view with: `npx cc-safe-setup --watch` or `npx cc-safe-setup --stats`
206
+
207
+ ## Still Stuck?
208
+
209
+ 1. Wrap the hook with debug wrapper: `npx cc-safe-setup --install-example hook-debug-wrapper`
210
+ 2. Check `~/.claude/hook-debug.log` for detailed I/O traces
211
+ 3. Run `npx cc-safe-setup --doctor` for automated checks
212
+ 4. Open an issue: [cc-safe-setup issues](https://github.com/yurukusa/cc-safe-setup/issues)
@@ -598,6 +598,21 @@ exit 0`
598
598
 
599
599
  return scripts[id] || '#!/bin/bash\nexit 0';
600
600
  }
601
+
602
+ // Auto-load from URL parameter: ?config=base64encodedJSON
603
+ (function() {
604
+ const params = new URLSearchParams(window.location.search);
605
+ const config = params.get('config');
606
+ if (config) {
607
+ try {
608
+ const json = atob(config);
609
+ document.getElementById('settings').value = json;
610
+ runAudit();
611
+ } catch(e) {
612
+ console.error('Invalid config parameter');
613
+ }
614
+ }
615
+ })();
601
616
  </script>
602
617
  </body>
603
618
  </html>
package/docs/index.html CHANGED
@@ -598,6 +598,21 @@ exit 0`
598
598
 
599
599
  return scripts[id] || '#!/bin/bash\nexit 0';
600
600
  }
601
+
602
+ // Auto-load from URL parameter: ?config=base64encodedJSON
603
+ (function() {
604
+ const params = new URLSearchParams(window.location.search);
605
+ const config = params.get('config');
606
+ if (config) {
607
+ try {
608
+ const json = atob(config);
609
+ document.getElementById('settings').value = json;
610
+ runAudit();
611
+ } catch(e) {
612
+ console.error('Invalid config parameter');
613
+ }
614
+ }
615
+ })();
601
616
  </script>
602
617
  </body>
603
618
  </html>
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # commit-quality-gate.sh — Enforce commit message quality
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude Code generates commit messages that are often too long,
7
+ # too vague ("update code"), or contain the full diff summary.
8
+ # This hook enforces minimum quality standards.
9
+ #
10
+ # TRIGGER: PreToolUse
11
+ # MATCHER: "Bash"
12
+ #
13
+ # CHECKS:
14
+ # 1. Subject line length (max 72 chars, warn at 50)
15
+ # 2. No vague subjects ("update", "fix", "changes", "misc")
16
+ # 3. No mega-commits (subject line shouldn't list 5+ changes)
17
+ # 4. Body line length (max 72 chars per line)
18
+ # 5. No empty subject line
19
+ #
20
+ # CONFIGURATION:
21
+ # CC_COMMIT_MAX_SUBJECT=72
22
+ # CC_COMMIT_WARN_SUBJECT=50
23
+ # ================================================================
24
+
25
+ INPUT=$(cat)
26
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
27
+
28
+ if [[ -z "$COMMAND" ]]; then
29
+ exit 0
30
+ fi
31
+
32
+ # Only check git commit commands
33
+ if ! echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
34
+ exit 0
35
+ fi
36
+
37
+ # Skip --amend (modifying existing) and --allow-empty
38
+ if echo "$COMMAND" | grep -qE '\-\-amend|\-\-allow-empty'; then
39
+ exit 0
40
+ fi
41
+
42
+ # Extract commit message
43
+ MSG=""
44
+ if echo "$COMMAND" | grep -qE '\-m\s'; then
45
+ # -m "message" or -m 'message'
46
+ MSG=$(echo "$COMMAND" | grep -oP "\-m\s+['\"]?\K[^'\"]+(?=['\"]?)" | head -1)
47
+ fi
48
+
49
+ if [[ -z "$MSG" ]]; then
50
+ exit 0 # No inline message (might use editor)
51
+ fi
52
+
53
+ MAX_SUBJECT="${CC_COMMIT_MAX_SUBJECT:-72}"
54
+ WARN_SUBJECT="${CC_COMMIT_WARN_SUBJECT:-50}"
55
+
56
+ # Get subject line (first line before any newline)
57
+ SUBJECT=$(echo "$MSG" | head -1)
58
+ SUBJECT_LEN=${#SUBJECT}
59
+
60
+ # Check 1: Empty subject
61
+ if [[ -z "$SUBJECT" ]] || [[ "$SUBJECT_LEN" -lt 3 ]]; then
62
+ echo "WARNING: Commit subject is empty or too short." >&2
63
+ exit 0 # Warn only
64
+ fi
65
+
66
+ # Check 2: Subject too long
67
+ if [[ "$SUBJECT_LEN" -gt "$MAX_SUBJECT" ]]; then
68
+ echo "WARNING: Commit subject is $SUBJECT_LEN chars (max $MAX_SUBJECT)." >&2
69
+ echo "Subject: $(echo "$SUBJECT" | head -c 80)..." >&2
70
+ echo "Tip: Keep the subject under $MAX_SUBJECT chars. Use the body for details." >&2
71
+ fi
72
+
73
+ # Check 3: Vague subjects
74
+ SUBJECT_LOWER=$(echo "$SUBJECT" | tr '[:upper:]' '[:lower:]')
75
+ VAGUE_PATTERNS="^(update|fix|change|misc|wip|tmp|test|stuff|things|minor|cleanup)$|^(update|fix|change)\s+(code|file|stuff|things)$"
76
+ if echo "$SUBJECT_LOWER" | grep -qiE "$VAGUE_PATTERNS"; then
77
+ echo "WARNING: Commit subject is too vague: \"$SUBJECT\"" >&2
78
+ echo "Be specific about what changed and why." >&2
79
+ fi
80
+
81
+ # Check 4: Mega-commit (too many changes listed)
82
+ COMMA_COUNT=$(echo "$SUBJECT" | grep -o ',' | wc -l)
83
+ AND_COUNT=$(echo "$SUBJECT_LOWER" | grep -o ' and ' | wc -l)
84
+ if [[ $((COMMA_COUNT + AND_COUNT)) -ge 4 ]]; then
85
+ echo "WARNING: Commit subject lists many changes. Consider splitting into smaller commits." >&2
86
+ echo "Subject: $(echo "$SUBJECT" | head -c 80)" >&2
87
+ fi
88
+
89
+ exit 0
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # hook-debug-wrapper.sh — Debug wrapper for any Claude Code hook
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Wraps any existing hook script and logs its input, output,
7
+ # exit code, and execution time. Invaluable for debugging hooks
8
+ # that silently fail or produce unexpected results.
9
+ #
10
+ # USAGE:
11
+ # Instead of:
12
+ # "command": "~/.claude/hooks/destructive-guard.sh"
13
+ # Use:
14
+ # "command": "~/.claude/hooks/hook-debug-wrapper.sh ~/.claude/hooks/destructive-guard.sh"
15
+ #
16
+ # Or set CC_HOOK_DEBUG=1 to log all hooks (requires wrapper for each).
17
+ #
18
+ # WHAT IT LOGS:
19
+ # - Timestamp
20
+ # - Hook script path
21
+ # - Input JSON (truncated to 500 chars)
22
+ # - Exit code
23
+ # - stdout (truncated)
24
+ # - stderr (truncated)
25
+ # - Execution time in ms
26
+ #
27
+ # LOG LOCATION: ~/.claude/hook-debug.log
28
+ #
29
+ # TRIGGER: Any (wraps any hook)
30
+ # MATCHER: Any
31
+ # ================================================================
32
+
33
+ HOOK_SCRIPT="$1"
34
+ DEBUG_LOG="${CC_HOOK_DEBUG_LOG:-$HOME/.claude/hook-debug.log}"
35
+
36
+ if [[ -z "$HOOK_SCRIPT" ]] || [[ ! -f "$HOOK_SCRIPT" ]]; then
37
+ echo "Usage: hook-debug-wrapper.sh <hook-script>" >&2
38
+ exit 0
39
+ fi
40
+
41
+ # Read input
42
+ INPUT=$(cat)
43
+
44
+ # Record start time
45
+ START_MS=$(($(date +%s%N) / 1000000))
46
+
47
+ # Run the actual hook, capturing all output
48
+ STDOUT_FILE=$(mktemp)
49
+ STDERR_FILE=$(mktemp)
50
+ echo "$INPUT" | bash "$HOOK_SCRIPT" > "$STDOUT_FILE" 2> "$STDERR_FILE"
51
+ EXIT_CODE=$?
52
+
53
+ END_MS=$(($(date +%s%N) / 1000000))
54
+ ELAPSED=$((END_MS - START_MS))
55
+
56
+ # Read outputs
57
+ STDOUT_CONTENT=$(cat "$STDOUT_FILE")
58
+ STDERR_CONTENT=$(cat "$STDERR_FILE")
59
+ rm -f "$STDOUT_FILE" "$STDERR_FILE"
60
+
61
+ # Log
62
+ HOOK_NAME=$(basename "$HOOK_SCRIPT")
63
+ INPUT_PREVIEW=$(echo "$INPUT" | head -c 500)
64
+ STDOUT_PREVIEW=$(echo "$STDOUT_CONTENT" | head -c 300)
65
+ STDERR_PREVIEW=$(echo "$STDERR_CONTENT" | head -c 300)
66
+
67
+ mkdir -p "$(dirname "$DEBUG_LOG")" 2>/dev/null
68
+ {
69
+ echo "=== $(date -Iseconds) === ${HOOK_NAME} ==="
70
+ echo "exit: ${EXIT_CODE} (${ELAPSED}ms)"
71
+ if [[ -n "$STDERR_PREVIEW" ]]; then
72
+ echo "stderr: ${STDERR_PREVIEW}"
73
+ fi
74
+ if [[ -n "$STDOUT_PREVIEW" ]]; then
75
+ echo "stdout: ${STDOUT_PREVIEW}"
76
+ fi
77
+ echo "input: ${INPUT_PREVIEW}"
78
+ echo ""
79
+ } >> "$DEBUG_LOG"
80
+
81
+ # Pass through the original output
82
+ if [[ -n "$STDOUT_CONTENT" ]]; then
83
+ echo "$STDOUT_CONTENT"
84
+ fi
85
+ if [[ -n "$STDERR_CONTENT" ]]; then
86
+ echo "$STDERR_CONTENT" >&2
87
+ fi
88
+
89
+ exit "$EXIT_CODE"
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # loop-detector.sh — Detect and break command repetition loops
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude Code sometimes gets stuck repeating the same command
7
+ # or cycle of commands. This hook detects repetition and warns
8
+ # before the loop wastes context and time.
9
+ #
10
+ # HOW IT WORKS:
11
+ # 1. Records last N commands in a state file
12
+ # 2. Checks if the current command matches recent commands
13
+ # 3. If same command appears 3+ times in last 5 calls → warn
14
+ # 4. If same command appears 5+ times → block
15
+ #
16
+ # TRIGGER: PreToolUse
17
+ # MATCHER: "Bash"
18
+ #
19
+ # CONFIGURATION:
20
+ # CC_LOOP_WARN=3 — warn after this many repeats (default: 3)
21
+ # CC_LOOP_BLOCK=5 — block after this many repeats (default: 5)
22
+ # CC_LOOP_WINDOW=10 — number of recent commands to track (default: 10)
23
+ # ================================================================
24
+
25
+ INPUT=$(cat)
26
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
27
+
28
+ if [[ -z "$COMMAND" ]]; then
29
+ exit 0
30
+ fi
31
+
32
+ STATE_FILE="/tmp/cc-loop-detector-history"
33
+ WARN_THRESHOLD="${CC_LOOP_WARN:-3}"
34
+ BLOCK_THRESHOLD="${CC_LOOP_BLOCK:-5}"
35
+ WINDOW="${CC_LOOP_WINDOW:-10}"
36
+
37
+ # Normalize command (strip whitespace variations)
38
+ NORMALIZED=$(echo "$COMMAND" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')
39
+
40
+ # Record command
41
+ echo "$NORMALIZED" >> "$STATE_FILE"
42
+
43
+ # Keep only last N entries
44
+ if [ -f "$STATE_FILE" ]; then
45
+ tail -n "$WINDOW" "$STATE_FILE" > "${STATE_FILE}.tmp"
46
+ mv "${STATE_FILE}.tmp" "$STATE_FILE"
47
+ fi
48
+
49
+ # Count occurrences of current command in history
50
+ COUNT=$(grep -cF "$NORMALIZED" "$STATE_FILE" 2>/dev/null || echo 0)
51
+
52
+ if [ "$COUNT" -ge "$BLOCK_THRESHOLD" ]; then
53
+ echo "BLOCKED: Command repeated $COUNT times in last $WINDOW calls." >&2
54
+ echo "" >&2
55
+ echo "Command: $COMMAND" >&2
56
+ echo "" >&2
57
+ echo "This looks like an infinite loop. Try a different approach." >&2
58
+ echo "To reset: rm /tmp/cc-loop-detector-history" >&2
59
+ exit 2
60
+ elif [ "$COUNT" -ge "$WARN_THRESHOLD" ]; then
61
+ echo "WARNING: Command repeated $COUNT times. Possible loop." >&2
62
+ echo "Command: $(echo "$COMMAND" | head -c 100)" >&2
63
+ fi
64
+
65
+ exit 0
package/index.mjs CHANGED
@@ -80,6 +80,9 @@ const IMPORT_FILE = IMPORT_IDX !== -1 ? process.argv[IMPORT_IDX + 1] : null;
80
80
  const STATS = process.argv.includes('--stats');
81
81
  const JSON_OUTPUT = process.argv.includes('--json');
82
82
  const LINT = process.argv.includes('--lint');
83
+ const DIFF_IDX = process.argv.findIndex(a => a === '--diff');
84
+ const DIFF_FILE = DIFF_IDX !== -1 ? process.argv[DIFF_IDX + 1] : null;
85
+ const SHARE = process.argv.includes('--share');
83
86
  const CREATE_IDX = process.argv.findIndex(a => a === '--create');
84
87
  const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
85
88
 
@@ -101,6 +104,8 @@ if (HELP) {
101
104
  npx cc-safe-setup --audit --json Machine-readable output for CI/CD
102
105
  npx cc-safe-setup --scan Detect tech stack, recommend hooks
103
106
  npx cc-safe-setup --learn Learn from your block history
107
+ npx cc-safe-setup --share Generate shareable URL for your setup
108
+ npx cc-safe-setup --diff <file> Compare your settings with another file
104
109
  npx cc-safe-setup --lint Static analysis of hook configuration
105
110
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
106
111
  npx cc-safe-setup --watch Live dashboard of blocked commands
@@ -316,6 +321,8 @@ function examples() {
316
321
  'test-before-push.sh': 'Block git push when tests have not passed',
317
322
  'timeout-guard.sh': 'Warn before long-running commands (servers, watchers)',
318
323
  'git-config-guard.sh': 'Block git config --global modifications',
324
+ 'case-sensitive-guard.sh': 'Detect case-insensitive FS collisions (exFAT/NTFS/HFS+)',
325
+ 'compound-command-approver.sh': 'Auto-approve safe compound commands (cd && git log)',
319
326
  },
320
327
  'Auto-Approve': {
321
328
  'auto-approve-build.sh': 'Auto-approve npm/yarn/cargo/go build, test, lint',
@@ -336,15 +343,19 @@ function examples() {
336
343
  'Recovery': {
337
344
  'auto-checkpoint.sh': 'Auto-commit after edits for rollback protection',
338
345
  'auto-snapshot.sh': 'Auto-save file snapshots before edits (rollback protection)',
346
+ 'session-checkpoint.sh': 'Save session state before context compaction',
339
347
  },
340
348
  'UX': {
341
349
  'notify-waiting.sh': 'Desktop notification when Claude waits for input',
350
+ 'tmp-cleanup.sh': 'Clean up /tmp/claude-*-cwd files on session end',
351
+ 'hook-debug-wrapper.sh': 'Wrap any hook to log input/output/exit/timing',
352
+ 'loop-detector.sh': 'Detect and break command repetition loops',
342
353
  },
343
354
  };
344
355
 
345
356
  console.log();
346
357
  console.log(c.bold + ' cc-safe-setup --examples' + c.reset);
347
- console.log(c.dim + ' 25 hooks beyond the 8 built-in ones' + c.reset);
358
+ console.log(c.dim + ' 30 hooks beyond the 8 built-in ones' + c.reset);
348
359
  console.log();
349
360
 
350
361
  for (const [cat, hooks] of Object.entries(CATEGORIES)) {
@@ -769,6 +780,147 @@ async function fullSetup() {
769
780
  console.log();
770
781
  }
771
782
 
783
+ function share() {
784
+ console.log();
785
+ console.log(c.bold + ' cc-safe-setup --share' + c.reset);
786
+ console.log();
787
+
788
+ if (!existsSync(SETTINGS_PATH)) {
789
+ console.log(c.red + ' No settings.json found.' + c.reset);
790
+ process.exit(1);
791
+ }
792
+
793
+ const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
794
+
795
+ // Strip sensitive data — only keep hooks and permissions structure
796
+ const shareable = {
797
+ hooks: settings.hooks || {},
798
+ permissions: settings.permissions || {},
799
+ defaultMode: settings.defaultMode,
800
+ };
801
+
802
+ // Remove full file paths, keep only script names
803
+ for (const trigger of Object.keys(shareable.hooks)) {
804
+ for (const entry of shareable.hooks[trigger]) {
805
+ for (const h of (entry.hooks || [])) {
806
+ if (h.command) {
807
+ // Keep only the filename
808
+ h.command = h.command.split('/').pop();
809
+ }
810
+ }
811
+ }
812
+ }
813
+
814
+ const json = JSON.stringify(shareable);
815
+ const encoded = Buffer.from(json).toString('base64');
816
+ const url = 'https://yurukusa.github.io/cc-safe-setup/?config=' + encoded;
817
+
818
+ console.log(c.green + ' Shareable URL:' + c.reset);
819
+ console.log();
820
+ console.log(' ' + url);
821
+ console.log();
822
+ console.log(c.dim + ' Anyone with this URL can audit your hook setup in their browser.' + c.reset);
823
+ console.log(c.dim + ' Only hook names and permissions are shared (no file paths or secrets).' + c.reset);
824
+ console.log();
825
+ }
826
+
827
+ function diff(otherFile) {
828
+ console.log();
829
+ console.log(c.bold + ' cc-safe-setup --diff' + c.reset);
830
+ console.log();
831
+
832
+ if (!existsSync(otherFile)) {
833
+ console.log(c.red + ' File not found: ' + otherFile + c.reset);
834
+ process.exit(1);
835
+ }
836
+
837
+ if (!existsSync(SETTINGS_PATH)) {
838
+ console.log(c.red + ' No local settings.json found.' + c.reset);
839
+ process.exit(1);
840
+ }
841
+
842
+ let local, other;
843
+ try { local = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch { console.log(c.red + ' Cannot parse local settings.json' + c.reset); process.exit(1); }
844
+ try { other = JSON.parse(readFileSync(otherFile, 'utf-8')); } catch { console.log(c.red + ' Cannot parse ' + otherFile + c.reset); process.exit(1); }
845
+
846
+ const getHookCommands = (settings, trigger) => {
847
+ return (settings.hooks?.[trigger] || []).flatMap(e => (e.hooks || []).map(h => {
848
+ const cmd = h.command || '';
849
+ return cmd.split('/').pop().replace(/\.sh$|\.js$|\.py$/, '');
850
+ }));
851
+ };
852
+
853
+ const getAllow = (settings) => settings.permissions?.allow || [];
854
+ const getDeny = (settings) => settings.permissions?.deny || [];
855
+
856
+ console.log(c.dim + ' Local: ' + SETTINGS_PATH + c.reset);
857
+ console.log(c.dim + ' Other: ' + otherFile + c.reset);
858
+ console.log();
859
+
860
+ // Compare hooks
861
+ const triggers = [...new Set([...Object.keys(local.hooks || {}), ...Object.keys(other.hooks || {})])];
862
+
863
+ let diffs = 0;
864
+ for (const trigger of triggers) {
865
+ const localHooks = new Set(getHookCommands(local, trigger));
866
+ const otherHooks = new Set(getHookCommands(other, trigger));
867
+
868
+ const onlyLocal = [...localHooks].filter(h => !otherHooks.has(h));
869
+ const onlyOther = [...otherHooks].filter(h => !localHooks.has(h));
870
+ const both = [...localHooks].filter(h => otherHooks.has(h));
871
+
872
+ if (onlyLocal.length > 0 || onlyOther.length > 0) {
873
+ console.log(c.bold + ' ' + trigger + c.reset);
874
+ for (const h of both) console.log(c.dim + ' = ' + h + c.reset);
875
+ for (const h of onlyLocal) { console.log(c.green + ' + ' + h + ' (local only)' + c.reset); diffs++; }
876
+ for (const h of onlyOther) { console.log(c.red + ' - ' + h + ' (other only)' + c.reset); diffs++; }
877
+ console.log();
878
+ }
879
+ }
880
+
881
+ // Compare permissions
882
+ const localAllow = getAllow(local);
883
+ const otherAllow = getAllow(other);
884
+ const onlyLocalAllow = localAllow.filter(a => !otherAllow.includes(a));
885
+ const onlyOtherAllow = otherAllow.filter(a => !localAllow.includes(a));
886
+
887
+ if (onlyLocalAllow.length > 0 || onlyOtherAllow.length > 0) {
888
+ console.log(c.bold + ' permissions.allow' + c.reset);
889
+ for (const a of onlyLocalAllow) { console.log(c.green + ' + ' + a + ' (local only)' + c.reset); diffs++; }
890
+ for (const a of onlyOtherAllow) { console.log(c.red + ' - ' + a + ' (other only)' + c.reset); diffs++; }
891
+ console.log();
892
+ }
893
+
894
+ // Compare deny
895
+ const localDeny = getDeny(local);
896
+ const otherDeny = getDeny(other);
897
+ const onlyLocalDeny = localDeny.filter(a => !otherDeny.includes(a));
898
+ const onlyOtherDeny = otherDeny.filter(a => !localDeny.includes(a));
899
+
900
+ if (onlyLocalDeny.length > 0 || onlyOtherDeny.length > 0) {
901
+ console.log(c.bold + ' permissions.deny' + c.reset);
902
+ for (const d of onlyLocalDeny) { console.log(c.green + ' + ' + d + ' (local only)' + c.reset); diffs++; }
903
+ for (const d of onlyOtherDeny) { console.log(c.red + ' - ' + d + ' (other only)' + c.reset); diffs++; }
904
+ console.log();
905
+ }
906
+
907
+ // Compare mode
908
+ if ((local.defaultMode || 'default') !== (other.defaultMode || 'default')) {
909
+ console.log(c.bold + ' defaultMode' + c.reset);
910
+ console.log(c.green + ' local: ' + (local.defaultMode || 'default') + c.reset);
911
+ console.log(c.red + ' other: ' + (other.defaultMode || 'default') + c.reset);
912
+ console.log();
913
+ diffs++;
914
+ }
915
+
916
+ if (diffs === 0) {
917
+ console.log(c.green + ' No differences found.' + c.reset);
918
+ } else {
919
+ console.log(c.dim + ' ' + diffs + ' difference(s) found.' + c.reset);
920
+ }
921
+ console.log();
922
+ }
923
+
772
924
  async function lint() {
773
925
  console.log();
774
926
  console.log(c.bold + ' cc-safe-setup --lint' + c.reset);
@@ -1832,6 +1984,8 @@ async function main() {
1832
1984
  if (FULL) return fullSetup();
1833
1985
  if (DOCTOR) return doctor();
1834
1986
  if (WATCH) return watch();
1987
+ if (SHARE) return share();
1988
+ if (DIFF_FILE) return diff(DIFF_FILE);
1835
1989
  if (LINT) return lint();
1836
1990
  if (CREATE_DESC) return createHook(CREATE_DESC);
1837
1991
  if (STATS) return stats();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "3.3.0",
4
- "description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 30 installable examples. Destructive blocker, branch guard, compound command approver, database wipe protection, tmp cleanup, and more.",
3
+ "version": "3.5.0",
4
+ "description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 33 examples. Create, audit, lint, diff, share, watch, learn. 2,500+ daily npm downloads.",
5
5
  "main": "index.mjs",
6
6
  "bin": {
7
7
  "cc-safe-setup": "index.mjs"