cc-safe-setup 10.1.0 → 10.3.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 +1 -1
- package/examples/stale-env-guard.sh +33 -0
- package/examples/test-coverage-guard.sh +27 -0
- package/index.mjs +91 -0
- 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 + 104 examples = **
|
|
9
|
+
8 built-in + 104 examples = **116 hooks**. 36 CLI commands. 531 tests. 5 languages. [**Hub**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [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) · [Examples](https://yurukusa.github.io/cc-safe-setup/by-example.html) · [Matrix](https://yurukusa.github.io/cc-safe-setup/matrix.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx cc-safe-setup
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# stale-env-guard.sh — Warn when .env files are very old
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# .env files with API keys should be rotated periodically.
|
|
7
|
+
# This hook warns when .env hasn't been modified in 90+ days,
|
|
8
|
+
# suggesting credential rotation.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
11
|
+
#
|
|
12
|
+
# CONFIG:
|
|
13
|
+
# CC_ENV_MAX_AGE_DAYS=90
|
|
14
|
+
# ================================================================
|
|
15
|
+
|
|
16
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
17
|
+
[ -z "$COMMAND" ] && exit 0
|
|
18
|
+
|
|
19
|
+
# Only check on deploy-related or env-reading commands
|
|
20
|
+
echo "$COMMAND" | grep -qE '(deploy|source\s+\.env|cat\s+\.env|docker.*\.env)' || exit 0
|
|
21
|
+
|
|
22
|
+
MAX_DAYS="${CC_ENV_MAX_AGE_DAYS:-90}"
|
|
23
|
+
|
|
24
|
+
for envfile in .env .env.local .env.production; do
|
|
25
|
+
[ -f "$envfile" ] || continue
|
|
26
|
+
AGE_DAYS=$(( ($(date +%s) - $(stat -c %Y "$envfile" 2>/dev/null || echo 0)) / 86400 ))
|
|
27
|
+
if [ "$AGE_DAYS" -gt "$MAX_DAYS" ]; then
|
|
28
|
+
echo "WARNING: $envfile is $AGE_DAYS days old (threshold: $MAX_DAYS)." >&2
|
|
29
|
+
echo "Consider rotating API keys and credentials." >&2
|
|
30
|
+
fi
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# test-coverage-guard.sh — Warn when code grows without tests
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude adds features without writing tests. This hook checks
|
|
7
|
+
# if source files changed more than test files, suggesting tests
|
|
8
|
+
# are needed before committing.
|
|
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
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
16
|
+
|
|
17
|
+
# Count staged source vs test file changes
|
|
18
|
+
SRC_CHANGES=$(git diff --cached --name-only 2>/dev/null | grep -cvE '(test|spec|__tests__|_test\.|\.test\.)' || echo 0)
|
|
19
|
+
TEST_CHANGES=$(git diff --cached --name-only 2>/dev/null | grep -cE '(test|spec|__tests__|_test\.|\.test\.)' || echo 0)
|
|
20
|
+
|
|
21
|
+
# If source changed significantly but no tests
|
|
22
|
+
if [ "$SRC_CHANGES" -gt 3 ] && [ "$TEST_CHANGES" -eq 0 ]; then
|
|
23
|
+
echo "WARNING: $SRC_CHANGES source files changed but 0 test files." >&2
|
|
24
|
+
echo "Consider adding tests for the new code." >&2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -97,6 +97,8 @@ const MIGRATE_FROM_IDX = process.argv.findIndex(a => a === '--migrate-from');
|
|
|
97
97
|
const MIGRATE_FROM = MIGRATE_FROM_IDX !== -1 ? process.argv[MIGRATE_FROM_IDX + 1] : null;
|
|
98
98
|
const HEALTH = process.argv.includes('--health');
|
|
99
99
|
const FROM_CLAUDEMD = process.argv.includes('--from-claudemd');
|
|
100
|
+
const GUARD_IDX = process.argv.findIndex(a => a === '--guard');
|
|
101
|
+
const GUARD_DESC = GUARD_IDX !== -1 ? process.argv.slice(GUARD_IDX + 1).join(' ') : null;
|
|
100
102
|
const DIFF_HOOKS_IDX = process.argv.findIndex(a => a === '--diff-hooks');
|
|
101
103
|
const DIFF_HOOKS = DIFF_HOOKS_IDX !== -1 ? process.argv[DIFF_HOOKS_IDX + 1] : null;
|
|
102
104
|
const PROFILE_IDX = process.argv.findIndex(a => a === '--profile');
|
|
@@ -136,6 +138,7 @@ if (HELP) {
|
|
|
136
138
|
npx cc-safe-setup --doctor Diagnose why hooks aren't working
|
|
137
139
|
npx cc-safe-setup --watch Live dashboard of blocked commands
|
|
138
140
|
npx cc-safe-setup --create "<desc>" Generate a custom hook from description
|
|
141
|
+
npx cc-safe-setup --guard "<rule>" Instantly enforce a rule (generate + install + activate)
|
|
139
142
|
npx cc-safe-setup --diff-hooks <path> Compare hooks between two settings files
|
|
140
143
|
npx cc-safe-setup --from-claudemd Convert CLAUDE.md rules into hooks
|
|
141
144
|
npx cc-safe-setup --health Hook health dashboard (size, permissions, age)
|
|
@@ -850,6 +853,93 @@ async function fullSetup() {
|
|
|
850
853
|
console.log();
|
|
851
854
|
}
|
|
852
855
|
|
|
856
|
+
async function guard(description) {
|
|
857
|
+
if (!description) {
|
|
858
|
+
console.log();
|
|
859
|
+
console.log(c.bold + ' cc-safe-setup --guard "<rule>"' + c.reset);
|
|
860
|
+
console.log(c.dim + ' Instantly enforce a rule — generates, installs, and activates a hook.' + c.reset);
|
|
861
|
+
console.log();
|
|
862
|
+
console.log(' Examples:');
|
|
863
|
+
console.log(c.dim + ' npx cc-safe-setup --guard "never touch the database"' + c.reset);
|
|
864
|
+
console.log(c.dim + ' npx cc-safe-setup --guard "block all sudo commands"' + c.reset);
|
|
865
|
+
console.log(c.dim + ' npx cc-safe-setup --guard "no force push"' + c.reset);
|
|
866
|
+
console.log(c.dim + ' npx cc-safe-setup --guard "warn before deleting files"' + c.reset);
|
|
867
|
+
console.log();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
console.log();
|
|
872
|
+
console.log(c.bold + ` 🛡️ Guard: "${description}"` + c.reset);
|
|
873
|
+
console.log();
|
|
874
|
+
|
|
875
|
+
const desc = description.toLowerCase();
|
|
876
|
+
let hookName, hookScript, trigger = 'PreToolUse', matcher = 'Bash';
|
|
877
|
+
|
|
878
|
+
// Map natural language to hook patterns
|
|
879
|
+
if (desc.match(/database|drop|migrate|prisma|sql/)) {
|
|
880
|
+
hookName = 'guard-database';
|
|
881
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qiE '(DROP\\s+(DATABASE|TABLE)|migrate:fresh|prisma\\s+reset|db:drop|TRUNCATE)'; then\n echo "BLOCKED: Database operation blocked by guard rule." >&2\n echo "Rule: ${description}" >&2\n exit 2\nfi\nexit 0`;
|
|
882
|
+
} else if (desc.match(/sudo|root|admin/)) {
|
|
883
|
+
hookName = 'guard-sudo';
|
|
884
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qE '^\\s*sudo\\b'; then\n echo "BLOCKED: sudo blocked by guard rule." >&2\n echo "Rule: ${description}" >&2\n exit 2\nfi\nexit 0`;
|
|
885
|
+
} else if (desc.match(/force.?push|push.*force/)) {
|
|
886
|
+
hookName = 'guard-force-push';
|
|
887
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qE 'git\\s+push\\s+.*--force'; then\n echo "BLOCKED: Force push blocked by guard rule." >&2\n echo "Rule: ${description}" >&2\n exit 2\nfi\nexit 0`;
|
|
888
|
+
} else if (desc.match(/push.*main|main.*push/)) {
|
|
889
|
+
hookName = 'guard-push-main';
|
|
890
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qE 'git\\s+push\\s+.*\\b(main|master)\\b'; then\n echo "BLOCKED: Push to main blocked by guard rule." >&2\n echo "Rule: ${description}" >&2\n exit 2\nfi\nexit 0`;
|
|
891
|
+
} else if (desc.match(/delet|rm|remov/)) {
|
|
892
|
+
hookName = 'guard-delete';
|
|
893
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qE 'rm\\s+.*-rf'; then\n echo "WARNING: Deletion detected." >&2\n echo "Rule: ${description}" >&2\nfi\nexit 0`;
|
|
894
|
+
} else if (desc.match(/deploy|ship|release|publish/)) {
|
|
895
|
+
hookName = 'guard-deploy';
|
|
896
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qiE '(deploy|publish|release|vercel|netlify)'; then\n echo "WARNING: Deploy/publish command detected." >&2\n echo "Rule: ${description}" >&2\nfi\nexit 0`;
|
|
897
|
+
} else if (desc.match(/test|spec/)) {
|
|
898
|
+
hookName = 'guard-test-required';
|
|
899
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qE 'git\\s+commit'; then\n echo "WARNING: Commit detected." >&2\n echo "Rule: ${description}" >&2\nfi\nexit 0`;
|
|
900
|
+
} else {
|
|
901
|
+
// Generic guard — extract keyword and block commands containing it
|
|
902
|
+
const keyword = desc.replace(/[^a-z0-9 ]/g, '').split(' ').filter(w => w.length > 3).pop() || 'guard';
|
|
903
|
+
hookName = `guard-${keyword}`;
|
|
904
|
+
hookScript = `#!/bin/bash\nCOMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)\n[ -z "$COMMAND" ] && exit 0\nif echo "$COMMAND" | grep -qiE '${keyword}'; then\n echo "WARNING: Command matches guard rule." >&2\n echo "Rule: ${description}" >&2\nfi\nexit 0`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Write hook
|
|
908
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
909
|
+
const hookPath = join(HOOKS_DIR, `${hookName}.sh`);
|
|
910
|
+
writeFileSync(hookPath, hookScript);
|
|
911
|
+
chmodSync(hookPath, 0o755);
|
|
912
|
+
console.log(c.green + ' ✓' + c.reset + ` Hook created: ${hookPath}`);
|
|
913
|
+
|
|
914
|
+
// Register in settings.json
|
|
915
|
+
let settings = {};
|
|
916
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
917
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
918
|
+
}
|
|
919
|
+
if (!settings.hooks) settings.hooks = {};
|
|
920
|
+
if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
|
|
921
|
+
|
|
922
|
+
const cmd = `bash ${hookPath}`;
|
|
923
|
+
const alreadyExists = JSON.stringify(settings.hooks).includes(hookName);
|
|
924
|
+
if (!alreadyExists) {
|
|
925
|
+
const existing = settings.hooks[trigger].find(e => e.matcher === matcher);
|
|
926
|
+
if (existing) {
|
|
927
|
+
existing.hooks.push({ type: 'command', command: cmd });
|
|
928
|
+
} else {
|
|
929
|
+
settings.hooks[trigger].push({ matcher, hooks: [{ type: 'command', command: cmd }] });
|
|
930
|
+
}
|
|
931
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
932
|
+
console.log(c.green + ' ✓' + c.reset + ' Registered in settings.json');
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
console.log(c.green + ' ✓' + c.reset + ' Guard active immediately');
|
|
936
|
+
console.log();
|
|
937
|
+
console.log(c.dim + ` Rule: "${description}"` + c.reset);
|
|
938
|
+
console.log(c.dim + ` Hook: ${hookName}.sh` + c.reset);
|
|
939
|
+
console.log(c.dim + ' Remove: npx cc-safe-setup --uninstall' + c.reset);
|
|
940
|
+
console.log();
|
|
941
|
+
}
|
|
942
|
+
|
|
853
943
|
async function diffHooks(otherPath) {
|
|
854
944
|
console.log();
|
|
855
945
|
console.log(c.bold + ' cc-safe-setup --diff-hooks' + c.reset);
|
|
@@ -3815,6 +3905,7 @@ async function main() {
|
|
|
3815
3905
|
if (FULL) return fullSetup();
|
|
3816
3906
|
if (DOCTOR) return doctor();
|
|
3817
3907
|
if (WATCH) return watch();
|
|
3908
|
+
if (GUARD_IDX !== -1) return guard(GUARD_DESC);
|
|
3818
3909
|
if (DIFF_HOOKS_IDX !== -1) return diffHooks(DIFF_HOOKS);
|
|
3819
3910
|
if (FROM_CLAUDEMD) return fromClaudeMd();
|
|
3820
3911
|
if (HEALTH) return health();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.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": {
|