cc-safe-setup 7.9.0 → 8.0.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/CLAUDE.md +18 -0
- package/examples/commit-scope-guard.sh +32 -0
- package/examples/worktree-guard.sh +28 -0
- package/index.mjs +207 -0
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Project Rules
|
|
2
|
+
|
|
3
|
+
## Safety
|
|
4
|
+
- Do not push to main/master directly
|
|
5
|
+
- Do not force-push
|
|
6
|
+
- Do not delete files outside this project
|
|
7
|
+
- Do not commit .env or credential files
|
|
8
|
+
- Run tests before committing
|
|
9
|
+
|
|
10
|
+
## Code Style
|
|
11
|
+
- Follow existing conventions
|
|
12
|
+
- Keep functions small and focused
|
|
13
|
+
- Add comments only when the logic isn't obvious
|
|
14
|
+
|
|
15
|
+
## Git
|
|
16
|
+
- Use descriptive commit messages
|
|
17
|
+
- One logical change per commit
|
|
18
|
+
- Create feature branches for new work
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# commit-scope-guard.sh — Warn when committing too many files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code can modify dozens of files and commit them all at
|
|
7
|
+
# once, making the commit hard to review and revert. This hook
|
|
8
|
+
# warns when staging more than a configurable number of files.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_MAX_COMMIT_FILES=15 (warn above 15 files)
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
17
|
+
[ -z "$COMMAND" ] && exit 0
|
|
18
|
+
|
|
19
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
20
|
+
|
|
21
|
+
MAX="${CC_MAX_COMMIT_FILES:-15}"
|
|
22
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l)
|
|
23
|
+
|
|
24
|
+
if [ "$STAGED" -gt "$MAX" ]; then
|
|
25
|
+
echo "WARNING: Committing $STAGED files (threshold: $MAX)." >&2
|
|
26
|
+
echo "Consider splitting into smaller, focused commits." >&2
|
|
27
|
+
echo "Files:" >&2
|
|
28
|
+
git diff --cached --name-only 2>/dev/null | head -10 | sed 's/^/ /' >&2
|
|
29
|
+
[ "$STAGED" -gt 10 ] && echo " ... and $((STAGED-10)) more" >&2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
exit 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# worktree-guard.sh — Warn when operating in a git worktree
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Git worktrees share the same .git directory. Destructive operations
|
|
7
|
+
# in one worktree (git clean, reset) can affect the main working tree.
|
|
8
|
+
# This hook warns when Claude is operating inside a worktree.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
# ================================================================
|
|
12
|
+
|
|
13
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
14
|
+
[ -z "$COMMAND" ] && exit 0
|
|
15
|
+
|
|
16
|
+
# Only check destructive git commands
|
|
17
|
+
echo "$COMMAND" | grep -qE '\bgit\s+(clean|reset|checkout\s+--|stash\s+drop)' || exit 0
|
|
18
|
+
|
|
19
|
+
# Check if we're in a worktree
|
|
20
|
+
GITDIR=$(git rev-parse --git-dir 2>/dev/null)
|
|
21
|
+
if echo "$GITDIR" | grep -q "worktrees"; then
|
|
22
|
+
MAIN_DIR=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/.git$||')
|
|
23
|
+
echo "WARNING: You are in a git worktree." >&2
|
|
24
|
+
echo "Main working tree: $MAIN_DIR" >&2
|
|
25
|
+
echo "Destructive git operations may affect the main tree." >&2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -90,6 +90,7 @@ const MIGRATE = process.argv.includes('--migrate');
|
|
|
90
90
|
const GENERATE_CI = process.argv.includes('--generate-ci');
|
|
91
91
|
const REPORT = process.argv.includes('--report');
|
|
92
92
|
const QUICKFIX = process.argv.includes('--quickfix');
|
|
93
|
+
const SHIELD = process.argv.includes('--shield');
|
|
93
94
|
const COMPARE_IDX = process.argv.findIndex(a => a === '--compare');
|
|
94
95
|
const COMPARE = COMPARE_IDX !== -1 ? { a: process.argv[COMPARE_IDX + 1], b: process.argv[COMPARE_IDX + 2] } : null;
|
|
95
96
|
const CREATE_IDX = process.argv.findIndex(a => a === '--create');
|
|
@@ -125,6 +126,7 @@ if (HELP) {
|
|
|
125
126
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
126
127
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
127
128
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
129
|
+
npx cc-safe-setup --shield Maximum safety in one command (fix + scan + install + CLAUDE.md)
|
|
128
130
|
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
129
131
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
130
132
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
@@ -831,6 +833,210 @@ async function fullSetup() {
|
|
|
831
833
|
console.log();
|
|
832
834
|
}
|
|
833
835
|
|
|
836
|
+
async function shield() {
|
|
837
|
+
const { execSync } = await import('child_process');
|
|
838
|
+
const { readdirSync } = await import('fs');
|
|
839
|
+
console.log();
|
|
840
|
+
console.log(c.bold + ' 🛡️ cc-safe-setup --shield' + c.reset);
|
|
841
|
+
console.log(c.dim + ' Maximum safety in one command' + c.reset);
|
|
842
|
+
console.log();
|
|
843
|
+
|
|
844
|
+
// Step 1: Fix environment issues
|
|
845
|
+
console.log(c.bold + ' Step 1: Fix environment' + c.reset);
|
|
846
|
+
await quickfix();
|
|
847
|
+
|
|
848
|
+
// Step 2: Install core safety hooks
|
|
849
|
+
console.log();
|
|
850
|
+
console.log(c.bold + ' Step 2: Install safety hooks' + c.reset);
|
|
851
|
+
// Run the default install
|
|
852
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
853
|
+
let installed = 0;
|
|
854
|
+
for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
|
|
855
|
+
const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
|
|
856
|
+
if (!existsSync(hookPath)) {
|
|
857
|
+
writeFileSync(hookPath, SCRIPTS[hookId]);
|
|
858
|
+
chmodSync(hookPath, 0o755);
|
|
859
|
+
installed++;
|
|
860
|
+
console.log(c.green + ' +' + c.reset + ` ${hookMeta.name}`);
|
|
861
|
+
} else {
|
|
862
|
+
console.log(c.dim + ' ✓' + c.reset + ` ${hookMeta.name} (already installed)`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Step 3: Detect project stack and install recommended examples
|
|
867
|
+
console.log();
|
|
868
|
+
console.log(c.bold + ' Step 3: Project-aware hooks' + c.reset);
|
|
869
|
+
const cwd = process.cwd();
|
|
870
|
+
const extras = [];
|
|
871
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
872
|
+
extras.push('auto-approve-build');
|
|
873
|
+
try {
|
|
874
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
875
|
+
if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) extras.push('block-database-wipe');
|
|
876
|
+
if (pkg.scripts?.deploy) extras.push('deploy-guard');
|
|
877
|
+
} catch {}
|
|
878
|
+
}
|
|
879
|
+
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) extras.push('auto-approve-python');
|
|
880
|
+
if (existsSync(join(cwd, 'Dockerfile'))) extras.push('auto-approve-docker');
|
|
881
|
+
if (existsSync(join(cwd, 'go.mod'))) extras.push('auto-approve-go');
|
|
882
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) extras.push('auto-approve-cargo');
|
|
883
|
+
if (existsSync(join(cwd, 'Makefile'))) extras.push('auto-approve-make');
|
|
884
|
+
if (existsSync(join(cwd, '.env'))) extras.push('env-source-guard');
|
|
885
|
+
|
|
886
|
+
// Always include these for maximum safety
|
|
887
|
+
extras.push('scope-guard', 'no-sudo-guard', 'protect-claudemd');
|
|
888
|
+
|
|
889
|
+
for (const ex of extras) {
|
|
890
|
+
const exPath = join(__dirname, 'examples', `${ex}.sh`);
|
|
891
|
+
const hookPath = join(HOOKS_DIR, `${ex}.sh`);
|
|
892
|
+
if (existsSync(exPath) && !existsSync(hookPath)) {
|
|
893
|
+
copyFileSync(exPath, hookPath);
|
|
894
|
+
chmodSync(hookPath, 0o755);
|
|
895
|
+
console.log(c.green + ' +' + c.reset + ` ${ex}`);
|
|
896
|
+
installed++;
|
|
897
|
+
} else if (existsSync(hookPath)) {
|
|
898
|
+
console.log(c.dim + ' ✓' + c.reset + ` ${ex} (already installed)`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Step 4: Update settings.json
|
|
903
|
+
console.log();
|
|
904
|
+
console.log(c.bold + ' Step 4: Configure settings.json' + c.reset);
|
|
905
|
+
let settings = {};
|
|
906
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
907
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
908
|
+
}
|
|
909
|
+
if (!settings.hooks) settings.hooks = {};
|
|
910
|
+
|
|
911
|
+
// Collect all installed hooks
|
|
912
|
+
const hookFiles = existsSync(HOOKS_DIR)
|
|
913
|
+
? readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'))
|
|
914
|
+
: [];
|
|
915
|
+
|
|
916
|
+
// Build hook entries by trigger type
|
|
917
|
+
const preToolHooks = [];
|
|
918
|
+
const postToolHooks = [];
|
|
919
|
+
const stopHooks = [];
|
|
920
|
+
|
|
921
|
+
for (const f of hookFiles) {
|
|
922
|
+
const content = readFileSync(join(HOOKS_DIR, f), 'utf-8');
|
|
923
|
+
const cmd = `bash ${join(HOOKS_DIR, f)}`;
|
|
924
|
+
|
|
925
|
+
// Check if already in settings
|
|
926
|
+
const alreadyConfigured = JSON.stringify(settings.hooks).includes(f);
|
|
927
|
+
if (alreadyConfigured) continue;
|
|
928
|
+
|
|
929
|
+
// Determine trigger from file content
|
|
930
|
+
if (content.includes('TRIGGER: Stop') || f.includes('api-error') || f.includes('revert-helper') || f.includes('session-handoff') || f.includes('compact-reminder') || f.includes('notify') || f.includes('tmp-cleanup')) {
|
|
931
|
+
stopHooks.push({ type: 'command', command: cmd });
|
|
932
|
+
} else if (content.includes('TRIGGER: PostToolUse') || f.includes('syntax-check') || f.includes('context-monitor') || f.includes('output-length') || f.includes('error-memory') || f.includes('cost-tracker')) {
|
|
933
|
+
postToolHooks.push({ type: 'command', command: cmd });
|
|
934
|
+
} else {
|
|
935
|
+
// Default: PreToolUse
|
|
936
|
+
const matcher = (f.includes('edit-guard') || f.includes('protect-dotfiles') || f.includes('overwrite-guard') || f.includes('binary-file') || f.includes('parallel-edit') || f.includes('test-deletion') || f.includes('memory-write'))
|
|
937
|
+
? 'Edit|Write'
|
|
938
|
+
: 'Bash';
|
|
939
|
+
preToolHooks.push({ type: 'command', command: cmd, _matcher: matcher });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Group PreToolUse hooks by matcher
|
|
944
|
+
if (preToolHooks.length > 0) {
|
|
945
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
946
|
+
const bashHooks = preToolHooks.filter(h => h._matcher === 'Bash').map(({ _matcher, ...h }) => h);
|
|
947
|
+
const editHooks = preToolHooks.filter(h => h._matcher === 'Edit|Write').map(({ _matcher, ...h }) => h);
|
|
948
|
+
if (bashHooks.length > 0) {
|
|
949
|
+
const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
950
|
+
if (existing) {
|
|
951
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
952
|
+
for (const h of bashHooks) {
|
|
953
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: bashHooks });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (editHooks.length > 0) {
|
|
960
|
+
const existing = settings.hooks.PreToolUse.find(e => e.matcher === 'Edit|Write');
|
|
961
|
+
if (existing) {
|
|
962
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
963
|
+
for (const h of editHooks) {
|
|
964
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
settings.hooks.PreToolUse.push({ matcher: 'Edit|Write', hooks: editHooks });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (postToolHooks.length > 0) {
|
|
972
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
973
|
+
const existing = settings.hooks.PostToolUse.find(e => e.matcher === '');
|
|
974
|
+
if (existing) {
|
|
975
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
976
|
+
for (const h of postToolHooks) {
|
|
977
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
settings.hooks.PostToolUse.push({ matcher: '', hooks: postToolHooks });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (stopHooks.length > 0) {
|
|
984
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
985
|
+
const existing = settings.hooks.Stop.find(e => e.matcher === '');
|
|
986
|
+
if (existing) {
|
|
987
|
+
const existingCmds = new Set(existing.hooks.map(h => h.command));
|
|
988
|
+
for (const h of stopHooks) {
|
|
989
|
+
if (!existingCmds.has(h.command)) existing.hooks.push(h);
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
settings.hooks.Stop.push({ matcher: '', hooks: stopHooks });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
997
|
+
console.log(c.green + ' ✓' + c.reset + ' settings.json updated');
|
|
998
|
+
|
|
999
|
+
// Step 5: Generate CLAUDE.md template if none exists
|
|
1000
|
+
console.log();
|
|
1001
|
+
console.log(c.bold + ' Step 5: CLAUDE.md' + c.reset);
|
|
1002
|
+
if (!existsSync(join(cwd, 'CLAUDE.md'))) {
|
|
1003
|
+
const template = `# Project Rules
|
|
1004
|
+
|
|
1005
|
+
## Safety
|
|
1006
|
+
- Do not push to main/master directly
|
|
1007
|
+
- Do not force-push
|
|
1008
|
+
- Do not delete files outside this project
|
|
1009
|
+
- Do not commit .env or credential files
|
|
1010
|
+
- Run tests before committing
|
|
1011
|
+
|
|
1012
|
+
## Code Style
|
|
1013
|
+
- Follow existing conventions
|
|
1014
|
+
- Keep functions small and focused
|
|
1015
|
+
- Add comments only when the logic isn't obvious
|
|
1016
|
+
|
|
1017
|
+
## Git
|
|
1018
|
+
- Use descriptive commit messages
|
|
1019
|
+
- One logical change per commit
|
|
1020
|
+
- Create feature branches for new work
|
|
1021
|
+
`;
|
|
1022
|
+
writeFileSync(join(cwd, 'CLAUDE.md'), template);
|
|
1023
|
+
console.log(c.green + ' +' + c.reset + ' Created CLAUDE.md with safety rules template');
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(c.dim + ' ✓' + c.reset + ' CLAUDE.md already exists');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Summary
|
|
1029
|
+
console.log();
|
|
1030
|
+
const totalHooks = hookFiles.length;
|
|
1031
|
+
console.log(c.bold + c.green + ' 🛡️ Shield activated!' + c.reset);
|
|
1032
|
+
console.log(c.dim + ` ${totalHooks} hooks installed and configured.` + c.reset);
|
|
1033
|
+
console.log(c.dim + ' Your Claude Code sessions are now protected.' + c.reset);
|
|
1034
|
+
console.log();
|
|
1035
|
+
console.log(c.dim + ' Verify: npx cc-safe-setup --verify' + c.reset);
|
|
1036
|
+
console.log(c.dim + ' Status: npx cc-safe-setup --status' + c.reset);
|
|
1037
|
+
console.log();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
834
1040
|
async function quickfix() {
|
|
835
1041
|
const { execSync } = await import('child_process');
|
|
836
1042
|
console.log();
|
|
@@ -2836,6 +3042,7 @@ async function main() {
|
|
|
2836
3042
|
if (FULL) return fullSetup();
|
|
2837
3043
|
if (DOCTOR) return doctor();
|
|
2838
3044
|
if (WATCH) return watch();
|
|
3045
|
+
if (SHIELD) return shield();
|
|
2839
3046
|
if (QUICKFIX) return quickfix();
|
|
2840
3047
|
if (REPORT) return report();
|
|
2841
3048
|
if (GENERATE_CI) return generateCI();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.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": {
|