cc-safe-setup 9.2.0 → 9.4.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 + 104 examples = **112 hooks**. 34 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) · [Examples](https://yurukusa.github.io/cc-safe-setup/by-example.html) · [Migration](https://yurukusa.github.io/cc-safe-setup/migration-guide.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
9
+ 8 built-in + 104 examples = **112 hooks**. 35 CLI commands. 457 tests. 5 languages. [**Hub**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [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) · [Examples](https://yurukusa.github.io/cc-safe-setup/by-example.html) · [Matrix](https://yurukusa.github.io/cc-safe-setup/matrix.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
10
10
 
11
11
  ```bash
12
12
  npx cc-safe-setup
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ # ci-skip-guard.sh — Warn when commit message skips CI
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
+ if echo "$COMMAND" | grep -qiE '\[skip ci\]|\[ci skip\]|\[no ci\]|--no-verify'; then
8
+ echo "WARNING: Commit will skip CI checks." >&2
9
+ echo "Remove [skip ci] or --no-verify unless you have a good reason." >&2
10
+ fi
11
+ exit 0
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ # debug-leftover-guard.sh — Detect debug code in commits
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
+ LEFTOVERS=$(git diff --cached 2>/dev/null | grep -cE '^\+.*(debugger|console\.debug|pdb\.set_trace|binding\.pry|pp\s|var_dump|print_r)' || echo 0)
8
+ if [ "$LEFTOVERS" -gt 0 ]; then
9
+ echo "WARNING: $LEFTOVERS debug statement(s) in staged changes." >&2
10
+ echo "Remove debugger/pdb/binding.pry before committing." >&2
11
+ fi
12
+ exit 0
@@ -0,0 +1,72 @@
1
+ // destructive_guard.rs — Claude Code PreToolUse hook in Rust
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: rustc destructive_guard.rs -o destructive-guard
7
+ // Usage: {"type": "command", "command": "/path/to/destructive-guard"}
8
+
9
+ use std::io::{self, Read};
10
+ use std::process;
11
+
12
+ fn main() {
13
+ let mut input = String::new();
14
+ if io::stdin().read_to_string(&mut input).is_err() {
15
+ process::exit(0);
16
+ }
17
+
18
+ // Simple JSON parsing without serde (zero dependencies)
19
+ let cmd = extract_command(&input);
20
+ if cmd.is_empty() {
21
+ process::exit(0);
22
+ }
23
+
24
+ // Skip echo/printf context
25
+ let trimmed = cmd.trim_start().to_lowercase();
26
+ if trimmed.starts_with("echo ") || trimmed.starts_with("printf ") {
27
+ process::exit(0);
28
+ }
29
+
30
+ let patterns: &[&str] = &[
31
+ "rm -rf /",
32
+ "rm -rf ~/",
33
+ "rm -rf ../",
34
+ "rm -rf .",
35
+ "git reset --hard",
36
+ "git clean -f",
37
+ "git checkout --force",
38
+ "chmod 777 /",
39
+ "find / -delete",
40
+ "--no-preserve-root",
41
+ "sudo mkfs",
42
+ ];
43
+
44
+ for pattern in patterns {
45
+ if cmd.to_lowercase().contains(&pattern.to_lowercase()) {
46
+ eprintln!("BLOCKED: Dangerous command detected");
47
+ eprintln!("Command: {}", cmd);
48
+ process::exit(2);
49
+ }
50
+ }
51
+
52
+ process::exit(0);
53
+ }
54
+
55
+ fn extract_command(json: &str) -> String {
56
+ // Extract .tool_input.command from JSON without a parser
57
+ if let Some(pos) = json.find("\"command\"") {
58
+ let rest = &json[pos + 9..];
59
+ if let Some(start) = rest.find('"') {
60
+ let value_start = start + 1;
61
+ let mut end = value_start;
62
+ let bytes = rest.as_bytes();
63
+ while end < bytes.len() {
64
+ if bytes[end] == b'"' && (end == 0 || bytes[end - 1] != b'\\') {
65
+ return rest[value_start..end].replace("\\\"", "\"").replace("\\\\", "\\");
66
+ }
67
+ end += 1;
68
+ }
69
+ }
70
+ }
71
+ String::new()
72
+ }
package/index.mjs CHANGED
@@ -96,6 +96,7 @@ const TEAM = process.argv.includes('--team');
96
96
  const MIGRATE_FROM_IDX = process.argv.findIndex(a => a === '--migrate-from');
97
97
  const MIGRATE_FROM = MIGRATE_FROM_IDX !== -1 ? process.argv[MIGRATE_FROM_IDX + 1] : null;
98
98
  const HEALTH = process.argv.includes('--health');
99
+ const FROM_CLAUDEMD = process.argv.includes('--from-claudemd');
99
100
  const PROFILE_IDX = process.argv.findIndex(a => a === '--profile');
100
101
  const PROFILE = PROFILE_IDX !== -1 ? process.argv[PROFILE_IDX + 1] : null;
101
102
  const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
@@ -133,6 +134,7 @@ if (HELP) {
133
134
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
134
135
  npx cc-safe-setup --watch Live dashboard of blocked commands
135
136
  npx cc-safe-setup --create "<desc>" Generate a custom hook from description
137
+ npx cc-safe-setup --from-claudemd Convert CLAUDE.md rules into hooks
136
138
  npx cc-safe-setup --health Hook health dashboard (size, permissions, age)
137
139
  npx cc-safe-setup --migrate-from <tool> Migrate from safety-net/hooks-mastery/etc.
138
140
  npx cc-safe-setup --team Set up project-level hooks (commit to repo for team)
@@ -845,6 +847,89 @@ async function fullSetup() {
845
847
  console.log();
846
848
  }
847
849
 
850
+ async function fromClaudeMd() {
851
+ console.log();
852
+ console.log(c.bold + ' cc-safe-setup --from-claudemd' + c.reset);
853
+ console.log(c.dim + ' Convert CLAUDE.md rules into enforceable hooks' + c.reset);
854
+ console.log();
855
+
856
+ // Find CLAUDE.md
857
+ const cwd = process.cwd();
858
+ let claudeMdPath = null;
859
+ for (const p of [join(cwd, 'CLAUDE.md'), join(cwd, '.claude', 'CLAUDE.md')]) {
860
+ if (existsSync(p)) { claudeMdPath = p; break; }
861
+ }
862
+
863
+ if (!claudeMdPath) {
864
+ console.log(c.yellow + ' No CLAUDE.md found in project.' + c.reset);
865
+ console.log(c.dim + ' Run: npx cc-safe-setup --shield (creates one)' + c.reset);
866
+ return;
867
+ }
868
+
869
+ const content = readFileSync(claudeMdPath, 'utf-8').toLowerCase();
870
+ console.log(c.dim + ' Reading: ' + claudeMdPath + c.reset);
871
+ console.log();
872
+
873
+ // Pattern matching: CLAUDE.md rules → hooks
874
+ const RULE_MAP = [
875
+ { patterns: ['do not push to main', 'don\'t push to main', 'no push main', 'never push to main'], hook: 'branch-guard', desc: '"Do not push to main" → branch-guard' },
876
+ { patterns: ['do not force', 'no force push', 'don\'t force push'], hook: 'branch-guard', desc: '"No force push" → branch-guard' },
877
+ { patterns: ['do not delete', 'don\'t delete', 'no rm -rf', 'never delete'], hook: 'destructive-guard', desc: '"Do not delete files" → destructive-guard' },
878
+ { patterns: ['do not commit .env', 'don\'t commit secret', 'no credentials', 'never commit .env'], hook: 'secret-guard', desc: '"No .env commits" → secret-guard' },
879
+ { patterns: ['run tests before', 'test before commit', 'tests must pass'], hook: 'verify-before-done', desc: '"Run tests first" → verify-before-done' },
880
+ { patterns: ['do not use sudo', 'no sudo', 'don\'t use sudo'], hook: 'no-sudo-guard', desc: '"No sudo" → no-sudo-guard' },
881
+ { patterns: ['stay in project', 'only this project', 'don\'t modify outside', 'do not edit files outside'], hook: 'scope-guard', desc: '"Stay in project" → scope-guard' },
882
+ { patterns: ['don\'t modify .bashrc', 'do not edit dotfile', 'protect home'], hook: 'protect-dotfiles', desc: '"Protect dotfiles" → protect-dotfiles' },
883
+ { patterns: ['do not deploy on friday', 'no friday deploy'], hook: 'no-deploy-friday', desc: '"No Friday deploy" → no-deploy-friday' },
884
+ { patterns: ['do not install global', 'no npm -g', 'don\'t install globally'], hook: 'no-install-global', desc: '"No global installs" → no-install-global' },
885
+ { patterns: ['descriptive commit', 'meaningful commit', 'good commit message'], hook: 'commit-quality-gate', desc: '"Good commit messages" → commit-quality-gate' },
886
+ { patterns: ['one logical change', 'small commit', 'focused commit', 'don\'t commit too many'], hook: 'commit-scope-guard', desc: '"Small commits" → commit-scope-guard' },
887
+ { patterns: ['feature branch', 'create branch', 'feat/ fix/ chore/'], hook: 'branch-naming-convention', desc: '"Feature branches" → branch-naming-convention' },
888
+ { patterns: ['do not drop database', 'no migrate:fresh', 'protect database'], hook: 'block-database-wipe', desc: '"Protect database" → block-database-wipe' },
889
+ { patterns: ['read before edit', 'understand before changing'], hook: 'read-before-edit', desc: '"Read first" → read-before-edit' },
890
+ { patterns: ['do not overwrite', 'use edit not write'], hook: 'overwrite-guard', desc: '"Use Edit, not Write" → overwrite-guard' },
891
+ ];
892
+
893
+ const matched = [];
894
+ for (const rule of RULE_MAP) {
895
+ if (rule.patterns.some(p => content.includes(p))) {
896
+ matched.push(rule);
897
+ }
898
+ }
899
+
900
+ if (matched.length === 0) {
901
+ console.log(c.yellow + ' No enforceable rules detected in CLAUDE.md.' + c.reset);
902
+ console.log(c.dim + ' CLAUDE.md may contain guidelines that can\'t be converted to hooks.' + c.reset);
903
+ console.log(c.dim + ' For custom hooks: npx cc-safe-setup --create "your rule"' + c.reset);
904
+ return;
905
+ }
906
+
907
+ console.log(c.bold + ` Found ${matched.length} rules that can be enforced with hooks:` + c.reset);
908
+ console.log();
909
+
910
+ for (const m of matched) {
911
+ const hookPath = join(HOOKS_DIR, `${m.hook}.sh`);
912
+ const installed = existsSync(hookPath);
913
+ const icon = installed ? c.green + '✓' + c.reset : c.yellow + '○' + c.reset;
914
+ const status = installed ? c.dim + '(installed)' + c.reset : '';
915
+ console.log(` ${icon} ${m.desc} ${status}`);
916
+ if (!installed) {
917
+ console.log(c.dim + ` npx cc-safe-setup --install-example ${m.hook}` + c.reset);
918
+ }
919
+ }
920
+
921
+ const notInstalled = matched.filter(m => !existsSync(join(HOOKS_DIR, `${m.hook}.sh`)));
922
+ if (notInstalled.length > 0) {
923
+ console.log();
924
+ console.log(c.bold + ` Install all ${notInstalled.length} missing hooks:` + c.reset);
925
+ console.log(c.dim + ' npx cc-safe-setup --shield' + c.reset);
926
+ } else {
927
+ console.log();
928
+ console.log(c.green + ' All detected rules are already enforced by hooks!' + c.reset);
929
+ }
930
+ console.log();
931
+ }
932
+
848
933
  async function health() {
849
934
  const { readdirSync, statSync } = await import('fs');
850
935
  console.log();
@@ -3638,6 +3723,7 @@ async function main() {
3638
3723
  if (FULL) return fullSetup();
3639
3724
  if (DOCTOR) return doctor();
3640
3725
  if (WATCH) return watch();
3726
+ if (FROM_CLAUDEMD) return fromClaudeMd();
3641
3727
  if (HEALTH) return health();
3642
3728
  if (MIGRATE_FROM_IDX !== -1) return migrateFrom(MIGRATE_FROM);
3643
3729
  if (TEAM) return team();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "9.2.0",
3
+ "version": "9.4.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": {