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 +1 -1
- package/examples/ci-skip-guard.sh +11 -0
- package/examples/debug-leftover-guard.sh +12 -0
- package/examples/rust/destructive_guard.rs +72 -0
- package/index.mjs +86 -0
- package/package.json +1 -1
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**.
|
|
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.
|
|
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": {
|