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 +31 -0
- package/examples/auto-approve-cargo.sh +9 -0
- package/examples/auto-approve-go.sh +9 -0
- package/examples/auto-approve-gradle.sh +9 -0
- package/examples/auto-approve-make.sh +9 -0
- package/examples/auto-approve-maven.sh +9 -0
- package/examples/max-line-length-check.sh +9 -0
- package/examples/no-deploy-friday.sh +15 -0
- package/examples/python/README.md +28 -0
- package/examples/python/__pycache__/destructive_guard.cpython-312.pyc +0 -0
- package/examples/python/__pycache__/secret_guard.cpython-312.pyc +0 -0
- package/examples/python/destructive_guard.py +65 -0
- package/examples/python/secret_guard.py +54 -0
- package/examples/require-issue-ref.sh +15 -0
- package/examples/work-hours-guard.sh +48 -0
- package/index.mjs +66 -0
- package/package.json +1 -1
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
|
|
Binary file
|
|
Binary file
|
|
@@ -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": "
|
|
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": {
|