cc-safe-setup 10.2.0 → 10.4.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
@@ -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 = **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)
9
+ 8 built-in + 104 examples = **118 hooks**. 37 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
@@ -105,6 +105,8 @@ Safe to run multiple times. Existing settings are preserved. A backup is created
105
105
 
106
106
  **Maximum safety:** `npx cc-safe-setup --shield` — one command: fix environment, install hooks, detect stack, configure settings, generate CLAUDE.md.
107
107
 
108
+ **Instant rule:** `npx cc-safe-setup --guard "never touch the database"` — generates, installs, activates a hook instantly from plain English.
109
+
108
110
  **Team setup:** `npx cc-safe-setup --team` — copy hooks to `.claude/hooks/` with relative paths, commit to repo for team sharing.
109
111
 
110
112
  **Preview first:** `npx cc-safe-setup --dry-run`
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # git-author-guard.sh — Verify commit author is configured correctly
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude Code sometimes commits with incorrect or default
7
+ # git author settings. This hook checks that user.name and
8
+ # user.email are set before allowing commits.
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
+ NAME=$(git config user.name 2>/dev/null)
18
+ EMAIL=$(git config user.email 2>/dev/null)
19
+
20
+ if [ -z "$NAME" ] || [ -z "$EMAIL" ]; then
21
+ echo "WARNING: Git author not configured." >&2
22
+ [ -z "$NAME" ] && echo " Missing: git config user.name" >&2
23
+ [ -z "$EMAIL" ] && echo " Missing: git config user.email" >&2
24
+ fi
25
+
26
+ # Warn on common placeholder values
27
+ if echo "$EMAIL" | grep -qE '(example\.com|noreply|placeholder)' 2>/dev/null; then
28
+ echo "WARNING: Git email looks like a placeholder: $EMAIL" >&2
29
+ fi
30
+
31
+ exit 0
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # permission-cache.sh — Remember approved commands in session
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude asks permission for the same safe command repeatedly.
7
+ # This hook caches approved command patterns within a session,
8
+ # auto-approving on subsequent calls. Resets when the state
9
+ # file is deleted (new session).
10
+ #
11
+ # TRIGGER: PreToolUse MATCHER: "Bash"
12
+ #
13
+ # Only caches commands that match safe patterns (not destructive).
14
+ # ================================================================
15
+
16
+ COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
17
+ [ -z "$COMMAND" ] && exit 0
18
+
19
+ # Never cache destructive commands
20
+ echo "$COMMAND" | grep -qE '(rm\s+-rf|git\s+reset|git\s+clean|git\s+push.*--force|sudo|chmod\s+777)' && exit 0
21
+
22
+ STATE="/tmp/cc-permission-cache-$(echo "$PWD" | md5sum | cut -c1-8)"
23
+
24
+ # Normalize command (strip args that change, keep base command)
25
+ BASE=$(echo "$COMMAND" | awk '{print $1, $2}' | head -c 40)
26
+ HASH=$(echo "$BASE" | md5sum | cut -c1-12)
27
+
28
+ if grep -q "^$HASH$" "$STATE" 2>/dev/null; then
29
+ # Already approved in this session
30
+ echo "{\"decision\":\"approve\",\"reason\":\"Previously approved in this session\"}"
31
+ exit 0
32
+ fi
33
+
34
+ # Record for future calls (will be approved by the normal flow)
35
+ echo "$HASH" >> "$STATE" 2>/dev/null
36
+
37
+ 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.2.0",
3
+ "version": "10.4.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": {