cc-safe-setup 2.2.0 → 2.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 +23 -0
- package/action.yml +34 -0
- package/examples/session-checkpoint.sh +54 -0
- package/index.mjs +194 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,6 +138,29 @@ npx cc-safe-setup --audit
|
|
|
138
138
|
|
|
139
139
|
Analyzes 9 safety dimensions and gives you a score (0-100) with one-command fixes for each risk.
|
|
140
140
|
|
|
141
|
+
### CI Integration (GitHub Action)
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
# .github/workflows/safety.yml
|
|
145
|
+
- uses: yurukusa/cc-safe-setup@main
|
|
146
|
+
with:
|
|
147
|
+
threshold: 70 # CI fails if score drops below this
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Project Scanner
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx cc-safe-setup --scan # detect tech stack, recommend hooks
|
|
154
|
+
npx cc-safe-setup --scan --apply # auto-create CLAUDE.md with project rules
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Self-Learning Safety
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npx cc-safe-setup --learn # analyze your block history for patterns
|
|
161
|
+
npx cc-safe-setup --learn --apply # auto-generate custom hooks from patterns
|
|
162
|
+
```
|
|
163
|
+
|
|
141
164
|
## Examples
|
|
142
165
|
|
|
143
166
|
Need custom hooks beyond the 8 built-in ones? Install any example with one command:
|
package/action.yml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: 'Claude Code Safety Audit'
|
|
2
|
+
description: 'Check your Claude Code safety setup and fail CI if score is below threshold'
|
|
3
|
+
branding:
|
|
4
|
+
icon: 'shield'
|
|
5
|
+
color: 'green'
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
threshold:
|
|
9
|
+
description: 'Minimum safety score (0-100). CI fails if below this.'
|
|
10
|
+
required: false
|
|
11
|
+
default: '70'
|
|
12
|
+
|
|
13
|
+
runs:
|
|
14
|
+
using: 'composite'
|
|
15
|
+
steps:
|
|
16
|
+
- name: Run safety audit
|
|
17
|
+
shell: bash
|
|
18
|
+
run: |
|
|
19
|
+
echo "::group::Claude Code Safety Audit"
|
|
20
|
+
npx cc-safe-setup@latest --audit 2>&1 | tee /tmp/audit-output.txt
|
|
21
|
+
echo "::endgroup::"
|
|
22
|
+
|
|
23
|
+
# Extract score
|
|
24
|
+
SCORE=$(grep -oP 'Safety Score: \K\d+' /tmp/audit-output.txt || echo "0")
|
|
25
|
+
THRESHOLD="${{ inputs.threshold }}"
|
|
26
|
+
|
|
27
|
+
echo "Safety Score: $SCORE / 100 (threshold: $THRESHOLD)"
|
|
28
|
+
|
|
29
|
+
if [ "$SCORE" -lt "$THRESHOLD" ]; then
|
|
30
|
+
echo "::error::Safety score $SCORE is below threshold $THRESHOLD. Run 'npx cc-safe-setup --audit --fix' to improve."
|
|
31
|
+
exit 1
|
|
32
|
+
else
|
|
33
|
+
echo "::notice::Safety score $SCORE meets threshold $THRESHOLD ✓"
|
|
34
|
+
fi
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-checkpoint.sh — Auto-save session state on every stop
|
|
3
|
+
#
|
|
4
|
+
# Solves: Session crashes/disconnects causing expensive re-analysis
|
|
5
|
+
# of the entire codebase on next start (#37866)
|
|
6
|
+
#
|
|
7
|
+
# Saves: git state, recent commits, modified files, working directory.
|
|
8
|
+
# Next session reads the checkpoint instead of re-analyzing everything.
|
|
9
|
+
#
|
|
10
|
+
# Usage: Add to settings.json as a Stop hook
|
|
11
|
+
#
|
|
12
|
+
# {
|
|
13
|
+
# "hooks": {
|
|
14
|
+
# "Stop": [{
|
|
15
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-checkpoint.sh" }]
|
|
16
|
+
# }]
|
|
17
|
+
# }
|
|
18
|
+
# }
|
|
19
|
+
#
|
|
20
|
+
# Recovery: Add to CLAUDE.md:
|
|
21
|
+
# "If ~/.claude/checkpoints/ has a file for this project, read it first."
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
REASON=$(echo "$INPUT" | jq -r '.stop_reason // empty' 2>/dev/null)
|
|
25
|
+
|
|
26
|
+
CHECKPOINT_DIR="$HOME/.claude/checkpoints"
|
|
27
|
+
mkdir -p "$CHECKPOINT_DIR"
|
|
28
|
+
|
|
29
|
+
PROJECT_NAME=$(basename "$(pwd)")
|
|
30
|
+
CHECKPOINT="$CHECKPOINT_DIR/${PROJECT_NAME}-latest.md"
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
echo "# Session Checkpoint"
|
|
34
|
+
echo "Saved: $(date -Iseconds)"
|
|
35
|
+
echo "Directory: $(pwd)"
|
|
36
|
+
echo "Stop reason: ${REASON:-unknown}"
|
|
37
|
+
echo ""
|
|
38
|
+
echo "## Recent Commits"
|
|
39
|
+
git log --oneline -10 2>/dev/null || echo "(not a git repo)"
|
|
40
|
+
echo ""
|
|
41
|
+
echo "## Uncommitted Changes"
|
|
42
|
+
git diff --stat 2>/dev/null || echo "(none)"
|
|
43
|
+
echo ""
|
|
44
|
+
echo "## Staged Files"
|
|
45
|
+
git diff --cached --name-only 2>/dev/null || echo "(none)"
|
|
46
|
+
echo ""
|
|
47
|
+
echo "## Current Branch"
|
|
48
|
+
git branch --show-current 2>/dev/null || echo "(unknown)"
|
|
49
|
+
} > "$CHECKPOINT" 2>/dev/null
|
|
50
|
+
|
|
51
|
+
# Cleanup: keep only last 10 checkpoints
|
|
52
|
+
ls -t "$CHECKPOINT_DIR"/*.md 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null
|
|
53
|
+
|
|
54
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -70,6 +70,8 @@ const INSTALL_EXAMPLE_IDX = process.argv.findIndex(a => a === '--install-example
|
|
|
70
70
|
const INSTALL_EXAMPLE = INSTALL_EXAMPLE_IDX !== -1 ? process.argv[INSTALL_EXAMPLE_IDX + 1] : null;
|
|
71
71
|
const AUDIT = process.argv.includes('--audit');
|
|
72
72
|
const LEARN = process.argv.includes('--learn');
|
|
73
|
+
const SCAN = process.argv.includes('--scan');
|
|
74
|
+
const FULL = process.argv.includes('--full');
|
|
73
75
|
|
|
74
76
|
if (HELP) {
|
|
75
77
|
console.log(`
|
|
@@ -567,6 +569,18 @@ async function audit() {
|
|
|
567
569
|
console.log();
|
|
568
570
|
console.log(c.dim + ' Run with --fix to auto-apply: npx cc-safe-setup --audit --fix' + c.reset);
|
|
569
571
|
}
|
|
572
|
+
|
|
573
|
+
// Badge output
|
|
574
|
+
if (process.argv.includes('--badge')) {
|
|
575
|
+
const color = score >= 80 ? 'brightgreen' : score >= 50 ? 'yellow' : 'red';
|
|
576
|
+
const badge = ``;
|
|
577
|
+
console.log();
|
|
578
|
+
console.log(c.bold + ' README Badge:' + c.reset);
|
|
579
|
+
console.log(' ' + badge);
|
|
580
|
+
console.log();
|
|
581
|
+
console.log(c.dim + ' Paste this into your README.md' + c.reset);
|
|
582
|
+
}
|
|
583
|
+
|
|
570
584
|
console.log();
|
|
571
585
|
}
|
|
572
586
|
|
|
@@ -682,6 +696,184 @@ exit 0`;
|
|
|
682
696
|
console.log();
|
|
683
697
|
}
|
|
684
698
|
|
|
699
|
+
async function fullSetup() {
|
|
700
|
+
console.log();
|
|
701
|
+
console.log(c.bold + c.green + ' cc-safe-setup --full' + c.reset);
|
|
702
|
+
console.log(c.dim + ' Complete safety setup in one command' + c.reset);
|
|
703
|
+
console.log();
|
|
704
|
+
|
|
705
|
+
const { execSync } = await import('child_process');
|
|
706
|
+
const self = process.argv[1];
|
|
707
|
+
|
|
708
|
+
// Step 1: Install 8 built-in hooks
|
|
709
|
+
console.log(c.bold + ' Step 1: Installing 8 built-in safety hooks...' + c.reset);
|
|
710
|
+
try {
|
|
711
|
+
execSync('node ' + self + ' --yes', { stdio: 'inherit' });
|
|
712
|
+
} catch(e) {
|
|
713
|
+
// --yes doesn't exist, run normal install
|
|
714
|
+
execSync('node ' + self, { stdio: 'inherit', input: 'y\n' });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Step 2: Scan project and create CLAUDE.md
|
|
718
|
+
console.log();
|
|
719
|
+
console.log(c.bold + ' Step 2: Scanning project and creating CLAUDE.md...' + c.reset);
|
|
720
|
+
scan();
|
|
721
|
+
|
|
722
|
+
// Step 3: Audit and show results
|
|
723
|
+
console.log();
|
|
724
|
+
console.log(c.bold + ' Step 3: Running safety audit...' + c.reset);
|
|
725
|
+
// Inject --badge into argv temporarily
|
|
726
|
+
process.argv.push('--badge');
|
|
727
|
+
await audit();
|
|
728
|
+
|
|
729
|
+
console.log(c.bold + c.green + ' ✓ Full setup complete!' + c.reset);
|
|
730
|
+
console.log(c.dim + ' Your project now has:' + c.reset);
|
|
731
|
+
console.log(c.dim + ' • 8 built-in safety hooks' + c.reset);
|
|
732
|
+
console.log(c.dim + ' • Project-specific hook recommendations' + c.reset);
|
|
733
|
+
console.log(c.dim + ' • Safety score and README badge' + c.reset);
|
|
734
|
+
console.log();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function scan() {
|
|
738
|
+
console.log();
|
|
739
|
+
console.log(c.bold + ' cc-safe-setup --scan' + c.reset);
|
|
740
|
+
console.log(c.dim + ' Scanning project to generate safety config...' + c.reset);
|
|
741
|
+
console.log();
|
|
742
|
+
|
|
743
|
+
const cwd = process.cwd();
|
|
744
|
+
const detected = { languages: [], frameworks: [], hasDb: false, hasDocker: false, isMonorepo: false };
|
|
745
|
+
const hooks = ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check'];
|
|
746
|
+
const claudeMdRules = ['# Project Safety Rules\n', '## Generated by cc-safe-setup --scan\n'];
|
|
747
|
+
|
|
748
|
+
// Detect languages & frameworks
|
|
749
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
750
|
+
detected.languages.push('JavaScript/TypeScript');
|
|
751
|
+
try {
|
|
752
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
753
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
754
|
+
if (allDeps.next) detected.frameworks.push('Next.js');
|
|
755
|
+
if (allDeps.react) detected.frameworks.push('React');
|
|
756
|
+
if (allDeps.express) detected.frameworks.push('Express');
|
|
757
|
+
if (allDeps.prisma || allDeps['@prisma/client']) { detected.frameworks.push('Prisma'); detected.hasDb = true; }
|
|
758
|
+
if (allDeps.sequelize) { detected.frameworks.push('Sequelize'); detected.hasDb = true; }
|
|
759
|
+
if (allDeps.typeorm) { detected.frameworks.push('TypeORM'); detected.hasDb = true; }
|
|
760
|
+
} catch(e) {}
|
|
761
|
+
}
|
|
762
|
+
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) {
|
|
763
|
+
detected.languages.push('Python');
|
|
764
|
+
if (existsSync(join(cwd, 'manage.py'))) { detected.frameworks.push('Django'); detected.hasDb = true; }
|
|
765
|
+
if (existsSync(join(cwd, 'app.py')) || existsSync(join(cwd, 'wsgi.py'))) detected.frameworks.push('Flask');
|
|
766
|
+
}
|
|
767
|
+
if (existsSync(join(cwd, 'Gemfile'))) {
|
|
768
|
+
detected.languages.push('Ruby');
|
|
769
|
+
detected.frameworks.push('Rails');
|
|
770
|
+
detected.hasDb = true;
|
|
771
|
+
}
|
|
772
|
+
if (existsSync(join(cwd, 'composer.json'))) {
|
|
773
|
+
detected.languages.push('PHP');
|
|
774
|
+
try {
|
|
775
|
+
const composer = JSON.parse(readFileSync(join(cwd, 'composer.json'), 'utf-8'));
|
|
776
|
+
if (composer.require?.['laravel/framework']) { detected.frameworks.push('Laravel'); detected.hasDb = true; }
|
|
777
|
+
if (composer.require?.['symfony/framework-bundle']) { detected.frameworks.push('Symfony'); detected.hasDb = true; }
|
|
778
|
+
} catch(e) {}
|
|
779
|
+
}
|
|
780
|
+
if (existsSync(join(cwd, 'go.mod'))) detected.languages.push('Go');
|
|
781
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) detected.languages.push('Rust');
|
|
782
|
+
|
|
783
|
+
// Docker
|
|
784
|
+
if (existsSync(join(cwd, 'Dockerfile')) || existsSync(join(cwd, 'docker-compose.yml')) || existsSync(join(cwd, 'compose.yaml'))) {
|
|
785
|
+
detected.hasDocker = true;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Monorepo
|
|
789
|
+
if (existsSync(join(cwd, 'pnpm-workspace.yaml')) || existsSync(join(cwd, 'lerna.json')) || existsSync(join(cwd, 'nx.json'))) {
|
|
790
|
+
detected.isMonorepo = true;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// .env files
|
|
794
|
+
const hasEnv = existsSync(join(cwd, '.env')) || existsSync(join(cwd, '.env.local'));
|
|
795
|
+
|
|
796
|
+
// Display detection results
|
|
797
|
+
console.log(c.bold + ' Detected:' + c.reset);
|
|
798
|
+
if (detected.languages.length) console.log(' ' + c.green + '✓' + c.reset + ' Languages: ' + detected.languages.join(', '));
|
|
799
|
+
if (detected.frameworks.length) console.log(' ' + c.green + '✓' + c.reset + ' Frameworks: ' + detected.frameworks.join(', '));
|
|
800
|
+
if (detected.hasDb) console.log(' ' + c.green + '✓' + c.reset + ' Database detected');
|
|
801
|
+
if (detected.hasDocker) console.log(' ' + c.green + '✓' + c.reset + ' Docker detected');
|
|
802
|
+
if (detected.isMonorepo) console.log(' ' + c.green + '✓' + c.reset + ' Monorepo detected');
|
|
803
|
+
if (hasEnv) console.log(' ' + c.yellow + '⚠' + c.reset + ' .env file found (secret leak risk)');
|
|
804
|
+
console.log();
|
|
805
|
+
|
|
806
|
+
// Generate recommendations
|
|
807
|
+
const examples = [];
|
|
808
|
+
|
|
809
|
+
if (detected.hasDb) {
|
|
810
|
+
examples.push('block-database-wipe');
|
|
811
|
+
claudeMdRules.push('- Never run destructive database commands (migrate:fresh, DROP DATABASE, prisma migrate reset)');
|
|
812
|
+
claudeMdRules.push('- Always backup database before schema changes');
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (detected.hasDocker) {
|
|
816
|
+
examples.push('auto-approve-docker');
|
|
817
|
+
claudeMdRules.push('- Docker commands are auto-approved for build/compose/ps/logs');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (hasEnv) {
|
|
821
|
+
claudeMdRules.push('- Never commit .env files. Use .env.example for templates');
|
|
822
|
+
claudeMdRules.push('- Never hardcode API keys or secrets in source files');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (detected.languages.includes('Python')) {
|
|
826
|
+
examples.push('auto-approve-python');
|
|
827
|
+
claudeMdRules.push('- Run pytest after every code change');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (detected.languages.includes('JavaScript/TypeScript')) {
|
|
831
|
+
examples.push('auto-approve-build');
|
|
832
|
+
claudeMdRules.push('- Run npm test after every code change');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Always recommend
|
|
836
|
+
examples.push('scope-guard');
|
|
837
|
+
examples.push('protect-dotfiles');
|
|
838
|
+
claudeMdRules.push('- Always run tests before committing');
|
|
839
|
+
claudeMdRules.push('- Never force-push to main/master');
|
|
840
|
+
claudeMdRules.push('- Create a backup branch before large refactors');
|
|
841
|
+
|
|
842
|
+
// Display recommendations
|
|
843
|
+
console.log(c.bold + ' Recommended hooks:' + c.reset);
|
|
844
|
+
console.log(' ' + c.dim + 'npx cc-safe-setup' + c.reset + ' (8 built-in hooks)');
|
|
845
|
+
for (const ex of examples) {
|
|
846
|
+
console.log(' ' + c.dim + 'npx cc-safe-setup --install-example ' + ex + c.reset);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Generate CLAUDE.md
|
|
850
|
+
console.log();
|
|
851
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
852
|
+
if (existsSync(claudeMdPath)) {
|
|
853
|
+
console.log(c.yellow + ' CLAUDE.md already exists. Suggested rules to add:' + c.reset);
|
|
854
|
+
console.log();
|
|
855
|
+
for (const rule of claudeMdRules.slice(2)) {
|
|
856
|
+
console.log(' ' + c.dim + rule + c.reset);
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
const content = claudeMdRules.join('\n') + '\n';
|
|
860
|
+
if (process.argv.includes('--apply')) {
|
|
861
|
+
writeFileSync(claudeMdPath, content);
|
|
862
|
+
console.log(c.green + ' ✓ CLAUDE.md created with ' + (claudeMdRules.length - 2) + ' project-specific rules.' + c.reset);
|
|
863
|
+
} else {
|
|
864
|
+
console.log(c.bold + ' Suggested CLAUDE.md:' + c.reset);
|
|
865
|
+
console.log();
|
|
866
|
+
for (const rule of claudeMdRules) {
|
|
867
|
+
console.log(' ' + c.dim + rule + c.reset);
|
|
868
|
+
}
|
|
869
|
+
console.log();
|
|
870
|
+
console.log(c.dim + ' Run with --apply to create: npx cc-safe-setup --scan --apply' + c.reset);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
console.log();
|
|
875
|
+
}
|
|
876
|
+
|
|
685
877
|
async function main() {
|
|
686
878
|
if (UNINSTALL) return uninstall();
|
|
687
879
|
if (VERIFY) return verify();
|
|
@@ -690,6 +882,8 @@ async function main() {
|
|
|
690
882
|
if (INSTALL_EXAMPLE) return installExample(INSTALL_EXAMPLE);
|
|
691
883
|
if (AUDIT) return audit();
|
|
692
884
|
if (LEARN) return learn();
|
|
885
|
+
if (SCAN) return scan();
|
|
886
|
+
if (FULL) return fullSetup();
|
|
693
887
|
|
|
694
888
|
console.log();
|
|
695
889
|
console.log(c.bold + ' cc-safe-setup' + c.reset);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 25 installable examples. Destructive blocker, branch guard, database wipe protection, dotfile guard, and more.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|