cc-safe-setup 6.3.1 → 7.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/README.md CHANGED
@@ -64,6 +64,37 @@ Claude Code ships with no safety hooks by default. This tool fixes that.
64
64
 
65
65
  Each hook exists because a real incident happened without it.
66
66
 
67
+ ## All 26 Commands
68
+
69
+ | Command | What It Does |
70
+ |---------|-------------|
71
+ | `npx cc-safe-setup` | Install 8 safety hooks |
72
+ | `--create "desc"` | Generate hook from plain English |
73
+ | `--audit [--fix\|--json\|--badge]` | Safety score 0-100 |
74
+ | `--lint` | Static analysis of config |
75
+ | `--diff <file>` | Compare settings |
76
+ | `--compare <a> <b>` | Side-by-side hook comparison |
77
+ | `--migrate` | Detect hooks from other projects |
78
+ | `--generate-ci` | Create GitHub Actions workflow |
79
+ | `--share` | Generate shareable URL |
80
+ | `--benchmark` | Measure hook speed |
81
+ | `--dashboard` | Real-time terminal UI |
82
+ | `--issues` | GitHub Issues each hook addresses |
83
+ | `--doctor` | Diagnose hook problems |
84
+ | `--watch` | Live blocked command feed |
85
+ | `--stats` | Block history analytics |
86
+ | `--learn [--apply]` | Pattern learning |
87
+ | `--scan [--apply]` | Tech stack detection |
88
+ | `--export / --import` | Team config sharing |
89
+ | `--verify` | Test each hook |
90
+ | `--install-example <name>` | Install from 51 examples |
91
+ | `--examples [filter]` | Browse examples by keyword |
92
+ | `--full` | All-in-one setup |
93
+ | `--status` | Check installed hooks |
94
+ | `--dry-run` | Preview changes |
95
+ | `--uninstall` | Remove all hooks |
96
+ | `--help` | Show help |
97
+
67
98
  ## How It Works
68
99
 
69
100
  1. Writes hook scripts to `~/.claude/hooks/`
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # auto-approve-cargo.sh — Auto-approve Rust cargo commands
3
+ # TRIGGER: PreToolUse MATCHER: "Bash"
4
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
5
+ [ -z "$COMMAND" ] && exit 0
6
+ if echo "$COMMAND" | grep -qE '^\s*cargo\s+(build|test|check|clippy|fmt|run|bench|doc|clean)(\s|$)'; then
7
+ jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"cargo command auto-approved"}}'
8
+ fi
9
+ exit 0
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # auto-approve-go.sh — Auto-approve Go build/test/vet commands
3
+ # TRIGGER: PreToolUse MATCHER: "Bash"
4
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
5
+ [ -z "$COMMAND" ] && exit 0
6
+ if echo "$COMMAND" | grep -qE '^\s*go\s+(build|test|vet|fmt|mod|run|generate|install|clean)(\s|$)'; then
7
+ jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"go command auto-approved"}}'
8
+ fi
9
+ exit 0
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # auto-approve-gradle.sh — Auto-approve Gradle build/test commands
3
+ # TRIGGER: PreToolUse MATCHER: "Bash"
4
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
5
+ [ -z "$COMMAND" ] && exit 0
6
+ if echo "$COMMAND" | grep -qE '^\s*(gradle|gradlew|./gradlew)\s+(build|test|check|assemble|clean|compileJava|compileKotlin|lint)(\s|$)'; then
7
+ jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"gradle command auto-approved"}}'
8
+ fi
9
+ exit 0
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # auto-approve-make.sh — Auto-approve common Make targets
3
+ # TRIGGER: PreToolUse MATCHER: "Bash"
4
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
5
+ [ -z "$COMMAND" ] && exit 0
6
+ if echo "$COMMAND" | grep -qE '^\s*make\s+(build|test|lint|format|check|clean|install|all|dev|start|run)(\s|$)'; then
7
+ jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"make target auto-approved"}}'
8
+ fi
9
+ exit 0
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # auto-approve-maven.sh — Auto-approve Maven build/test commands
3
+ # TRIGGER: PreToolUse MATCHER: "Bash"
4
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
5
+ [ -z "$COMMAND" ] && exit 0
6
+ if echo "$COMMAND" | grep -qE '^\s*(mvn|mvnw|./mvnw)\s+(compile|test|verify|package|clean|install)(\s|$)'; then
7
+ jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"maven command auto-approved"}}'
8
+ fi
9
+ exit 0
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ # max-line-length-check.sh — Warn on lines exceeding max length after edit
3
+ # TRIGGER: PostToolUse MATCHER: "Edit|Write"
4
+ FILE=$(cat | jq -r '.tool_input.file_path // empty' 2>/dev/null)
5
+ [ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
6
+ MAX="${CC_MAX_LINE_LENGTH:-120}"
7
+ LONG=$(awk -v max="$MAX" 'length > max {count++} END {print count+0}' "$FILE" 2>/dev/null)
8
+ [ "$LONG" -gt 0 ] && echo "NOTE: $FILE has $LONG lines exceeding $MAX characters." >&2
9
+ exit 0
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ # no-deploy-friday.sh — Block deploys on Fridays
3
+ # TRIGGER: PreToolUse MATCHER: "Bash"
4
+ # "Don't deploy on Friday" — every ops team ever
5
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
6
+ [ -z "$COMMAND" ] && exit 0
7
+ DOW=$(date +%u) # 5 = Friday
8
+ if [ "$DOW" = "5" ]; then
9
+ if echo "$COMMAND" | grep -qiE '(deploy|firebase|vercel|netlify|fly\s+deploy|heroku|aws\s+s3\s+sync|kubectl\s+apply|docker\s+push)'; then
10
+ echo "BLOCKED: No deploys on Friday." >&2
11
+ echo "Come back Monday." >&2
12
+ exit 2
13
+ fi
14
+ fi
15
+ exit 0
@@ -0,0 +1,28 @@
1
+ # Python Hook Examples
2
+
3
+ Same functionality as the bash hooks, written in Python.
4
+
5
+ | Hook | What It Does |
6
+ |------|-------------|
7
+ | [destructive_guard.py](destructive_guard.py) | Block rm -rf, git reset --hard, PowerShell destructive |
8
+ | [secret_guard.py](secret_guard.py) | Block git add .env, credential files |
9
+
10
+ ## Usage
11
+
12
+ ```json
13
+ {
14
+ "hooks": {
15
+ "PreToolUse": [{
16
+ "matcher": "Bash",
17
+ "hooks": [{"type": "command", "command": "python3 /path/to/destructive_guard.py"}]
18
+ }]
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Why Python?
24
+
25
+ - Easier to extend with complex logic
26
+ - Better string handling for pattern matching
27
+ - Familiar to Python developers
28
+ - Same exit code convention: 0=allow, 2=block
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """Destructive Command Guard — Python version.
3
+
4
+ Blocks rm -rf on sensitive paths, git reset --hard, git clean -fd,
5
+ PowerShell Remove-Item, and sudo with dangerous commands.
6
+
7
+ Usage in settings.json:
8
+ {
9
+ "hooks": {
10
+ "PreToolUse": [{
11
+ "matcher": "Bash",
12
+ "hooks": [{"type": "command", "command": "python3 /path/to/destructive_guard.py"}]
13
+ }]
14
+ }
15
+ }
16
+ """
17
+
18
+ import json
19
+ import re
20
+ import sys
21
+
22
+ def main():
23
+ try:
24
+ data = json.load(sys.stdin)
25
+ except (json.JSONDecodeError, EOFError):
26
+ sys.exit(0)
27
+
28
+ command = data.get("tool_input", {}).get("command", "")
29
+ if not command:
30
+ sys.exit(0)
31
+
32
+ # rm -rf on sensitive paths
33
+ if re.search(r'rm\s+(-[rf]+\s+)*(\/|~|\.\.\/)', command):
34
+ safe_dirs = {"node_modules", "dist", "build", ".cache", "__pycache__", "coverage"}
35
+ if not any(command.rstrip().endswith(d) for d in safe_dirs):
36
+ block("rm on sensitive path detected")
37
+
38
+ # git reset --hard
39
+ if re.search(r'(^|;|&&)\s*git\s+reset\s+--hard', command):
40
+ block("git reset --hard discards all uncommitted changes")
41
+
42
+ # git clean -fd
43
+ if re.search(r'(^|;|&&)\s*git\s+clean\s+-[a-z]*[fd]', command):
44
+ block("git clean removes untracked files permanently")
45
+
46
+ # git checkout/switch --force
47
+ if re.search(r'(^|;|&&)\s*git\s+(checkout|switch)\s+.*(--force|-f\b)', command):
48
+ block("git checkout --force discards uncommitted changes")
49
+
50
+ # PowerShell destructive
51
+ if re.search(r'Remove-Item.*-Recurse.*-Force|rd\s+/s\s+/q', command, re.IGNORECASE):
52
+ block("Destructive PowerShell command detected")
53
+
54
+ # sudo with dangerous commands
55
+ if re.search(r'^\s*sudo\s+(rm\s+-[rf]|chmod\s+(-R\s+)?777|dd\s+if=|mkfs)', command):
56
+ block("sudo with dangerous command detected")
57
+
58
+ sys.exit(0)
59
+
60
+ def block(reason):
61
+ print(f"BLOCKED: {reason}", file=sys.stderr)
62
+ sys.exit(2)
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+ """Secret Leak Prevention — Python version.
3
+
4
+ Blocks git add .env, credential files, and git add . with .env present.
5
+
6
+ Usage in settings.json:
7
+ {
8
+ "hooks": {
9
+ "PreToolUse": [{
10
+ "matcher": "Bash",
11
+ "hooks": [{"type": "command", "command": "python3 /path/to/secret_guard.py"}]
12
+ }]
13
+ }
14
+ }
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+
22
+ def main():
23
+ try:
24
+ data = json.load(sys.stdin)
25
+ except (json.JSONDecodeError, EOFError):
26
+ sys.exit(0)
27
+
28
+ command = data.get("tool_input", {}).get("command", "")
29
+ if not command or not re.search(r'^\s*git\s+add', command):
30
+ sys.exit(0)
31
+
32
+ # Direct .env staging
33
+ if re.search(r'git\s+add\s+.*\.env(\s|$|\.)', command, re.IGNORECASE):
34
+ block(".env file staging — add to .gitignore instead")
35
+
36
+ # Credential files
37
+ patterns = [r'credentials', r'\.pem$', r'\.key$', r'\.p12$', r'id_rsa', r'id_ed25519']
38
+ for p in patterns:
39
+ if re.search(rf'git\s+add\s+.*{p}', command, re.IGNORECASE):
40
+ block("Credential/key file — never commit these")
41
+
42
+ # git add . with .env present
43
+ if re.search(r'git\s+add\s+(-A|--all|\.)\s*$', command):
44
+ if os.path.exists(".env") or os.path.exists(".env.local"):
45
+ block("git add . with .env present — stage specific files instead")
46
+
47
+ sys.exit(0)
48
+
49
+ def block(reason):
50
+ print(f"BLOCKED: {reason}", file=sys.stderr)
51
+ sys.exit(2)
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ # require-issue-ref.sh — Warn when commit message lacks issue reference
3
+ # TRIGGER: PreToolUse MATCHER: "Bash"
4
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
5
+ [ -z "$COMMAND" ] && exit 0
6
+ if echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
7
+ MSG=$(echo "$COMMAND" | grep -oP "\-m\s+['\"]?\K[^'\"]+")
8
+ if [ -n "$MSG" ]; then
9
+ if ! echo "$MSG" | grep -qE '#[0-9]+|[A-Z]+-[0-9]+'; then
10
+ echo "WARNING: Commit message has no issue reference (#123 or PROJ-123)." >&2
11
+ echo "Consider linking to an issue for traceability." >&2
12
+ fi
13
+ fi
14
+ fi
15
+ exit 0
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # work-hours-guard.sh — Restrict risky operations outside work hours
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # During off-hours (nights/weekends), block high-risk operations
7
+ # that a human should review. Safe read-only ops still pass.
8
+ #
9
+ # TRIGGER: PreToolUse
10
+ # MATCHER: "Bash"
11
+ #
12
+ # CONFIGURATION:
13
+ # CC_WORK_START=9 (default: 9am)
14
+ # CC_WORK_END=18 (default: 6pm)
15
+ # CC_WORK_DAYS=12345 (default: Mon-Fri, 1=Mon 7=Sun)
16
+ # ================================================================
17
+
18
+ INPUT=$(cat)
19
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
20
+ [ -z "$COMMAND" ] && exit 0
21
+
22
+ HOUR=$(date +%H)
23
+ DOW=$(date +%u) # 1=Monday, 7=Sunday
24
+
25
+ START="${CC_WORK_START:-9}"
26
+ END="${CC_WORK_END:-18}"
27
+ DAYS="${CC_WORK_DAYS:-12345}"
28
+
29
+ # Check if within work hours
30
+ IN_HOURS=0
31
+ if echo "$DAYS" | grep -q "$DOW"; then
32
+ if [ "$HOUR" -ge "$START" ] && [ "$HOUR" -lt "$END" ]; then
33
+ IN_HOURS=1
34
+ fi
35
+ fi
36
+
37
+ # During work hours, allow everything
38
+ [ "$IN_HOURS" = "1" ] && exit 0
39
+
40
+ # Outside work hours, block high-risk operations
41
+ if echo "$COMMAND" | grep -qE 'git\s+push|deploy|npm\s+publish|docker\s+push'; then
42
+ echo "BLOCKED: High-risk operation outside work hours ($HOUR:00)." >&2
43
+ echo "Command: $COMMAND" >&2
44
+ echo "Work hours: ${START}:00-${END}:00 (days: $DAYS)" >&2
45
+ exit 2
46
+ fi
47
+
48
+ exit 0
package/index.mjs CHANGED
@@ -88,6 +88,7 @@ const DASHBOARD = process.argv.includes('--dashboard');
88
88
  const ISSUES = process.argv.includes('--issues');
89
89
  const MIGRATE = process.argv.includes('--migrate');
90
90
  const GENERATE_CI = process.argv.includes('--generate-ci');
91
+ const REPORT = process.argv.includes('--report');
91
92
  const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
92
93
  const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
93
94
  const CREATE_IDX = process.argv.findIndex(a => a === '--create');
@@ -828,6 +829,70 @@ async function fullSetup() {
828
829
  console.log();
829
830
  }
830
831
 
832
+ async function report() {
833
+ // Generate markdown safety report
834
+ let hookCount = 0;
835
+ let scriptCount = 0;
836
+ let auditScore = 0;
837
+
838
+ if (existsSync(SETTINGS_PATH)) {
839
+ try {
840
+ const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
841
+ for (const entries of Object.values(s.hooks || {})) {
842
+ hookCount += entries.reduce((n, e) => n + (e.hooks || []).length, 0);
843
+ }
844
+ // Quick audit
845
+ let risks = 0;
846
+ const pre = s.hooks?.PreToolUse || [];
847
+ const post = s.hooks?.PostToolUse || [];
848
+ if (pre.length === 0) risks += 30;
849
+ const allCmds = JSON.stringify(pre).toLowerCase();
850
+ if (!allCmds.match(/destructive|guard|rm.*rf/)) risks += 20;
851
+ if (!allCmds.match(/branch|push|main/)) risks += 20;
852
+ if (!allCmds.match(/secret|env|credential/)) risks += 20;
853
+ if (post.length === 0) risks += 10;
854
+ auditScore = Math.max(0, 100 - risks);
855
+ } catch {}
856
+ }
857
+
858
+ if (existsSync(HOOKS_DIR)) {
859
+ const fsModule = await import('fs');
860
+ scriptCount = fsModule.readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh')).length;
861
+ }
862
+
863
+ const grade = auditScore >= 80 ? 'A' : auditScore >= 60 ? 'B' : auditScore >= 40 ? 'C' : 'F';
864
+ const emoji = auditScore >= 80 ? '🟢' : auditScore >= 50 ? '🟡' : '🔴';
865
+
866
+ const blockLog = join(HOME, '.claude', 'blocked-commands.log');
867
+ let totalBlocks = 0;
868
+ if (existsSync(blockLog)) {
869
+ totalBlocks = readFileSync(blockLog, 'utf-8').split('\n').filter(l => l.trim()).length;
870
+ }
871
+
872
+ const md = `## ${emoji} Claude Code Safety Report
873
+
874
+ | Metric | Value |
875
+ |--------|-------|
876
+ | Safety Score | **${auditScore}/100** (Grade ${grade}) |
877
+ | Hooks Registered | ${hookCount} |
878
+ | Hook Scripts | ${scriptCount} |
879
+ | Commands Blocked | ${totalBlocks} |
880
+ | Generated | ${new Date().toISOString().split('T')[0]} |
881
+
882
+ ### Quick Actions
883
+ - Audit: \`npx cc-safe-setup --audit\`
884
+ - Dashboard: \`npx cc-safe-setup --dashboard\`
885
+ - Find hooks: \`npx cc-hook-registry recommend\`
886
+ `;
887
+
888
+ console.log(md);
889
+
890
+ // Also write to file
891
+ const reportPath = join(process.cwd(), 'SAFETY_REPORT.md');
892
+ writeFileSync(reportPath, md);
893
+ console.log(c.green + 'Report saved: ' + reportPath + c.reset);
894
+ }
895
+
831
896
  function generateCI() {
832
897
  const workflowDir = join(process.cwd(), '.github', 'workflows');
833
898
  const workflowPath = join(workflowDir, 'claude-code-safety.yml');
@@ -2575,6 +2640,7 @@ async function main() {
2575
2640
  if (FULL) return fullSetup();
2576
2641
  if (DOCTOR) return doctor();
2577
2642
  if (WATCH) return watch();
2643
+ if (REPORT) return report();
2578
2644
  if (GENERATE_CI) return generateCI();
2579
2645
  if (MIGRATE) return migrate();
2580
2646
  if (COMPARE) return compare(COMPARE.a, COMPARE.b);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "6.3.1",
3
+ "version": "7.1.0",
4
4
  "description": "One command to make Claude Code safe. 59 hooks (8 built-in + 51 examples). 26 CLI commands: dashboard, create, audit, lint, diff, migrate, compare, generate-ci. 284 tests.",
5
5
  "main": "index.mjs",
6
6
  "bin": {