cc-safe-setup 5.2.0 → 5.3.1

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 -0
  2. package/index.mjs +91 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -224,6 +224,7 @@ Or browse all available examples in [`examples/`](examples/):
224
224
  - **session-handoff.sh** — Auto-save git state and session info to `~/.claude/session-handoff.md` on session end
225
225
  - **diff-size-guard.sh** — Warn/block when committing too many files at once (default: warn at 10, block at 50)
226
226
  - **dependency-audit.sh** — Warn when installing packages not in manifest (npm/pip/cargo supply chain awareness)
227
+ - **env-source-guard.sh** — Block sourcing .env files into shell environment ([#401](https://github.com/anthropics/claude-code/issues/401))
227
228
  - **symlink-guard.sh** — Detect symlink/junction traversal in rm targets ([#36339](https://github.com/anthropics/claude-code/issues/36339) [#764](https://github.com/anthropics/claude-code/issues/764))
228
229
  - **binary-file-guard.sh** — Warn when Write targets binary file types (images, archives)
229
230
  - **stale-branch-guard.sh** — Warn when working branch is far behind default
package/index.mjs CHANGED
@@ -86,6 +86,8 @@ const SHARE = process.argv.includes('--share');
86
86
  const BENCHMARK = process.argv.includes('--benchmark');
87
87
  const DASHBOARD = process.argv.includes('--dashboard');
88
88
  const ISSUES = process.argv.includes('--issues');
89
+ const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
90
+ const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
89
91
  const CREATE_IDX = process.argv.findIndex(a => a === '--create');
90
92
  const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
91
93
 
@@ -107,6 +109,7 @@ if (HELP) {
107
109
  npx cc-safe-setup --audit --json Machine-readable output for CI/CD
108
110
  npx cc-safe-setup --scan Detect tech stack, recommend hooks
109
111
  npx cc-safe-setup --learn Learn from your block history
112
+ npx cc-safe-setup --compare <a> <b> Compare two hooks side-by-side
110
113
  npx cc-safe-setup --issues Show GitHub Issues each hook addresses
111
114
  npx cc-safe-setup --dashboard Real-time status dashboard
112
115
  npx cc-safe-setup --benchmark Measure hook execution time
@@ -360,6 +363,9 @@ function examples() {
360
363
  'commit-quality-gate.sh': 'Warn on vague or too-long commit messages',
361
364
  'diff-size-guard.sh': 'Warn/block on large diffs (10+ files warn, 50+ block)',
362
365
  'dependency-audit.sh': 'Warn on new package installs not in manifest',
366
+ 'binary-file-guard.sh': 'Warn when Write targets binary file types',
367
+ 'stale-branch-guard.sh': 'Warn when branch is far behind default',
368
+ 'symlink-guard.sh': 'Detect symlink/junction traversal in rm targets',
363
369
  'cost-tracker.sh': 'Estimate session token cost ($1 warn, $5 alert)',
364
370
  'read-before-edit.sh': 'Warn when editing files not recently read',
365
371
  },
@@ -807,6 +813,90 @@ async function fullSetup() {
807
813
  console.log();
808
814
  }
809
815
 
816
+ async function compare(hookA, hookB) {
817
+ const { spawnSync } = await import('child_process');
818
+ const { statSync } = await import('fs');
819
+
820
+ console.log();
821
+ console.log(c.bold + ' cc-safe-setup --compare' + c.reset);
822
+ console.log();
823
+
824
+ if (!hookA || !hookB) {
825
+ console.log(c.red + ' Usage: npx cc-safe-setup --compare <hook-a.sh> <hook-b.sh>' + c.reset);
826
+ process.exit(1);
827
+ }
828
+
829
+ // Resolve paths
830
+ const resolveHook = (h) => {
831
+ if (existsSync(h)) return h;
832
+ const inHooks = join(HOOKS_DIR, h);
833
+ if (existsSync(inHooks)) return inHooks;
834
+ const inExamples = join(__dirname, 'examples', h);
835
+ if (existsSync(inExamples)) return inExamples;
836
+ return null;
837
+ };
838
+
839
+ const pathA = resolveHook(hookA);
840
+ const pathB = resolveHook(hookB);
841
+
842
+ if (!pathA) { console.log(c.red + ' Hook A not found: ' + hookA + c.reset); process.exit(1); }
843
+ if (!pathB) { console.log(c.red + ' Hook B not found: ' + hookB + c.reset); process.exit(1); }
844
+
845
+ const nameA = hookA.split('/').pop();
846
+ const nameB = hookB.split('/').pop();
847
+
848
+ // Test cases
849
+ const tests = [
850
+ { name: 'empty input', input: '{}' },
851
+ { name: 'safe command', input: '{"tool_input":{"command":"echo hello"}}' },
852
+ { name: 'rm -rf /', input: '{"tool_input":{"command":"rm -rf /"}}' },
853
+ { name: 'rm -rf ~', input: '{"tool_input":{"command":"rm -rf ~"}}' },
854
+ { name: 'git push main', input: '{"tool_input":{"command":"git push origin main"}}' },
855
+ { name: 'git push --force', input: '{"tool_input":{"command":"git push --force"}}' },
856
+ { name: 'git add .env', input: '{"tool_input":{"command":"git add .env"}}' },
857
+ { name: 'git reset --hard', input: '{"tool_input":{"command":"git reset --hard"}}' },
858
+ { name: 'npm test', input: '{"tool_input":{"command":"npm test"}}' },
859
+ { name: 'cd && git log', input: '{"tool_input":{"command":"cd /tmp && git log"}}' },
860
+ ];
861
+
862
+ function runHook(path, input) {
863
+ const start = process.hrtime.bigint();
864
+ const result = spawnSync('bash', [path], { input, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
865
+ const ms = Number(process.hrtime.bigint() - start) / 1_000_000;
866
+ return { exit: result.status ?? -1, ms, stderr: (result.stderr || Buffer.alloc(0)).toString().slice(0, 80) };
867
+ }
868
+
869
+ // Header
870
+ console.log(' ' + c.bold + 'Test'.padEnd(20) + nameA.padEnd(25) + nameB + c.reset);
871
+ console.log(' ' + '-'.repeat(65));
872
+
873
+ let sameCount = 0;
874
+ let diffCount = 0;
875
+
876
+ for (const test of tests) {
877
+ const a = runHook(pathA, test.input);
878
+ const b = runHook(pathB, test.input);
879
+ const same = a.exit === b.exit;
880
+ if (same) sameCount++; else diffCount++;
881
+
882
+ const exitA = a.exit === 0 ? c.green + 'allow' + c.reset : a.exit === 2 ? c.red + 'BLOCK' + c.reset : c.yellow + 'err' + a.exit + c.reset;
883
+ const exitB = b.exit === 0 ? c.green + 'allow' + c.reset : b.exit === 2 ? c.red + 'BLOCK' + c.reset : c.yellow + 'err' + b.exit + c.reset;
884
+ const marker = same ? ' ' : c.yellow + '≠' + c.reset;
885
+
886
+ console.log(' ' + marker + ' ' + test.name.padEnd(18) + (exitA + ' ' + a.ms.toFixed(0) + 'ms').padEnd(30) + exitB + ' ' + b.ms.toFixed(0) + 'ms');
887
+ }
888
+
889
+ // Size comparison
890
+ const sizeA = statSync(pathA).size;
891
+ const sizeB = statSync(pathB).size;
892
+
893
+ console.log(' ' + '-'.repeat(65));
894
+ console.log(' Same decisions: ' + sameCount + '/' + tests.length);
895
+ if (diffCount > 0) console.log(' ' + c.yellow + 'Different: ' + diffCount + c.reset);
896
+ console.log(' Size: ' + nameA + ' ' + sizeA + 'B vs ' + nameB + ' ' + sizeB + 'B');
897
+ console.log();
898
+ }
899
+
810
900
  function issues() {
811
901
  // Map hooks to the GitHub Issues they address
812
902
  const ISSUE_MAP = [
@@ -2327,6 +2417,7 @@ async function main() {
2327
2417
  if (FULL) return fullSetup();
2328
2418
  if (DOCTOR) return doctor();
2329
2419
  if (WATCH) return watch();
2420
+ if (COMPARE) return compare(COMPARE.a, COMPARE.b);
2330
2421
  if (ISSUES) return issues();
2331
2422
  if (DASHBOARD) return dashboard();
2332
2423
  if (BENCHMARK) return benchmark();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "5.2.0",
3
+ "version": "5.3.1",
4
4
  "description": "One command to make Claude Code safe for autonomous operation. 8 built-in + 39 examples. 23 commands including dashboard, issues, create, audit, lint, diff. 260 tests. 2,500+ daily npm downloads.",
5
5
  "main": "index.mjs",
6
6
  "bin": {