cc-safe-setup 3.6.1 → 3.7.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
@@ -223,6 +223,7 @@ Or browse all available examples in [`examples/`](examples/):
223
223
  - **commit-quality-gate.sh** — Warn on vague commit messages ("update code"), long subjects, mega-commits
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
+ - **dependency-audit.sh** — Warn when installing packages not in manifest (npm/pip/cargo supply chain awareness)
226
227
 
227
228
  ## Safety Checklist
228
229
 
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # dependency-audit.sh — Warn before installing unknown packages
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude Code may install packages you've never heard of.
7
+ # This hook warns when npm/pip/cargo installs a new dependency,
8
+ # giving you a chance to review before it executes.
9
+ #
10
+ # Doesn't block devDependencies or packages already in
11
+ # package.json/requirements.txt.
12
+ #
13
+ # TRIGGER: PreToolUse
14
+ # MATCHER: "Bash"
15
+ #
16
+ # WHAT IT WARNS ON:
17
+ # - npm install <pkg> (not in package.json)
18
+ # - pip install <pkg> (not in requirements.txt)
19
+ # - cargo add <pkg> (not in Cargo.toml)
20
+ #
21
+ # WHAT IT ALLOWS:
22
+ # - npm install (no args = install from package.json)
23
+ # - pip install -r requirements.txt
24
+ # - Packages already in manifest files
25
+ # ================================================================
26
+
27
+ INPUT=$(cat)
28
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
29
+
30
+ if [[ -z "$COMMAND" ]]; then
31
+ exit 0
32
+ fi
33
+
34
+ # npm install <package>
35
+ if echo "$COMMAND" | grep -qE '^\s*npm\s+install\s+\S'; then
36
+ # Skip if no specific package (just `npm install`)
37
+ PKG=$(echo "$COMMAND" | grep -oP 'npm\s+install\s+(-[DSg]\s+)*\K[^-\s]\S*' | head -1)
38
+ if [[ -n "$PKG" ]] && [[ -f "package.json" ]]; then
39
+ if ! grep -q "\"$PKG\"" package.json 2>/dev/null; then
40
+ echo "NOTE: Installing new npm package: $PKG" >&2
41
+ echo "Not found in package.json. Review before proceeding." >&2
42
+ fi
43
+ fi
44
+ fi
45
+
46
+ # pip install <package>
47
+ if echo "$COMMAND" | grep -qE '^\s*(pip3?|python3?\s+-m\s+pip)\s+install\s+\S'; then
48
+ # Skip -r requirements.txt
49
+ if ! echo "$COMMAND" | grep -qE '\-r\s+'; then
50
+ PKG=$(echo "$COMMAND" | grep -oP '(pip3?|python3?\s+-m\s+pip)\s+install\s+(-[^\s]+\s+)*\K[^-\s]\S*' | head -1)
51
+ if [[ -n "$PKG" ]] && [[ -f "requirements.txt" ]]; then
52
+ if ! grep -qi "$PKG" requirements.txt 2>/dev/null; then
53
+ echo "NOTE: Installing new pip package: $PKG" >&2
54
+ echo "Not found in requirements.txt." >&2
55
+ fi
56
+ fi
57
+ fi
58
+ fi
59
+
60
+ # cargo add <package>
61
+ if echo "$COMMAND" | grep -qE '^\s*cargo\s+add\s+\S'; then
62
+ PKG=$(echo "$COMMAND" | grep -oP 'cargo\s+add\s+\K\S+' | head -1)
63
+ if [[ -n "$PKG" ]] && [[ -f "Cargo.toml" ]]; then
64
+ if ! grep -q "$PKG" Cargo.toml 2>/dev/null; then
65
+ echo "NOTE: Adding new cargo dependency: $PKG" >&2
66
+ fi
67
+ fi
68
+ fi
69
+
70
+ exit 0
package/index.mjs CHANGED
@@ -83,6 +83,7 @@ const LINT = process.argv.includes('--lint');
83
83
  const DIFF_IDX = process.argv.findIndex(a => a === '--diff');
84
84
  const DIFF_FILE = DIFF_IDX !== -1 ? process.argv[DIFF_IDX + 1] : null;
85
85
  const SHARE = process.argv.includes('--share');
86
+ const BENCHMARK = process.argv.includes('--benchmark');
86
87
  const CREATE_IDX = process.argv.findIndex(a => a === '--create');
87
88
  const CREATE_DESC = CREATE_IDX !== -1 ? process.argv.slice(CREATE_IDX + 1).join(' ') : null;
88
89
 
@@ -104,6 +105,7 @@ if (HELP) {
104
105
  npx cc-safe-setup --audit --json Machine-readable output for CI/CD
105
106
  npx cc-safe-setup --scan Detect tech stack, recommend hooks
106
107
  npx cc-safe-setup --learn Learn from your block history
108
+ npx cc-safe-setup --benchmark Measure hook execution time
107
109
  npx cc-safe-setup --share Generate shareable URL for your setup
108
110
  npx cc-safe-setup --diff <file> Compare your settings with another file
109
111
  npx cc-safe-setup --lint Static analysis of hook configuration
@@ -353,6 +355,7 @@ function examples() {
353
355
  'session-handoff.sh': 'Auto-save session state for next session resume',
354
356
  'commit-quality-gate.sh': 'Warn on vague or too-long commit messages',
355
357
  'diff-size-guard.sh': 'Warn/block on large diffs (10+ files warn, 50+ block)',
358
+ 'dependency-audit.sh': 'Warn on new package installs not in manifest',
356
359
  },
357
360
  };
358
361
 
@@ -783,6 +786,89 @@ async function fullSetup() {
783
786
  console.log();
784
787
  }
785
788
 
789
+ async function benchmark() {
790
+ const { spawnSync } = await import('child_process');
791
+
792
+ console.log();
793
+ console.log(c.bold + ' cc-safe-setup --benchmark' + c.reset);
794
+ console.log(c.dim + ' Measuring hook execution time (10 runs each)...' + c.reset);
795
+ console.log();
796
+
797
+ if (!existsSync(SETTINGS_PATH)) {
798
+ console.log(c.red + ' No settings.json found.' + c.reset);
799
+ process.exit(1);
800
+ }
801
+
802
+ const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
803
+ const hooks = settings.hooks || {};
804
+ const results = [];
805
+ const testInput = JSON.stringify({ tool_input: { command: 'echo hello' } });
806
+ const RUNS = 10;
807
+
808
+ for (const [trigger, entries] of Object.entries(hooks)) {
809
+ for (const entry of entries) {
810
+ for (const h of (entry.hooks || [])) {
811
+ if (h.type !== 'command') continue;
812
+ let scriptPath = (h.command || '').replace(/^(bash|sh|node)\s+/, '').split(/\s+/)[0];
813
+ scriptPath = scriptPath.replace(/^~/, HOME);
814
+ if (!existsSync(scriptPath)) continue;
815
+
816
+ const name = scriptPath.split('/').pop();
817
+ const times = [];
818
+
819
+ for (let i = 0; i < RUNS; i++) {
820
+ const start = process.hrtime.bigint();
821
+ spawnSync('bash', [scriptPath], {
822
+ input: testInput,
823
+ timeout: 5000,
824
+ stdio: ['pipe', 'pipe', 'pipe'],
825
+ });
826
+ const end = process.hrtime.bigint();
827
+ times.push(Number(end - start) / 1_000_000); // ms
828
+ }
829
+
830
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
831
+ const max = Math.max(...times);
832
+ const min = Math.min(...times);
833
+
834
+ results.push({ name, trigger, avg, max, min, matcher: entry.matcher || '(all)' });
835
+ }
836
+ }
837
+ }
838
+
839
+ // Sort by avg time descending
840
+ results.sort((a, b) => b.avg - a.avg);
841
+
842
+ // Display
843
+ const maxAvg = results[0]?.avg || 1;
844
+ console.log(c.bold + ' Hook Avg Min Max Trigger' + c.reset);
845
+ console.log(' ' + '-'.repeat(75));
846
+
847
+ for (const r of results) {
848
+ const bar = '█'.repeat(Math.ceil(r.avg / maxAvg * 15));
849
+ const avgColor = r.avg > 100 ? c.red : r.avg > 50 ? c.yellow : c.green;
850
+ console.log(
851
+ ' ' + r.name.padEnd(30) +
852
+ avgColor + r.avg.toFixed(1).padStart(6) + 'ms' + c.reset +
853
+ r.min.toFixed(1).padStart(6) + 'ms' +
854
+ r.max.toFixed(1).padStart(6) + 'ms' +
855
+ ' ' + c.dim + r.trigger + c.reset
856
+ );
857
+ }
858
+
859
+ console.log();
860
+ const totalAvg = results.reduce((s, r) => s + r.avg, 0);
861
+ const slow = results.filter(r => r.avg > 50);
862
+
863
+ console.log(c.dim + ' Total avg per tool call: ' + totalAvg.toFixed(1) + 'ms (sum of all hooks on that trigger)' + c.reset);
864
+ if (slow.length > 0) {
865
+ console.log(c.yellow + ' ' + slow.length + ' hook(s) over 50ms — consider optimizing' + c.reset);
866
+ } else {
867
+ console.log(c.green + ' All hooks under 50ms — good performance' + c.reset);
868
+ }
869
+ console.log();
870
+ }
871
+
786
872
  function share() {
787
873
  console.log();
788
874
  console.log(c.bold + ' cc-safe-setup --share' + c.reset);
@@ -1987,6 +2073,7 @@ async function main() {
1987
2073
  if (FULL) return fullSetup();
1988
2074
  if (DOCTOR) return doctor();
1989
2075
  if (WATCH) return watch();
2076
+ if (BENCHMARK) return benchmark();
1990
2077
  if (SHARE) return share();
1991
2078
  if (DIFF_FILE) return diff(DIFF_FILE);
1992
2079
  if (LINT) return lint();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "3.6.1",
4
- "description": "One command to make Claude Code safe for autonomous operation. 8 built-in hooks + 34 examples. Create, audit, lint, diff, share, watch, learn. Session handoff, loop detection, commit quality gate.",
3
+ "version": "3.7.0",
4
+ "description": "One command to make Claude Code safe for autonomous operation. 8 built-in + 36 examples. 21 commands: create, audit, lint, diff, share, benchmark, watch, learn. 2,500+ daily npm downloads.",
5
5
  "main": "index.mjs",
6
6
  "bin": {
7
7
  "cc-safe-setup": "index.mjs"