cc-safe-setup 7.9.0 → 8.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/CLAUDE.md +18 -0
- package/README.md +3 -1
- package/examples/backup-before-refactor.sh +10 -0
- package/examples/branch-naming-convention.sh +13 -0
- package/examples/changelog-reminder.sh +22 -0
- package/examples/commit-scope-guard.sh +32 -0
- package/examples/file-size-limit.sh +12 -0
- package/examples/hardcoded-secret-detector.sh +60 -0
- package/examples/license-check.sh +13 -0
- package/examples/no-console-log.sh +10 -0
- package/examples/no-eval.sh +9 -0
- package/examples/no-todo-ship.sh +12 -0
- package/examples/no-wildcard-import.sh +9 -0
- package/examples/pr-description-check.sh +11 -0
- package/examples/rate-limit-guard.sh +15 -0
- package/examples/worktree-guard.sh +28 -0
- package/index.mjs +207 -0
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Project Rules
|
|
2
|
+
|
|
3
|
+
## Safety
|
|
4
|
+
- Do not push to main/master directly
|
|
5
|
+
- Do not force-push
|
|
6
|
+
- Do not delete files outside this project
|
|
7
|
+
- Do not commit .env or credential files
|
|
8
|
+
- Run tests before committing
|
|
9
|
+
|
|
10
|
+
## Code Style
|
|
11
|
+
- Follow existing conventions
|
|
12
|
+
- Keep functions small and focused
|
|
13
|
+
- Add comments only when the logic isn't obvious
|
|
14
|
+
|
|
15
|
+
## Git
|
|
16
|
+
- Use descriptive commit messages
|
|
17
|
+
- One logical change per commit
|
|
18
|
+
- Create feature branches for new work
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
|
|
8
8
|
|
|
9
|
-
8 built-in +
|
|
9
|
+
8 built-in + 92 examples = **100 hooks**. 29 CLI commands. 433 tests. [Web Tool](https://yurukusa.github.io/cc-safe-setup/) · [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Builder](https://yurukusa.github.io/cc-safe-setup/builder.html) · [FAQ](https://yurukusa.github.io/cc-safe-setup/faq.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx cc-safe-setup
|
|
@@ -103,6 +103,8 @@ Each hook exists because a real incident happened without it.
|
|
|
103
103
|
|
|
104
104
|
Safe to run multiple times. Existing settings are preserved. A backup is created if settings.json can't be parsed.
|
|
105
105
|
|
|
106
|
+
**Maximum safety:** `npx cc-safe-setup --shield` — one command: fix environment, install hooks, detect stack, configure settings, generate CLAUDE.md.
|
|
107
|
+
|
|
106
108
|
**Preview first:** `npx cc-safe-setup --dry-run`
|
|
107
109
|
|
|
108
110
|
**Check status:** `npx cc-safe-setup --status` — see which hooks are installed (exit code 1 if missing).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
if echo "$COMMAND" | grep -qE '\bgit\s+mv\b.*\b(src|lib|app)\b'; then
|
|
7
|
+
git stash push -m "pre-refactor-backup-$(date +%s)" 2>/dev/null
|
|
8
|
+
echo "NOTE: Stashed changes as pre-refactor backup." >&2
|
|
9
|
+
fi
|
|
10
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
if echo "$COMMAND" | grep -qE '\bgit\s+(checkout|switch)\s+-b\s+'; then
|
|
7
|
+
BRANCH=$(echo "$COMMAND" | grep -oE '(-b|--create)\s+(\S+)' | awk '{print $2}')
|
|
8
|
+
if [ -n "$BRANCH" ] && ! echo "$BRANCH" | grep -qE '^(feat|fix|chore|docs|test|refactor)/'; then
|
|
9
|
+
echo "WARNING: Branch '$BRANCH' doesn't follow convention." >&2
|
|
10
|
+
echo "Use: feat/, fix/, chore/, docs/, test/, refactor/" >&2
|
|
11
|
+
fi
|
|
12
|
+
fi
|
|
13
|
+
exit 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# changelog-reminder.sh — Remind to update CHANGELOG on version bump
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# When Claude bumps a version number (npm version, cargo set-version,
|
|
7
|
+
# etc.), this hook reminds to update CHANGELOG.md with the changes.
|
|
8
|
+
#
|
|
9
|
+
# TRIGGER: PostToolUse MATCHER: "Bash"
|
|
10
|
+
# ================================================================
|
|
11
|
+
|
|
12
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
13
|
+
[ -z "$COMMAND" ] && exit 0
|
|
14
|
+
|
|
15
|
+
# Detect version bump commands
|
|
16
|
+
if echo "$COMMAND" | grep -qE '(npm\s+version|cargo\s+set-version|bump2version|poetry\s+version)'; then
|
|
17
|
+
if [ -f "CHANGELOG.md" ]; then
|
|
18
|
+
echo "REMINDER: Update CHANGELOG.md with the new version's changes." >&2
|
|
19
|
+
fi
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exit 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# commit-scope-guard.sh — Warn when committing too many files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code can modify dozens of files and commit them all at
|
|
7
|
+
# once, making the commit hard to review and revert. This hook
|
|
8
|
+
# warns when staging more than a configurable number of files.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_MAX_COMMIT_FILES=15 (warn above 15 files)
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
17
|
+
[ -z "$COMMAND" ] && exit 0
|
|
18
|
+
|
|
19
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
20
|
+
|
|
21
|
+
MAX="${CC_MAX_COMMIT_FILES:-15}"
|
|
22
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l)
|
|
23
|
+
|
|
24
|
+
if [ "$STAGED" -gt "$MAX" ]; then
|
|
25
|
+
echo "WARNING: Committing $STAGED files (threshold: $MAX)." >&2
|
|
26
|
+
echo "Consider splitting into smaller, focused commits." >&2
|
|
27
|
+
echo "Files:" >&2
|
|
28
|
+
git diff --cached --name-only 2>/dev/null | head -10 | sed 's/^/ /' >&2
|
|
29
|
+
[ "$STAGED" -gt 10 ] && echo " ... and $((STAGED-10)) more" >&2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
exit 0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
LEN=${#CONTENT}
|
|
7
|
+
MAX="${CC_MAX_FILE_SIZE:-1048576}"
|
|
8
|
+
if [ "$LEN" -gt "$MAX" ]; then
|
|
9
|
+
echo "BLOCKED: File content is ${LEN} bytes (limit: ${MAX})." >&2
|
|
10
|
+
exit 2
|
|
11
|
+
fi
|
|
12
|
+
exit 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# hardcoded-secret-detector.sh — Detect hardcoded secrets in edits
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude sometimes hardcodes API keys, passwords, or tokens
|
|
7
|
+
# directly into source files instead of using environment
|
|
8
|
+
# variables. This hook checks edited content for secret patterns.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PostToolUse MATCHER: "Edit|Write"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
INPUT=$(cat)
|
|
14
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
15
|
+
[ -z "$CONTENT" ] && exit 0
|
|
16
|
+
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
# Skip config/env files (secrets are expected there)
|
|
20
|
+
case "$FILE" in
|
|
21
|
+
*.env*|*credentials*|*secret*|*.key|*.pem) exit 0 ;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
FOUND=0
|
|
25
|
+
|
|
26
|
+
# AWS keys (AKIA...)
|
|
27
|
+
if echo "$CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
|
|
28
|
+
echo "WARNING: Possible AWS access key in $FILE" >&2
|
|
29
|
+
FOUND=1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Generic API key patterns
|
|
33
|
+
if echo "$CONTENT" | grep -qE "(api_key|apikey|api-key|secret_key|access_token)\s*[=:]\s*['\"][a-zA-Z0-9]{20,}['\"]"; then
|
|
34
|
+
echo "WARNING: Possible hardcoded API key in $FILE" >&2
|
|
35
|
+
FOUND=1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Password patterns
|
|
39
|
+
if echo "$CONTENT" | grep -qiE "(password|passwd|pwd)\s*[=:]\s*['\"][^'\"]{8,}['\"]"; then
|
|
40
|
+
echo "WARNING: Possible hardcoded password in $FILE" >&2
|
|
41
|
+
FOUND=1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# JWT tokens
|
|
45
|
+
if echo "$CONTENT" | grep -qE 'eyJ[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]{20,}'; then
|
|
46
|
+
echo "WARNING: Possible JWT token in $FILE" >&2
|
|
47
|
+
FOUND=1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Private keys
|
|
51
|
+
if echo "$CONTENT" | grep -qE 'BEGIN (RSA |EC |DSA )?PRIVATE KEY'; then
|
|
52
|
+
echo "WARNING: Private key detected in $FILE" >&2
|
|
53
|
+
FOUND=1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ "$FOUND" -eq 1 ]; then
|
|
57
|
+
echo "Use environment variables instead of hardcoding secrets." >&2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# license-check.sh — Warn when creating files without a license header
|
|
3
|
+
# TRIGGER: PostToolUse MATCHER: "Write"
|
|
4
|
+
FILE=$(cat | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$FILE" ] && exit 0
|
|
6
|
+
case "$FILE" in *.js|*.ts|*.py|*.go|*.rs|*.java|*.rb|*.sh) ;; *) exit 0 ;; esac
|
|
7
|
+
[ ! -f "$FILE" ] && exit 0
|
|
8
|
+
if ! head -5 "$FILE" | grep -qiE '(license|copyright|MIT|Apache|GPL)'; then
|
|
9
|
+
if [ -f "LICENSE" ] || [ -f "LICENSE.md" ]; then
|
|
10
|
+
echo "NOTE: New source file $FILE has no license header." >&2
|
|
11
|
+
fi
|
|
12
|
+
fi
|
|
13
|
+
exit 0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
case "$FILE" in *.test.*|*.spec.*|*debug*) exit 0 ;; esac
|
|
7
|
+
if echo "$CONTENT" | grep -qE '\bconsole\.(log|debug)\b'; then
|
|
8
|
+
echo "WARNING: console.log detected in $FILE. Use proper logging." >&2
|
|
9
|
+
fi
|
|
10
|
+
exit 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
if echo "$CONTENT" | grep -qE '\beval\s*\('; then
|
|
7
|
+
echo "WARNING: eval() detected in $FILE. Avoid eval for security." >&2
|
|
8
|
+
fi
|
|
9
|
+
exit 0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# no-todo-ship.sh — Block commits with TODO/FIXME/HACK markers
|
|
3
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
4
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
7
|
+
TODOS=$(git diff --cached 2>/dev/null | grep -cE '^\+.*\b(TODO|FIXME|HACK|XXX)\b' || echo 0)
|
|
8
|
+
if [ "$TODOS" -gt 0 ]; then
|
|
9
|
+
echo "WARNING: $TODOS TODO/FIXME/HACK markers in staged changes." >&2
|
|
10
|
+
echo "Resolve them before shipping, or document why they're needed." >&2
|
|
11
|
+
fi
|
|
12
|
+
exit 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$CONTENT" ] && exit 0
|
|
6
|
+
if echo "$CONTENT" | grep -qE '(from\s+\S+\s+import\s+\*|import\s+\*\s+from)'; then
|
|
7
|
+
echo "WARNING: Wildcard import detected. Import specific names." >&2
|
|
8
|
+
fi
|
|
9
|
+
exit 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
if echo "$COMMAND" | grep -qE '\bgh\s+pr\s+create\b'; then
|
|
7
|
+
if ! echo "$COMMAND" | grep -qE '\-\-body|\-b\s'; then
|
|
8
|
+
echo "WARNING: PR created without --body description." >&2
|
|
9
|
+
fi
|
|
10
|
+
fi
|
|
11
|
+
exit 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
|
|
4
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
5
|
+
STATE="/tmp/cc-rate-limit-$(echo "$PWD" | md5sum | cut -c1-8)"
|
|
6
|
+
NOW=$(date +%s)
|
|
7
|
+
if [ -f "$STATE" ]; then
|
|
8
|
+
LAST=$(cat "$STATE")
|
|
9
|
+
DIFF=$((NOW - LAST))
|
|
10
|
+
if [ "$DIFF" -lt 1 ]; then
|
|
11
|
+
echo "WARNING: Rapid tool calls (${DIFF}s apart). Slow down." >&2
|
|
12
|
+
fi
|
|
13
|
+
fi
|
|
14
|
+
echo "$NOW" > "$STATE"
|
|
15
|
+
exit 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# worktree-guard.sh — Warn when operating in a git worktree
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Git worktrees share the same .git directory. Destructive operations
|
|
7
|
+
# in one worktree (git clean, reset) can affect the main working tree.
|
|
8
|
+
# This hook warns when Claude is operating inside a worktree.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
14
|
+
[ -z "$COMMAND" ] && exit 0
|
|
15
|
+
|
|
16
|
+
# Only check destructive git commands
|
|
17
|
+
echo "$COMMAND" | grep -qE '\bgit\s+(clean|reset|checkout\s+--|stash\s+drop)' || exit 0
|
|
18
|
+
|
|
19
|
+
# Check if we're in a worktree
|
|
20
|
+
GITDIR=$(git rev-parse --git-dir 2>/dev/null)
|
|
21
|
+
if echo "$GITDIR" | grep -q "worktrees"; then
|
|
22
|
+
MAIN_DIR=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/.git$||')
|
|
23
|
+
echo "WARNING: You are in a git worktree." >&2
|
|
24
|
+
echo "Main working tree: $MAIN_DIR" >&2
|
|
25
|
+
echo "Destructive git operations may affect the main tree." >&2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -90,6 +90,7 @@ const MIGRATE = process.argv.includes('--migrate');
|
|
|
90
90
|
const GENERATE_CI = process.argv.includes('--generate-ci');
|
|
91
91
|
const REPORT = process.argv.includes('--report');
|
|
92
92
|
const QUICKFIX = process.argv.includes('--quickfix');
|
|
93
|
+
const SHIELD = process.argv.includes('--shield');
|
|
93
94
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
94
95
|
const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
|
|
95
96
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
@@ -125,6 +126,7 @@ if (HELP) {
|
|
|
125
126
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
126
127
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
127
128
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
129
|
+
npx cc-safe-setup --shield Maximum safety in one command (fix + scan + install + CLAUDE.md)
|
|
128
130
|
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
129
131
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
130
132
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
@@ -831,6 +833,210 @@ async function fullSetup() {
|
|
|
831
833
|
console.log();
|
|
832
834
|
}
|
|
833
835
|
|
|
836
|
+
async function shield() {
|
|
837
|
+
const { execSync } = await import('child_process');
|
|
838
|
+
const { readdirSync } = await import('fs');
|
|
839
|
+
console.log();
|
|
840
|
+
console.log(c.bold + ' 🛡️ cc-safe-setup --shield' + c.reset);
|
|
841
|
+
console.log(c.dim + ' Maximum safety in one command' + c.reset);
|
|
842
|
+
console.log();
|
|
843
|
+
|
|
844
|
+
// Step 1: Fix environment issues
|
|
845
|
+
console.log(c.bold + ' Step 1: Fix environment' + c.reset);
|
|
846
|
+
await quickfix();
|
|
847
|
+
|
|
848
|
+
// Step 2: Install core safety hooks
|
|
849
|
+
console.log();
|
|
850
|
+
console.log(c.bold + ' Step 2: Install safety hooks' + c.reset);
|
|
851
|
+
// Run the default install
|
|
852
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
853
|
+
let installed = 0;
|
|
854
|
+
for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
|
|
855
|
+
const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
|
|
856
|
+
if (!existsSync(hookPath)) {
|
|
857
|
+
writeFileSync(hookPath, SCRIPTS[hookId]);
|
|
858
|
+
chmodSync(hookPath, 0o755);
|
|
859
|
+
installed++;
|
|
860
|
+
console.log(c.green + ' +' + c.reset + ` ${hookMeta.name}`);
|
|
861
|
+
} else {
|
|
862
|
+
console.log(c.dim + ' ✓' + c.reset + ` ${hookMeta.name} (already installed)`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Step 3: Detect project stack and install recommended examples
|
|
867
|
+
console.log();
|
|
868
|
+
console.log(c.bold + ' Step 3: Project-aware hooks' + c.reset);
|
|
869
|
+
const cwd = process.cwd();
|
|
870
|
+
const extras = [];
|
|
871
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
872
|
+
extras.push('auto-approve-build');
|
|
873
|
+
try {
|
|
874
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
875
|
+
if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) extras.push('block-database-wipe');
|
|
876
|
+
if (pkg.scripts?.deploy) extras.push('deploy-guard');
|
|
877
|
+
} catch {}
|
|
878
|
+
}
|
|
879
|
+
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) extras.push('auto-approve-python');
|
|
880
|
+
if (existsSync(join(cwd, 'Dockerfile'))) extras.push('auto-approve-docker');
|
|
881
|
+
if (existsSync(join(cwd, 'go.mod'))) extras.push('auto-approve-go');
|
|
882
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) extras.push('auto-approve-cargo');
|
|
883
|
+
if (existsSync(join(cwd, 'Makefile'))) extras.push('auto-approve-make');
|
|
884
|
+
if (existsSync(join(cwd, '.env'))) extras.push('env-source-guard');
|
|
885
|
+
|
|
886
|
+
// Always include these for maximum safety
|
|
887
|
+
extras.push('scope-guard', 'no-sudo-guard', 'protect-claudemd');
|
|
888
|
+
|
|
889
|
+
for (const ex of extras) {
|
|
890
|
+
const exPath = join(__dirname, 'examples', `${ex}.sh`);
|
|
891
|
+
const hookPath = join(HOOKS_DIR, `${ex}.sh`);
|
|
892
|
+
if (existsSync(exPath) && !existsSync(hookPath)) {
|
|
893
|
+
copyFileSync(exPath, hookPath);
|
|
894
|
+
chmodSync(hookPath, 0o755);
|
|
895
|
+
console.log(c.green + ' +' + c.reset + ` ${ex}`);
|
|
896
|
+
installed++;
|
|
897
|
+
} else if (existsSync(hookPath)) {
|
|
898
|
+
console.log(c.dim + ' ✓' + c.reset + ` ${ex} (already installed)`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Step 4: Update settings.json
|
|
903
|
+
console.log();
|
|
904
|
+
console.log(c.bold + ' Step 4: Configure settings.json' + c.reset);
|
|
905
|
+
let settings = {};
|
|
906
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
907
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
908
|
+
}
|
|
909
|
+
if (!settings.hooks) settings.hooks = {};
|
|
910
|
+
|
|
911
|
+
// Collect all installed hooks
|
|
912
|
+
const hookFiles = existsSync(HOOKS_DIR)
|
|
913
|
+
? readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'))
|
|
914
|
+
: [];
|
|
915
|
+
|
|
916
|
+
// Build hook entries by trigger type
|
|
917
|
+
const preToolHooks = [];
|
|
918
|
+
const postToolHooks = [];
|
|
919
|
+
const stopHooks = [];
|
|
920
|
+
|
|
921
|
+
for (const f of hookFiles) {
|
|
922
|
+
const content = readFileSync(join(HOOKS_DIR, f), 'utf-8');
|
|
923
|
+
const cmd = `bash ${join(HOOKS_DIR, f)}`;
|
|
924
|
+
|
|
925
|
+
// Check if already in settings
|
|
926
|
+
const alreadyConfigured = JSON.stringify(settings.hooks).includes(f);
|
|
927
|
+
if (alreadyConfigured) continue;
|
|
928
|
+
|
|
929
|
+
// Determine trigger from file content
|
|
930
|
+
if (content.includes('TRIGGER: Stop') || f.includes('api-error') || f.includes('revert-helper') || f.includes('session-handoff') || f.includes('compact-reminder') || f.includes('notify') || f.includes('tmp-cleanup')) {
|
|
931
|
+
stopHooks.push({ type: 'command', command: cmd });
|
|
932
|
+
} else if (content.includes('TRIGGER: PostToolUse') || f.includes('syntax-check') || f.includes('context-monitor') || f.includes('output-length') || f.includes('error-memory') || f.includes('cost-tracker')) {
|
|
933
|
+
postToolHooks.push({ type: 'command', command: cmd });
|
|
934
|
+
} else {
|
|
935
|
+
// Default: PreToolUse
|
|
936
|
+
const matcher = (f.includes('edit-guard') || f.includes('protect-dotfiles') || f.includes('overwrite-guard') || f.includes('binary-file') || f.includes('parallel-edit') || f.includes('test-deletion') || f.includes('memory-write'))
|
|
937
|
+
? 'Edit|Write'
|
|
938
|
+
: 'Bash';
|
|
939
|
+
preToolHooks.push({ type: 'command', command: cmd, _matcher: matcher });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Group PreToolUse hooks by matcher
|
|
944
|
+
if (preToolHooks.length > 0) {
|
|
945
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
946
|
+
const bashHooks = preToolHooks.filter(h => h._matcher === 'Bash').map(({ _matcher, ...h }) => h);
|
|
947
|
+
const editHooks = preToolHooks.filter(h => h._matcher === 'Edit|Write').map(({ _matcher, ...h }) => h);
|
|
948
|
+
if (bashHooks.length > 0) {
|
|
949
|
+
const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
950
|
+
if (existing) {
|
|
951
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
952
|
+
for (const h of bashHooks) {
|
|
953
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: bashHooks });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (editHooks.length > 0) {
|
|
960
|
+
const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Edit|Write');
|
|
961
|
+
if (existing) {
|
|
962
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
963
|
+
for (const h of editHooks) {
|
|
964
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
settings.hooks.PreToolUse.push({ matcher: 'Edit|Write', hooks: editHooks });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (postToolHooks.length > 0) {
|
|
972
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
973
|
+
const existing = settings.hooks.PostToolUse.find(e => e.matcher === '');
|
|
974
|
+
if (existing) {
|
|
975
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
976
|
+
for (const h of postToolHooks) {
|
|
977
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
settings.hooks.PostToolUse.push({ matcher: '', hooks: postToolHooks });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (stopHooks.length > 0) {
|
|
984
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
985
|
+
const existing = settings.hooks.Stop.find(e => e.matcher === '');
|
|
986
|
+
if (existing) {
|
|
987
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
988
|
+
for (const h of stopHooks) {
|
|
989
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
settings.hooks.Stop.push({ matcher: '', hooks: stopHooks });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
997
|
+
console.log(c.green + ' ✓' + c.reset + ' settings.json updated');
|
|
998
|
+
|
|
999
|
+
// Step 5: Generate CLAUDE.md template if none exists
|
|
1000
|
+
console.log();
|
|
1001
|
+
console.log(c.bold + ' Step 5: CLAUDE.md' + c.reset);
|
|
1002
|
+
if (!existsSync(join(cwd, 'CLAUDE.md'))) {
|
|
1003
|
+
const template = `# Project Rules
|
|
1004
|
+
|
|
1005
|
+
## Safety
|
|
1006
|
+
- Do not push to main/master directly
|
|
1007
|
+
- Do not force-push
|
|
1008
|
+
- Do not delete files outside this project
|
|
1009
|
+
- Do not commit .env or credential files
|
|
1010
|
+
- Run tests before committing
|
|
1011
|
+
|
|
1012
|
+
## Code Style
|
|
1013
|
+
- Follow existing conventions
|
|
1014
|
+
- Keep functions small and focused
|
|
1015
|
+
- Add comments only when the logic isn't obvious
|
|
1016
|
+
|
|
1017
|
+
## Git
|
|
1018
|
+
- Use descriptive commit messages
|
|
1019
|
+
- One logical change per commit
|
|
1020
|
+
- Create feature branches for new work
|
|
1021
|
+
`;
|
|
1022
|
+
writeFileSync(join(cwd, 'CLAUDE.md'), template);
|
|
1023
|
+
console.log(c.green + ' +' + c.reset + ' Created CLAUDE.md with safety rules template');
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(c.dim + ' ✓' + c.reset + ' CLAUDE.md already exists');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Summary
|
|
1029
|
+
console.log();
|
|
1030
|
+
const totalHooks = hookFiles.length;
|
|
1031
|
+
console.log(c.bold + c.green + ' 🛡️ Shield activated!' + c.reset);
|
|
1032
|
+
console.log(c.dim + ` ${totalHooks} hooks installed and configured.` + c.reset);
|
|
1033
|
+
console.log(c.dim + ' Your Claude Code sessions are now protected.' + c.reset);
|
|
1034
|
+
console.log();
|
|
1035
|
+
console.log(c.dim + ' Verify: npx cc-safe-setup --verify' + c.reset);
|
|
1036
|
+
console.log(c.dim + ' Status: npx cc-safe-setup --status' + c.reset);
|
|
1037
|
+
console.log();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
834
1040
|
async function quickfix() {
|
|
835
1041
|
const { execSync } = await import('child_process');
|
|
836
1042
|
console.log();
|
|
@@ -2836,6 +3042,7 @@ async function main() {
|
|
|
2836
3042
|
if (FULL) return fullSetup();
|
|
2837
3043
|
if (DOCTOR) return doctor();
|
|
2838
3044
|
if (WATCH) return watch();
|
|
3045
|
+
if (SHIELD) return shield();
|
|
2839
3046
|
if (QUICKFIX) return quickfix();
|
|
2840
3047
|
if (REPORT) return report();
|
|
2841
3048
|
if (GENERATE_CI) return generateCI();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.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": {
|