cc-safe-setup 8.4.0 → 8.6.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.** [日本語](docs/README.ja.md)
8
8
 
9
- 8 built-in + 92 examples = **100 hooks**. 31 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)
9
+ 8 built-in + 95 examples = **103 hooks**. 33 CLI commands. 457 tests. 4 languages. [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
@@ -105,6 +105,8 @@ Safe to run multiple times. Existing settings are preserved. A backup is created
105
105
 
106
106
  **Maximum safety:** `npx cc-safe-setup --shield` — one command: fix environment, install hooks, detect stack, configure settings, generate CLAUDE.md.
107
107
 
108
+ **Team setup:** `npx cc-safe-setup --team` — copy hooks to `.claude/hooks/` with relative paths, commit to repo for team sharing.
109
+
108
110
  **Preview first:** `npx cc-safe-setup --dry-run`
109
111
 
110
112
  **Check status:** `npx cc-safe-setup --status` — see which hooks are installed (exit code 1 if missing).
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # context-snapshot.sh — Save session state before context loss
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # After /compact or when context is low, Claude loses track of
7
+ # what files were being edited, which branch it's on, and what
8
+ # the current task was. This hook saves a snapshot of the session
9
+ # state after every Stop event, so the next message can recover.
10
+ #
11
+ # TRIGGER: Stop MATCHER: ""
12
+ #
13
+ # Creates: .claude/session-snapshot.md (overwritten each time)
14
+ # ================================================================
15
+
16
+ SNAPSHOT=".claude/session-snapshot.md"
17
+ mkdir -p .claude 2>/dev/null
18
+
19
+ {
20
+ echo "# Session Snapshot (auto-generated)"
21
+ echo "Updated: $(date -Iseconds)"
22
+ echo ""
23
+
24
+ # Git state
25
+ BRANCH=$(git branch --show-current 2>/dev/null)
26
+ if [ -n "$BRANCH" ]; then
27
+ echo "## Git"
28
+ echo "- Branch: \`$BRANCH\`"
29
+ DIRTY=$(git status --porcelain 2>/dev/null | wc -l)
30
+ echo "- Uncommitted changes: $DIRTY file(s)"
31
+ if [ "$DIRTY" -gt 0 ]; then
32
+ echo '```'
33
+ git status --short 2>/dev/null | head -15
34
+ echo '```'
35
+ fi
36
+ echo "- Last commit: $(git log --oneline -1 2>/dev/null)"
37
+ echo ""
38
+ fi
39
+
40
+ # Recently modified files
41
+ echo "## Recent Files"
42
+ echo '```'
43
+ find . -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.go' -o -name '*.rs' \
44
+ -o -name '*.java' -o -name '*.sh' -o -name '*.md' 2>/dev/null \
45
+ | xargs ls -lt 2>/dev/null | head -10 | awk '{print $NF}'
46
+ echo '```'
47
+ echo ""
48
+
49
+ # Active TODO/FIXME
50
+ TODOS=$(grep -rl 'TODO\|FIXME' --include='*.js' --include='*.ts' --include='*.py' . 2>/dev/null | wc -l)
51
+ if [ "$TODOS" -gt 0 ]; then
52
+ echo "## Active TODOs: $TODOS file(s)"
53
+ echo ""
54
+ fi
55
+
56
+ } > "$SNAPSHOT" 2>/dev/null
57
+
58
+ exit 0
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # git-lfs-guard.sh — Suggest Git LFS for large binary files
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude sometimes git-adds large binary files (images, videos,
7
+ # compiled binaries) that bloat the repository. This hook warns
8
+ # when staging files larger than a threshold and suggests LFS.
9
+ #
10
+ # TRIGGER: PreToolUse MATCHER: "Bash"
11
+ #
12
+ # CONFIG:
13
+ # CC_LFS_THRESHOLD_KB=500 (warn above 500KB)
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+add' || exit 0
20
+
21
+ THRESHOLD="${CC_LFS_THRESHOLD_KB:-500}"
22
+
23
+ # Extract files being added
24
+ FILES=$(echo "$COMMAND" | sed 's/git add//' | tr ' ' '\n' | grep -v '^-' | grep -v '^$')
25
+
26
+ for f in $FILES; do
27
+ [ -f "$f" ] || continue
28
+ SIZE_KB=$(du -k "$f" 2>/dev/null | cut -f1)
29
+ [ -z "$SIZE_KB" ] && continue
30
+
31
+ if [ "$SIZE_KB" -gt "$THRESHOLD" ]; then
32
+ echo "WARNING: $f is ${SIZE_KB}KB — consider Git LFS." >&2
33
+ echo " git lfs track '$f' && git add .gitattributes $f" >&2
34
+ fi
35
+ done
36
+
37
+ exit 0
@@ -0,0 +1,69 @@
1
+ // destructive_guard.go — Claude Code PreToolUse hook in Go
2
+ //
3
+ // Blocks rm -rf /, git reset --hard, git clean -fd, and similar
4
+ // destructive commands. Exit code 2 = block, 0 = allow.
5
+ //
6
+ // Build: go build -o destructive-guard destructive_guard.go
7
+ // Usage in settings.json:
8
+ // {"type": "command", "command": "/path/to/destructive-guard"}
9
+ package main
10
+
11
+ import (
12
+ "encoding/json"
13
+ "fmt"
14
+ "io"
15
+ "os"
16
+ "regexp"
17
+ "strings"
18
+ )
19
+
20
+ type HookInput struct {
21
+ ToolInput struct {
22
+ Command string `json:"command"`
23
+ } `json:"tool_input"`
24
+ }
25
+
26
+ var dangerousPatterns = []*regexp.Regexp{
27
+ regexp.MustCompile(`\brm\s+.*-rf\s+(/|~/?\s*$|\.\./)`),
28
+ regexp.MustCompile(`\bgit\s+reset\s+--hard`),
29
+ regexp.MustCompile(`\bgit\s+clean\s+-[a-zA-Z]*f`),
30
+ regexp.MustCompile(`\bgit\s+checkout\s+--force`),
31
+ regexp.MustCompile(`\bchmod\s+(-R\s+)?777\s+/`),
32
+ regexp.MustCompile(`\bfind\s+/\s+-delete`),
33
+ regexp.MustCompile(`Remove-Item.*-Recurse.*-Force`),
34
+ regexp.MustCompile(`--no-preserve-root`),
35
+ regexp.MustCompile(`\bsudo\s+mkfs\b`),
36
+ }
37
+
38
+ func main() {
39
+ data, err := io.ReadAll(os.Stdin)
40
+ if err != nil {
41
+ os.Exit(0) // Don't block on read error
42
+ }
43
+
44
+ var input HookInput
45
+ if err := json.Unmarshal(data, &input); err != nil {
46
+ os.Exit(0) // Don't block on parse error
47
+ }
48
+
49
+ cmd := input.ToolInput.Command
50
+ if cmd == "" {
51
+ os.Exit(0)
52
+ }
53
+
54
+ // Skip if command is in an echo/printf context
55
+ lower := strings.ToLower(cmd)
56
+ if strings.HasPrefix(strings.TrimSpace(lower), "echo ") ||
57
+ strings.HasPrefix(strings.TrimSpace(lower), "printf ") {
58
+ os.Exit(0)
59
+ }
60
+
61
+ for _, pattern := range dangerousPatterns {
62
+ if pattern.MatchString(cmd) {
63
+ fmt.Fprintf(os.Stderr, "BLOCKED: Dangerous command detected\nCommand: %s\n", cmd)
64
+ os.Exit(2)
65
+ }
66
+ }
67
+
68
+ os.Exit(0)
69
+ }
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # lockfile-guard.sh — Warn when lockfiles are modified unexpectedly
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude sometimes runs npm install or pip install that modifies
7
+ # lockfiles (package-lock.json, yarn.lock, Cargo.lock, etc.)
8
+ # without the user intending a dependency change. This hook warns
9
+ # when a lockfile appears in staged changes.
10
+ #
11
+ # TRIGGER: PreToolUse MATCHER: "Bash"
12
+ # ================================================================
13
+
14
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
15
+ [ -z "$COMMAND" ] && exit 0
16
+
17
+ # Only check git commit
18
+ echo "$COMMAND" | grep -qE '^\s*git\s+(commit|add)' || exit 0
19
+
20
+ # Check for lockfile changes
21
+ LOCKFILES=$(git diff --cached --name-only 2>/dev/null | grep -E '(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|Gemfile\.lock|poetry\.lock|composer\.lock|go\.sum)' 2>/dev/null)
22
+
23
+ if [ -n "$LOCKFILES" ]; then
24
+ COUNT=$(echo "$LOCKFILES" | wc -l)
25
+ echo "WARNING: $COUNT lockfile(s) modified:" >&2
26
+ echo "$LOCKFILES" | sed 's/^/ /' >&2
27
+ echo "Verify the dependency change was intentional." >&2
28
+ fi
29
+
30
+ exit 0
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * destructive-guard.ts — Claude Code PreToolUse hook in TypeScript
4
+ *
5
+ * Blocks rm -rf /, git reset --hard, git clean -fd, and similar
6
+ * destructive commands. Exit code 2 = block, 0 = allow.
7
+ *
8
+ * Run with: npx tsx destructive-guard.ts
9
+ * Or compile: npx tsc destructive-guard.ts && node destructive-guard.js
10
+ */
11
+
12
+ interface HookInput {
13
+ tool_input: {
14
+ command?: string;
15
+ };
16
+ }
17
+
18
+ const DANGEROUS_PATTERNS: RegExp[] = [
19
+ /\brm\s+.*-rf\s+(\/|~\/?\s*$|\.\.\/)/,
20
+ /\bgit\s+reset\s+--hard/,
21
+ /\bgit\s+clean\s+-[a-zA-Z]*f/,
22
+ /\bgit\s+checkout\s+--force/,
23
+ /\bchmod\s+(-R\s+)?777\s+\//,
24
+ /\bfind\s+\/\s+-delete/,
25
+ /Remove-Item.*-Recurse.*-Force/,
26
+ /--no-preserve-root/,
27
+ /\bsudo\s+mkfs\b/,
28
+ ];
29
+
30
+ async function main(): Promise<void> {
31
+ let data = '';
32
+ for await (const chunk of process.stdin) {
33
+ data += chunk;
34
+ }
35
+
36
+ let input: HookInput;
37
+ try {
38
+ input = JSON.parse(data);
39
+ } catch {
40
+ process.exit(0); // Don't block on parse error
41
+ }
42
+
43
+ const cmd = input.tool_input?.command;
44
+ if (!cmd) {
45
+ process.exit(0);
46
+ }
47
+
48
+ // Skip echo/printf context
49
+ const trimmed = cmd.trimStart().toLowerCase();
50
+ if (trimmed.startsWith('echo ') || trimmed.startsWith('printf ')) {
51
+ process.exit(0);
52
+ }
53
+
54
+ for (const pattern of DANGEROUS_PATTERNS) {
55
+ if (pattern.test(cmd)) {
56
+ process.stderr.write(`BLOCKED: Dangerous command detected\nCommand: ${cmd}\n`);
57
+ process.exit(2);
58
+ }
59
+ }
60
+
61
+ process.exit(0);
62
+ }
63
+
64
+ main();
package/index.mjs CHANGED
@@ -93,6 +93,8 @@ const QUICKFIX = process.argv.includes('--quickfix');
93
93
  const SHIELD = process.argv.includes('--shield');
94
94
  const ANALYZE = process.argv.includes('--analyze');
95
95
  const TEAM = process.argv.includes('--team');
96
+ const MIGRATE_FROM_IDX = process.argv.findIndex(a => a === '--migrate-from');
97
+ const MIGRATE_FROM = MIGRATE_FROM_IDX !== -1 ? process.argv[MIGRATE_FROM_IDX + 1] : null;
96
98
  const PROFILE_IDX = process.argv.findIndex(a => a === '--profile');
97
99
  const PROFILE = PROFILE_IDX !== -1 ? process.argv[PROFILE_IDX + 1] : null;
98
100
  const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
@@ -130,6 +132,7 @@ if (HELP) {
130
132
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
131
133
  npx cc-safe-setup --watch Live dashboard of blocked commands
132
134
  npx cc-safe-setup --create "<desc>" Generate a custom hook from description
135
+ npx cc-safe-setup --migrate-from <tool> Migrate from safety-net/hooks-mastery/etc.
133
136
  npx cc-safe-setup --team Set up project-level hooks (commit to repo for team)
134
137
  npx cc-safe-setup --profile <level> Switch safety profile (strict/standard/minimal)
135
138
  npx cc-safe-setup --analyze Analyze what Claude did in your last session
@@ -840,6 +843,141 @@ async function fullSetup() {
840
843
  console.log();
841
844
  }
842
845
 
846
+ async function migrateFrom(tool) {
847
+ console.log();
848
+ console.log(c.bold + ' cc-safe-setup --migrate-from ' + (tool || '?') + c.reset);
849
+ console.log();
850
+
851
+ const KNOWN_TOOLS = {
852
+ 'safety-net': {
853
+ name: 'Claude Code Safety Net',
854
+ npm: '@anthropic-ai/claude-code-safety-net',
855
+ detect: () => {
856
+ if (!existsSync(SETTINGS_PATH)) return false;
857
+ const s = readFileSync(SETTINGS_PATH, 'utf-8');
858
+ return s.includes('safety-net') || s.includes('claude-code-safety-net');
859
+ },
860
+ mapping: {
861
+ 'destructive-commands': 'destructive-guard',
862
+ 'secret-files': 'secret-guard',
863
+ 'branch-protection': 'branch-guard',
864
+ 'git-operations': 'branch-guard',
865
+ },
866
+ desc: 'TypeScript hooks with configurable severity levels'
867
+ },
868
+ 'hooks-mastery': {
869
+ name: 'Claude Code Hooks Mastery',
870
+ npm: null,
871
+ detect: () => {
872
+ if (!existsSync(SETTINGS_PATH)) return false;
873
+ const s = readFileSync(SETTINGS_PATH, 'utf-8');
874
+ return s.includes('hooks_mastery') || s.includes('hooks-mastery');
875
+ },
876
+ mapping: {
877
+ 'safety': 'destructive-guard',
878
+ 'git-safety': 'branch-guard',
879
+ },
880
+ desc: 'Python hooks for all events + LLM integration'
881
+ },
882
+ 'manual': {
883
+ name: 'Custom/Manual Hooks',
884
+ npm: null,
885
+ detect: () => {
886
+ if (!existsSync(SETTINGS_PATH)) return false;
887
+ const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
888
+ return Object.keys(s.hooks || {}).length > 0;
889
+ },
890
+ mapping: {},
891
+ desc: 'Hand-written hooks in settings.json'
892
+ }
893
+ };
894
+
895
+ if (!tool) {
896
+ console.log(c.bold + ' Supported migration sources:' + c.reset);
897
+ console.log();
898
+ for (const [id, info] of Object.entries(KNOWN_TOOLS)) {
899
+ const detected = info.detect();
900
+ const icon = detected ? c.green + '●' + c.reset : c.dim + '○' + c.reset;
901
+ console.log(` ${icon} ${c.bold}${id}${c.reset} — ${info.desc}`);
902
+ if (detected) console.log(` ${c.green}Detected in your settings${c.reset}`);
903
+ console.log(` ${c.dim}npx cc-safe-setup --migrate-from ${id}${c.reset}`);
904
+ console.log();
905
+ }
906
+ return;
907
+ }
908
+
909
+ const source = KNOWN_TOOLS[tool];
910
+ if (!source) {
911
+ console.log(c.red + ` Unknown tool: ${tool}` + c.reset);
912
+ console.log(c.dim + ' Supported: ' + Object.keys(KNOWN_TOOLS).join(', ') + c.reset);
913
+ return;
914
+ }
915
+
916
+ console.log(c.dim + ` Migrating from: ${source.name}` + c.reset);
917
+ console.log();
918
+
919
+ // Read current settings
920
+ let settings = {};
921
+ if (existsSync(SETTINGS_PATH)) {
922
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
923
+ }
924
+
925
+ // Analyze existing hooks
926
+ let existingHooks = [];
927
+ for (const [trigger, groups] of Object.entries(settings.hooks || {})) {
928
+ for (const group of groups) {
929
+ for (const hook of (group.hooks || [])) {
930
+ existingHooks.push({ trigger, matcher: group.matcher, command: hook.command || '' });
931
+ }
932
+ }
933
+ }
934
+
935
+ console.log(` Found ${existingHooks.length} existing hook(s) in settings.json`);
936
+ console.log();
937
+
938
+ // Identify what cc-safe-setup equivalents exist
939
+ const replacements = [];
940
+ for (const h of existingHooks) {
941
+ const cmd = h.command.toLowerCase();
942
+ let replacement = null;
943
+
944
+ // Try source-specific mapping
945
+ for (const [pattern, ccHook] of Object.entries(source.mapping)) {
946
+ if (cmd.includes(pattern)) {
947
+ replacement = ccHook;
948
+ break;
949
+ }
950
+ }
951
+
952
+ // Generic detection
953
+ if (!replacement) {
954
+ if (cmd.includes('rm') || cmd.includes('destruct')) replacement = 'destructive-guard';
955
+ else if (cmd.includes('branch') || cmd.includes('push')) replacement = 'branch-guard';
956
+ else if (cmd.includes('secret') || cmd.includes('env')) replacement = 'secret-guard';
957
+ else if (cmd.includes('syntax') || cmd.includes('lint')) replacement = 'syntax-check';
958
+ else if (cmd.includes('context') || cmd.includes('monitor')) replacement = 'context-monitor';
959
+ }
960
+
961
+ if (replacement) {
962
+ replacements.push({ old: h.command, new: replacement });
963
+ console.log(` ${c.yellow}→${c.reset} ${h.command.substring(0, 50)}`);
964
+ console.log(` ${c.green}→${c.reset} cc-safe-setup: ${replacement}`);
965
+ } else {
966
+ console.log(` ${c.dim}?${c.reset} ${h.command.substring(0, 50)} (no equivalent, keeping)`);
967
+ }
968
+ }
969
+
970
+ console.log();
971
+ console.log(c.bold + ' Migration plan:' + c.reset);
972
+ console.log(` ${c.green}${replacements.length}${c.reset} hooks can be replaced with cc-safe-setup equivalents`);
973
+ console.log(` ${c.dim}${existingHooks.length - replacements.length}${c.reset} hooks will be kept as-is`);
974
+ console.log();
975
+ console.log(c.dim + ' To apply: npx cc-safe-setup --shield' + c.reset);
976
+ console.log(c.dim + ' This installs cc-safe-setup hooks alongside existing ones.' + c.reset);
977
+ console.log(c.dim + ' Remove old hooks manually after verifying the new ones work.' + c.reset);
978
+ console.log();
979
+ }
980
+
843
981
  async function team() {
844
982
  console.log();
845
983
  console.log(c.bold + ' cc-safe-setup --team' + c.reset);
@@ -3381,6 +3519,7 @@ async function main() {
3381
3519
  if (FULL) return fullSetup();
3382
3520
  if (DOCTOR) return doctor();
3383
3521
  if (WATCH) return watch();
3522
+ if (MIGRATE_FROM_IDX !== -1) return migrateFrom(MIGRATE_FROM);
3384
3523
  if (TEAM) return team();
3385
3524
  if (PROFILE_IDX !== -1) return profile(PROFILE);
3386
3525
  if (ANALYZE) return analyze();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "8.4.0",
3
+ "version": "8.6.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": {