cc-safe-setup 8.2.0 → 8.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.mjs +229 -0
  3. 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 + 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)
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)
10
10
 
11
11
  ```bash
12
12
  npx cc-safe-setup
package/index.mjs CHANGED
@@ -92,6 +92,9 @@ const REPORT = process.argv.includes('--report');
92
92
  const QUICKFIX = process.argv.includes('--quickfix');
93
93
  const SHIELD = process.argv.includes('--shield');
94
94
  const ANALYZE = process.argv.includes('--analyze');
95
+ const TEAM = process.argv.includes('--team');
96
+ const PROFILE_IDX = process.argv.findIndex(a => a === '--profile');
97
+ const PROFILE = PROFILE_IDX !== -1 ? process.argv[PROFILE_IDX + 1] : null;
95
98
  const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
96
99
  const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
97
100
  const CREATE_IDX = process.argv.findIndex(a => a === '--create');
@@ -127,6 +130,8 @@ if (HELP) {
127
130
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
128
131
  npx cc-safe-setup --watch Live dashboard of blocked commands
129
132
  npx cc-safe-setup --create "<desc>" Generate a custom hook from description
133
+ npx cc-safe-setup --team Set up project-level hooks (commit to repo for team)
134
+ npx cc-safe-setup --profile <level> Switch safety profile (strict/standard/minimal)
130
135
  npx cc-safe-setup --analyze Analyze what Claude did in your last session
131
136
  npx cc-safe-setup --shield Maximum safety in one command (fix + scan + install + CLAUDE.md)
132
137
  npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
@@ -835,6 +840,228 @@ async function fullSetup() {
835
840
  console.log();
836
841
  }
837
842
 
843
+ async function team() {
844
+ console.log();
845
+ console.log(c.bold + ' cc-safe-setup --team' + c.reset);
846
+ console.log(c.dim + ' Set up project-level hooks (commit to repo for team sharing)' + c.reset);
847
+ console.log();
848
+
849
+ const cwd = process.cwd();
850
+ const projectHooksDir = join(cwd, '.claude', 'hooks');
851
+ const projectSettings = join(cwd, '.claude', 'settings.local.json');
852
+
853
+ // Create .claude/hooks/ in project
854
+ mkdirSync(projectHooksDir, { recursive: true });
855
+
856
+ // Copy core safety hooks to project
857
+ const coreHooks = ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check',
858
+ 'context-monitor', 'comment-strip', 'cd-git-allow', 'api-error-alert'];
859
+
860
+ let installed = 0;
861
+ for (const hookId of coreHooks) {
862
+ const destPath = join(projectHooksDir, `${hookId}.sh`);
863
+ if (existsSync(destPath)) {
864
+ console.log(c.dim + ' ✓' + c.reset + ` ${hookId}`);
865
+ continue;
866
+ }
867
+
868
+ if (SCRIPTS[hookId]) {
869
+ writeFileSync(destPath, SCRIPTS[hookId]);
870
+ chmodSync(destPath, 0o755);
871
+ installed++;
872
+ console.log(c.green + ' +' + c.reset + ` ${hookId}`);
873
+ }
874
+ }
875
+
876
+ // Detect stack and add relevant hooks
877
+ const extras = [];
878
+ if (existsSync(join(cwd, 'package.json'))) extras.push('auto-approve-build');
879
+ if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) extras.push('auto-approve-python');
880
+ if (existsSync(join(cwd, 'go.mod'))) extras.push('auto-approve-go');
881
+ if (existsSync(join(cwd, 'Cargo.toml'))) extras.push('auto-approve-cargo');
882
+ if (existsSync(join(cwd, 'Dockerfile'))) extras.push('auto-approve-docker');
883
+
884
+ for (const ex of extras) {
885
+ const destPath = join(projectHooksDir, `${ex}.sh`);
886
+ const srcPath = join(__dirname, 'examples', `${ex}.sh`);
887
+ if (existsSync(destPath)) continue;
888
+ if (existsSync(srcPath)) {
889
+ copyFileSync(srcPath, destPath);
890
+ chmodSync(destPath, 0o755);
891
+ installed++;
892
+ console.log(c.green + ' +' + c.reset + ` ${ex} (project-specific)`);
893
+ }
894
+ }
895
+
896
+ // Generate settings.local.json
897
+ const allHooks = [...coreHooks, ...extras].filter(h => existsSync(join(projectHooksDir, `${h}.sh`)));
898
+
899
+ const bashHooks = allHooks.map(h => ({
900
+ type: 'command',
901
+ command: `bash .claude/hooks/${h}.sh`
902
+ }));
903
+
904
+ const settings = {
905
+ hooks: {
906
+ PreToolUse: [
907
+ { matcher: 'Bash', hooks: bashHooks.filter((_, i) => {
908
+ const name = allHooks[i];
909
+ return !['syntax-check', 'context-monitor', 'api-error-alert'].includes(name);
910
+ })},
911
+ { matcher: 'Edit|Write', hooks: [
912
+ { type: 'command', command: 'bash .claude/hooks/syntax-check.sh' }
913
+ ]}
914
+ ],
915
+ PostToolUse: [
916
+ { matcher: '', hooks: [
917
+ { type: 'command', command: 'bash .claude/hooks/context-monitor.sh' }
918
+ ]}
919
+ ],
920
+ Stop: [
921
+ { matcher: '', hooks: [
922
+ { type: 'command', command: 'bash .claude/hooks/api-error-alert.sh' }
923
+ ]}
924
+ ]
925
+ }
926
+ };
927
+
928
+ writeFileSync(projectSettings, JSON.stringify(settings, null, 2));
929
+ console.log();
930
+ console.log(c.green + ' ✓' + c.reset + ' Created .claude/settings.local.json');
931
+
932
+ // Add .claude/hooks to .gitignore if not there
933
+ const gitignorePath = join(cwd, '.gitignore');
934
+ if (existsSync(gitignorePath)) {
935
+ const gi = readFileSync(gitignorePath, 'utf-8');
936
+ if (!gi.includes('.claude/')) {
937
+ // Don't add — hooks should be committed for team sharing
938
+ }
939
+ }
940
+
941
+ console.log();
942
+ console.log(c.bold + ' Next steps:' + c.reset);
943
+ console.log(c.dim + ' 1. git add .claude/' + c.reset);
944
+ console.log(c.dim + ' 2. git commit -m "chore: add Claude Code safety hooks"' + c.reset);
945
+ console.log(c.dim + ' 3. Team members get hooks automatically on git pull' + c.reset);
946
+ console.log();
947
+ console.log(c.dim + ` ${installed} hooks installed, ${allHooks.length} total configured.` + c.reset);
948
+ console.log(c.dim + ' Hooks use relative paths (.claude/hooks/) — portable across machines.' + c.reset);
949
+ console.log();
950
+ }
951
+
952
+ async function profile(level) {
953
+ const { readdirSync } = await import('fs');
954
+ console.log();
955
+
956
+ const PROFILES = {
957
+ strict: {
958
+ desc: 'Maximum safety — blocks everything dangerous, requires verification',
959
+ hooks: ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check',
960
+ 'context-monitor', 'comment-strip', 'cd-git-allow', 'api-error-alert',
961
+ 'scope-guard', 'no-sudo-guard', 'protect-claudemd', 'env-source-guard',
962
+ 'no-install-global', 'deploy-guard', 'protect-dotfiles', 'symlink-guard',
963
+ 'strict-allowlist', 'uncommitted-work-guard', 'test-deletion-guard',
964
+ 'overwrite-guard', 'error-memory-guard', 'hardcoded-secret-detector',
965
+ 'conflict-marker-guard', 'token-budget-guard', 'fact-check-gate',
966
+ 'block-database-wipe', 'no-eval', 'file-size-limit', 'large-read-guard',
967
+ 'loop-detector', 'verify-before-done', 'diff-size-guard', 'commit-scope-guard']
968
+ },
969
+ standard: {
970
+ desc: 'Balanced — blocks dangerous commands, auto-approves safe ones',
971
+ hooks: ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check',
972
+ 'context-monitor', 'comment-strip', 'cd-git-allow', 'api-error-alert',
973
+ 'scope-guard', 'no-sudo-guard', 'protect-claudemd',
974
+ 'auto-approve-build', 'auto-approve-python', 'auto-approve-docker',
975
+ 'loop-detector', 'deploy-guard', 'block-database-wipe',
976
+ 'compound-command-approver', 'session-handoff', 'cost-tracker']
977
+ },
978
+ minimal: {
979
+ desc: 'Essential only — just the 8 core safety hooks',
980
+ hooks: ['destructive-guard', 'branch-guard', 'secret-guard', 'syntax-check',
981
+ 'context-monitor', 'comment-strip', 'cd-git-allow', 'api-error-alert']
982
+ },
983
+ };
984
+
985
+ if (!level || !PROFILES[level]) {
986
+ console.log(c.bold + ' Safety Profiles' + c.reset);
987
+ console.log();
988
+ for (const [name, prof] of Object.entries(PROFILES)) {
989
+ console.log(` ${c.bold}${name}${c.reset} (${prof.hooks.length} hooks)`);
990
+ console.log(` ${c.dim}${prof.desc}${c.reset}`);
991
+ console.log(` ${c.dim}npx cc-safe-setup --profile ${name}${c.reset}`);
992
+ console.log();
993
+ }
994
+ return;
995
+ }
996
+
997
+ const prof = PROFILES[level];
998
+ console.log(c.bold + ` Applying "${level}" profile` + c.reset);
999
+ console.log(c.dim + ` ${prof.desc}` + c.reset);
1000
+ console.log();
1001
+
1002
+ mkdirSync(HOOKS_DIR, { recursive: true });
1003
+ let installed = 0;
1004
+
1005
+ for (const hookId of prof.hooks) {
1006
+ const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
1007
+ if (existsSync(hookPath)) {
1008
+ console.log(c.dim + ' ✓' + c.reset + ` ${hookId}`);
1009
+ continue;
1010
+ }
1011
+
1012
+ // Try built-in first
1013
+ if (SCRIPTS[hookId]) {
1014
+ writeFileSync(hookPath, SCRIPTS[hookId]);
1015
+ chmodSync(hookPath, 0o755);
1016
+ installed++;
1017
+ console.log(c.green + ' +' + c.reset + ` ${hookId}`);
1018
+ continue;
1019
+ }
1020
+
1021
+ // Try examples
1022
+ const exPath = join(__dirname, 'examples', `${hookId}.sh`);
1023
+ if (existsSync(exPath)) {
1024
+ copyFileSync(exPath, hookPath);
1025
+ chmodSync(hookPath, 0o755);
1026
+ installed++;
1027
+ console.log(c.green + ' +' + c.reset + ` ${hookId}`);
1028
+ continue;
1029
+ }
1030
+
1031
+ console.log(c.yellow + ' ?' + c.reset + ` ${hookId} (not found)`);
1032
+ }
1033
+
1034
+ // Update settings.json
1035
+ let settings = {};
1036
+ if (existsSync(SETTINGS_PATH)) {
1037
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
1038
+ }
1039
+ if (!settings.hooks) settings.hooks = {};
1040
+
1041
+ // Register all hooks in settings
1042
+ const hookFiles = prof.hooks.filter(h => existsSync(join(HOOKS_DIR, `${h}.sh`)));
1043
+ const bashHooks = hookFiles.map(h => ({ type: 'command', command: `bash ${join(HOOKS_DIR, h + '.sh')}` }));
1044
+
1045
+ // Simplified: put all under PreToolUse Bash for now
1046
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
1047
+ const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
1048
+ if (existing) {
1049
+ const existingCmds = new Set(existing.hooks.map(h => h.command));
1050
+ for (const h of bashHooks) {
1051
+ if (!existingCmds.has(h.command)) existing.hooks.push(h);
1052
+ }
1053
+ } else {
1054
+ settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: bashHooks });
1055
+ }
1056
+
1057
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
1058
+
1059
+ console.log();
1060
+ console.log(c.green + ` ✓ "${level}" profile applied (${installed} new hooks installed)` + c.reset);
1061
+ console.log(c.dim + ` ${prof.hooks.length} hooks total in profile` + c.reset);
1062
+ console.log();
1063
+ }
1064
+
838
1065
  async function analyze() {
839
1066
  const { execSync } = await import('child_process');
840
1067
  const { readdirSync, statSync } = await import('fs');
@@ -3154,6 +3381,8 @@ async function main() {
3154
3381
  if (FULL) return fullSetup();
3155
3382
  if (DOCTOR) return doctor();
3156
3383
  if (WATCH) return watch();
3384
+ if (TEAM) return team();
3385
+ if (PROFILE_IDX !== -1) return profile(PROFILE);
3157
3386
  if (ANALYZE) return analyze();
3158
3387
  if (SHIELD) return shield();
3159
3388
  if (QUICKFIX) return quickfix();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "8.2.0",
3
+ "version": "8.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": {