cc-safe-setup 3.6.0 → 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 +4 -0
- package/examples/dependency-audit.sh +70 -0
- package/examples/diff-size-guard.sh +61 -0
- package/index.mjs +90 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -220,6 +220,10 @@ Or browse all available examples in [`examples/`](examples/):
|
|
|
220
220
|
- **verify-before-commit.sh** — Block git commit when lint/test commands haven't been run ([#37818](https://github.com/anthropics/claude-code/issues/37818))
|
|
221
221
|
- **hook-debug-wrapper.sh** — Wrap any hook to log input/output/exit code/timing to `~/.claude/hook-debug.log`
|
|
222
222
|
- **loop-detector.sh** — Detect and break command repetition loops (warn at 3, block at 5 repeats)
|
|
223
|
+
- **commit-quality-gate.sh** — Warn on vague commit messages ("update code"), long subjects, mega-commits
|
|
224
|
+
- **session-handoff.sh** — Auto-save git state and session info to `~/.claude/session-handoff.md` on session end
|
|
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)
|
|
223
227
|
|
|
224
228
|
## Safety Checklist
|
|
225
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
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# diff-size-guard.sh — Warn on large uncommitted changes
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code can modify dozens of files in a single session
|
|
7
|
+
# without committing. By the time you notice, the diff is
|
|
8
|
+
# unmanageable. This hook warns when the working tree has
|
|
9
|
+
# too many changed files.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Bash"
|
|
13
|
+
#
|
|
14
|
+
# WHEN IT FIRES:
|
|
15
|
+
# On git commit — checks if the commit is too large
|
|
16
|
+
# Configurable thresholds via environment variables
|
|
17
|
+
#
|
|
18
|
+
# CONFIGURATION:
|
|
19
|
+
# CC_DIFF_WARN=10 — warn when staging 10+ files (default)
|
|
20
|
+
# CC_DIFF_BLOCK=50 — block when staging 50+ files (default)
|
|
21
|
+
# ================================================================
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
25
|
+
|
|
26
|
+
if [[ -z "$COMMAND" ]]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Only check git commit and git add
|
|
31
|
+
if ! echo "$COMMAND" | grep -qE '^\s*git\s+(commit|add\s+(-A|--all|\.))'; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Check number of changed files
|
|
36
|
+
if ! command -v git &>/dev/null || ! [ -d .git ]; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
WARN="${CC_DIFF_WARN:-10}"
|
|
41
|
+
BLOCK="${CC_DIFF_BLOCK:-50}"
|
|
42
|
+
|
|
43
|
+
# Count staged + unstaged changed files
|
|
44
|
+
CHANGED=$(git diff --name-only HEAD 2>/dev/null | wc -l)
|
|
45
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l)
|
|
46
|
+
TOTAL=$((CHANGED + STAGED))
|
|
47
|
+
|
|
48
|
+
if [ "$TOTAL" -ge "$BLOCK" ]; then
|
|
49
|
+
echo "BLOCKED: $TOTAL files changed — too large for one commit." >&2
|
|
50
|
+
echo "" >&2
|
|
51
|
+
echo "Break this into smaller, reviewable commits:" >&2
|
|
52
|
+
echo " git add src/feature/ && git commit -m 'feat: add feature'" >&2
|
|
53
|
+
echo " git add tests/ && git commit -m 'test: add feature tests'" >&2
|
|
54
|
+
echo "" >&2
|
|
55
|
+
echo "Set CC_DIFF_BLOCK to adjust the limit (current: $BLOCK)." >&2
|
|
56
|
+
exit 2
|
|
57
|
+
elif [ "$TOTAL" -ge "$WARN" ]; then
|
|
58
|
+
echo "WARNING: $TOTAL files changed. Consider splitting into smaller commits." >&2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
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
|
|
@@ -350,6 +352,10 @@ function examples() {
|
|
|
350
352
|
'tmp-cleanup.sh': 'Clean up /tmp/claude-*-cwd files on session end',
|
|
351
353
|
'hook-debug-wrapper.sh': 'Wrap any hook to log input/output/exit/timing',
|
|
352
354
|
'loop-detector.sh': 'Detect and break command repetition loops',
|
|
355
|
+
'session-handoff.sh': 'Auto-save session state for next session resume',
|
|
356
|
+
'commit-quality-gate.sh': 'Warn on vague or too-long commit messages',
|
|
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',
|
|
353
359
|
},
|
|
354
360
|
};
|
|
355
361
|
|
|
@@ -780,6 +786,89 @@ async function fullSetup() {
|
|
|
780
786
|
console.log();
|
|
781
787
|
}
|
|
782
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
|
+
|
|
783
872
|
function share() {
|
|
784
873
|
console.log();
|
|
785
874
|
console.log(c.bold + ' cc-safe-setup --share' + c.reset);
|
|
@@ -1984,6 +2073,7 @@ async function main() {
|
|
|
1984
2073
|
if (FULL) return fullSetup();
|
|
1985
2074
|
if (DOCTOR) return doctor();
|
|
1986
2075
|
if (WATCH) return watch();
|
|
2076
|
+
if (BENCHMARK) return benchmark();
|
|
1987
2077
|
if (SHARE) return share();
|
|
1988
2078
|
if (DIFF_FILE) return diff(DIFF_FILE);
|
|
1989
2079
|
if (LINT) return lint();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "One command to make Claude Code safe for autonomous operation. 8 built-in
|
|
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"
|