cc-safe-setup 29.6.40 → 29.8.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.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +66 -0
  2. package/.claude-plugin/plugin.json +11 -0
  3. package/README.md +123 -12
  4. package/SETTINGS_REFERENCE.md +2 -0
  5. package/SKILL.md +47 -0
  6. package/examples/README.md +11 -1
  7. package/examples/auto-approve-compound-git.sh +3 -0
  8. package/examples/auto-compact-context-monitor.sh +35 -0
  9. package/examples/auto-mode-safety-enforcer.sh +57 -0
  10. package/examples/background-task-guard.sh +57 -0
  11. package/examples/broad-find-guard.sh +62 -0
  12. package/examples/cache-creation-spike-detector.sh +32 -0
  13. package/examples/case-insensitive-path-guard.sh +96 -0
  14. package/examples/cjk-punctuation-guard.sh +44 -0
  15. package/examples/clipboard-secret-guard.sh +29 -0
  16. package/examples/compact-circuit-breaker.sh +72 -0
  17. package/examples/context-size-alert.sh +38 -0
  18. package/examples/context-usage-drift-alert.sh +33 -0
  19. package/examples/dangerous-pip-flag-guard.sh +51 -0
  20. package/examples/deny-bypass-detector.sh +143 -0
  21. package/examples/dotenv-read-guard.sh +48 -0
  22. package/examples/dotfile-protection-guard.sh +60 -0
  23. package/examples/effort-tracking-logger.sh +30 -0
  24. package/examples/exploration-budget-guard.sh +77 -0
  25. package/examples/financial-operation-guard.sh +47 -0
  26. package/examples/full-rewrite-detector.sh +63 -0
  27. package/examples/home-critical-bash-guard.sh +56 -0
  28. package/examples/idle-session-cost-alert.sh +36 -0
  29. package/examples/model-version-alert.sh +18 -0
  30. package/examples/model-version-change-alert.sh +31 -0
  31. package/examples/move-delete-sequence-guard.sh +92 -0
  32. package/examples/pii-upload-guard.sh +72 -0
  33. package/examples/pr-duplicate-guard.sh +14 -0
  34. package/examples/production-port-kill-guard.sh +60 -0
  35. package/examples/quota-reset-cycle-monitor.sh +30 -0
  36. package/examples/repo-visibility-guard.sh +33 -0
  37. package/examples/sandbox-relative-path-audit.sh +51 -0
  38. package/examples/session-agent-cost-limiter.sh +43 -0
  39. package/examples/session-cost-alert.sh +62 -0
  40. package/examples/session-memory-watchdog.sh +9 -0
  41. package/examples/settings-integrity-monitor.sh +55 -0
  42. package/examples/settings-json-model-guard.sh +89 -0
  43. package/examples/shell-config-truncation-guard.sh +97 -0
  44. package/examples/shell-wrapper-guard.sh +4 -4
  45. package/examples/subagent-spawn-rate-monitor.sh +34 -0
  46. package/examples/subcommand-chain-guard.sh +44 -0
  47. package/examples/system-dir-protection-guard.sh +100 -0
  48. package/examples/thinking-display-enforcer.sh +25 -0
  49. package/examples/thinking-stall-detector.sh +61 -0
  50. package/examples/tool-retry-budget-guard.sh +59 -0
  51. package/examples/worktree-branch-pollution-detector.sh +35 -0
  52. package/examples/worktree-create-log.sh +6 -0
  53. package/examples/worktree-hook-linker.sh +72 -0
  54. package/examples/worktree-remove-uncommitted-guard.sh +20 -0
  55. package/hooks/hooks.json +60 -0
  56. package/index.mjs +92 -6
  57. package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
  58. package/package.json +2 -2
  59. package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
  60. package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
  61. package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
  62. package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
  63. package/skills/safety-setup/SKILL.md +47 -0
  64. package/tests/dotenv-read-guard.test.sh +65 -0
  65. package/tests/test-auto-mode-safety-enforcer.sh +55 -0
  66. package/tests/test-case-insensitive-path-guard.sh +78 -0
  67. package/tests/test-compact-circuit-breaker.sh +134 -0
  68. package/tests/test-context-usage-drift-alert.sh +52 -0
  69. package/tests/test-dangerous-pip-flag-guard.sh +56 -0
  70. package/tests/test-dotfile-protection-guard.sh +68 -0
  71. package/tests/test-effort-tracking-logger.sh +55 -0
  72. package/tests/test-exploration-budget-guard.sh +164 -0
  73. package/tests/test-financial-operation-guard.sh +59 -0
  74. package/tests/test-home-critical-bash-guard.sh +59 -0
  75. package/tests/test-model-version-change-alert.sh +55 -0
  76. package/tests/test-move-delete-sequence-guard.sh +63 -0
  77. package/tests/test-pr-duplicate-guard.sh +29 -0
  78. package/tests/test-quota-reset-cycle-monitor.sh +52 -0
  79. package/tests/test-shell-config-truncation-guard.sh +104 -0
  80. package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
  81. package/tests/test-system-dir-protection-guard.sh +81 -0
  82. package/tests/test-thinking-stall-detector.sh +151 -0
  83. package/tests/test-tool-retry-budget-guard.sh +75 -0
  84. package/tests/test-worktree-branch-pollution-detector.sh +50 -0
  85. package/tests/test-worktree-lifecycle-hooks.sh +29 -0
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "credential-guard",
3
+ "description": "Protect secrets and credentials from Claude Code. Blocks writes to .env files, detects API keys in shell commands, prevents hardcoded tokens, and guards service account JSON files.",
4
+ "version": "1.1.0",
5
+ "author": { "name": "yurukusa" },
6
+ "homepage": "https://yurukusa.github.io/cc-safe-setup/",
7
+ "repository": "https://github.com/yurukusa/cc-safe-setup",
8
+ "license": "MIT",
9
+ "hooks": {
10
+ "PreToolUse": [
11
+ {
12
+ "matcher": "Write",
13
+ "hooks": [
14
+ {
15
+ "type": "command",
16
+ "command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.env$|\\.env\\.|credentials|secret'; then echo \"BLOCKED: Writing to sensitive file: $FILE\" >&2; exit 2; fi"
17
+ }
18
+ ]
19
+ },
20
+ {
21
+ "matcher": "Edit",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.env$|\\.env\\.|credentials|secret'; then echo \"BLOCKED: Editing sensitive file: $FILE\" >&2; exit 2; fi"
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "matcher": "Bash",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE '(sk|pk|api|key|token|secret|password)[-_]?[a-zA-Z0-9]{20,}'; then echo 'WARNING: Possible API key or token detected in command. Verify no secrets are exposed.' >&2; fi"
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "matcher": "Write",
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE 'serviceaccount.*\\.json|key\\.json|credentials\\.json'; then echo \"BLOCKED: Writing to service account file: $FILE\" >&2; exit 2; fi"
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "matcher": "Bash",
49
+ "hooks": [
50
+ {
51
+ "type": "command",
52
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'ANTHROPIC_API_KEY|OPENAI_API_KEY|AWS_SECRET|GITHUB_TOKEN|DATABASE_URL'; then echo 'WARNING: Environment variable with potential secret detected in command.' >&2; fi"
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "git-protection",
3
+ "description": "Git safety hooks for Claude Code. Blocks force-push, protects main/master branch, prevents hard-reset, guards interactive rebase, and blocks git clean -fd.",
4
+ "version": "1.1.0",
5
+ "author": { "name": "yurukusa" },
6
+ "homepage": "https://yurukusa.github.io/cc-safe-setup/",
7
+ "repository": "https://github.com/yurukusa/cc-safe-setup",
8
+ "license": "MIT",
9
+ "hooks": {
10
+ "PreToolUse": [
11
+ {
12
+ "matcher": "Bash",
13
+ "hooks": [
14
+ {
15
+ "type": "command",
16
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push.*--force|git\\s+push.*-f\\b'; then echo 'BLOCKED: Force push. Use --force-with-lease for safer alternative.' >&2; exit 2; fi"
17
+ }
18
+ ]
19
+ },
20
+ {
21
+ "matcher": "Bash",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push\\s+(origin\\s+)?(main|master)\\b'; then echo 'BLOCKED: Direct push to main/master. Use a feature branch and PR.' >&2; exit 2; fi"
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "matcher": "Bash",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+reset\\s+--hard'; then echo 'BLOCKED: git reset --hard. Use git stash or git reset --soft.' >&2; exit 2; fi"
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "matcher": "Bash",
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+clean\\s+-fd|git\\s+clean\\s+-f'; then echo 'BLOCKED: git clean removes untracked files permanently. Review with git clean -n first.' >&2; exit 2; fi"
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "matcher": "Bash",
49
+ "hooks": [
50
+ {
51
+ "type": "command",
52
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+branch\\s+-D'; then echo 'BLOCKED: Force branch deletion. Use -d (safe delete) instead of -D.' >&2; exit 2; fi"
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "safety-essentials",
3
+ "description": "5 essential safety hooks for Claude Code. Blocks rm -rf, force-push, hard-reset, .env overwrites, and package publish. The minimum viable safety net from 800+ hours of autonomous operation.",
4
+ "version": "1.1.0",
5
+ "author": { "name": "yurukusa" },
6
+ "homepage": "https://yurukusa.github.io/cc-safe-setup/",
7
+ "repository": "https://github.com/yurukusa/cc-safe-setup",
8
+ "license": "MIT",
9
+ "hooks": {
10
+ "PreToolUse": [
11
+ {
12
+ "matcher": "Bash",
13
+ "hooks": [
14
+ {
15
+ "type": "command",
16
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'rm\\s+-r(f|F)|rm\\s+-(f|F)r|rm\\s+--force.*-r|rm\\s+-r.*--force'; then echo 'BLOCKED: rm -rf detected. Use git clean or manual deletion instead.' >&2; exit 2; fi"
17
+ }
18
+ ]
19
+ },
20
+ {
21
+ "matcher": "Bash",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push.*--force|git\\s+push.*-f\\b'; then echo 'BLOCKED: Force push detected. Use --force-with-lease or push normally.' >&2; exit 2; fi"
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "matcher": "Bash",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+reset\\s+--hard'; then echo 'BLOCKED: git reset --hard discards uncommitted changes. Use git stash instead.' >&2; exit 2; fi"
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "matcher": "Write",
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.env$|\\.env\\.'; then echo \"BLOCKED: Writing to environment file: $FILE\" >&2; exit 2; fi"
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "matcher": "Bash",
49
+ "hooks": [
50
+ {
51
+ "type": "command",
52
+ "command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'npm\\s+publish|yarn\\s+publish'; then echo 'BLOCKED: Package publish requires manual execution.' >&2; exit 2; fi"
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "token-guard",
3
+ "description": "Token consumption guards for Claude Code. Warns on large file reads (100KB+), limits unique file reads per session, estimates token budget, and caps subagent spawns. From 800+ hours of autonomous operation data.",
4
+ "version": "1.0.0",
5
+ "author": { "name": "yurukusa" },
6
+ "homepage": "https://yurukusa.github.io/cc-safe-setup/token-book.html",
7
+ "repository": "https://github.com/yurukusa/cc-safe-setup",
8
+ "license": "MIT",
9
+ "hooks": {
10
+ "PreToolUse": [
11
+ {
12
+ "matcher": "Read",
13
+ "hooks": [
14
+ {
15
+ "type": "command",
16
+ "command": "INPUT=$(cat); FP=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FP\" ] || [ ! -f \"$FP\" ] && exit 0; SZ=$(stat -c%s \"$FP\" 2>/dev/null || stat -f%z \"$FP\" 2>/dev/null); [ \"$SZ\" -gt 102400 ] 2>/dev/null && echo \"Warning: $(basename $FP) is $((SZ/1024))KB. Use limit parameter to read only what you need.\" || true"
17
+ }
18
+ ]
19
+ },
20
+ {
21
+ "matcher": "Read",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "BUDGET=${CC_READ_BUDGET:-100}; WARN=${CC_READ_WARN:-50}; T=/tmp/cc-read-budget-$PPID; INPUT=$(cat); FP=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FP\" ] && exit 0; C=0; if [ -f \"$T\" ]; then grep -qF \"$FP\" \"$T\" || echo \"$FP\" >> \"$T\"; C=$(wc -l < \"$T\"); else echo \"$FP\" > \"$T\"; C=1; fi; [ \"$C\" -ge \"$BUDGET\" ] && { echo \"[BLOCK] Read budget reached (${BUDGET} files). Use Glob/Grep to narrow down.\"; exit 2; }; [ \"$C\" -ge \"$WARN\" ] && echo \"Warning: ${C}/${BUDGET} files read.\""
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "matcher": "Agent",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "MAX=${CC_MAX_AGENTS:-3}; T=/tmp/cc-agents-$PPID; C=0; [ -f \"$T\" ] && C=$(cat \"$T\"); C=$((C+1)); echo $C > \"$T\"; [ $C -gt $MAX ] && { echo \"[BLOCK] Subagent limit (${MAX}). Complete existing agents first.\"; exit 2; }; [ $C -ge $MAX ] && echo \"Warning: ${C}/${MAX} subagents spawned.\""
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ "PostToolUse": [
40
+ {
41
+ "matcher": {},
42
+ "hooks": [
43
+ {
44
+ "type": "command",
45
+ "command": "WARN=${CC_TOKEN_BUDGET:-50000}; BLOCK=${CC_TOKEN_BLOCK:-100000}; T=/tmp/cc-tokens-$PPID; INPUT=$(cat); SZ=${#INPUT}; TK=$((SZ/4)); TOTAL=0; [ -f \"$T\" ] && TOTAL=$(cat \"$T\"); TOTAL=$((TOTAL+TK)); echo $TOTAL > \"$T\"; [ $TOTAL -ge $BLOCK ] && { echo \"[BLOCK] Token budget exceeded (~${TOTAL}). Run /compact.\"; exit 2; }; [ $TOTAL -ge $WARN ] && echo \"Warning: ~${TOTAL} tokens consumed (limit: ${BLOCK}).\""
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ }
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: cc-safe-setup
3
+ description: Safety hooks for Claude Code — 695 pre-built hooks that prevent file deletion, credential leaks, git disasters, and token waste during autonomous AI coding sessions. Install with npx cc-safe-setup.
4
+ ---
5
+
6
+ # cc-safe-setup
7
+
8
+ Safety-first configuration for Claude Code. Prevents the accidents that happen when AI writes code autonomously.
9
+
10
+ ## What it does
11
+
12
+ Installs pre-built safety hooks into your Claude Code environment. These hooks run automatically before/after tool calls to block dangerous operations.
13
+
14
+ **Categories:**
15
+ - **File protection**: Block `rm -rf`, prevent overwriting files outside project
16
+ - **Git safety**: Prevent force-push to main, block `reset --hard`
17
+ - **Credential guards**: Stop `.env` files from being committed or read by AI
18
+ - **Token optimization**: Warn on large file reads, limit subagent spawning
19
+ - **Quality gates**: Detect lazy rewrites, verify claims before committing
20
+
21
+ ## Quick start
22
+
23
+ ```bash
24
+ npx cc-safe-setup
25
+ ```
26
+
27
+ This runs an interactive wizard that configures hooks based on your risk profile.
28
+
29
+ ## Install individual hooks
30
+
31
+ ```bash
32
+ npx cc-safe-setup --install-example large-read-guard
33
+ npx cc-safe-setup --install-example prevent-rm-rf
34
+ npx cc-safe-setup --install-example git-force-push-block
35
+ ```
36
+
37
+ ## Why hooks instead of CLAUDE.md rules
38
+
39
+ Rules in CLAUDE.md are suggestions — Claude can forget them. Hooks are enforced at the system level. A hook that blocks `rm -rf` cannot be overridden by the AI.
40
+
41
+ From 800+ hours of autonomous operation: the hooks that matter most are the ones you don't notice until something goes wrong.
42
+
43
+ ## Resources
44
+
45
+ - Repository: https://github.com/yurukusa/cc-safe-setup
46
+ - Hook Selector (find hooks for your setup): https://yurukusa.github.io/cc-safe-setup/hook-selector.html
47
+ - Token Checkup (diagnose waste): https://yurukusa.github.io/cc-safe-setup/token-checkup.html
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # Tests for dotenv-read-guard.sh
3
+ HOOK="$(dirname "$0")/../examples/dotenv-read-guard.sh"
4
+ PASS=0; FAIL=0
5
+
6
+ run_test() {
7
+ local desc="$1" input="$2" expect="$3"
8
+ result=$(echo "$input" | bash "$HOOK" 2>/dev/null; echo $?)
9
+ code=$(echo "$result" | tail -1)
10
+ if [ "$code" = "$expect" ]; then
11
+ echo "PASS: $desc"
12
+ ((PASS++))
13
+ else
14
+ echo "FAIL: $desc (expected $expect, got $code)"
15
+ ((FAIL++))
16
+ fi
17
+ }
18
+
19
+ # Should block .env files
20
+ run_test "Block .env" \
21
+ '{"tool_input":{"file_path":"/home/user/project/.env"}}' "2"
22
+
23
+ run_test "Block .env.local" \
24
+ '{"tool_input":{"file_path":"/app/.env.local"}}' "2"
25
+
26
+ run_test "Block .env.production" \
27
+ '{"tool_input":{"file_path":"/deploy/.env.production"}}' "2"
28
+
29
+ run_test "Block .env.staging" \
30
+ '{"tool_input":{"file_path":"/app/.env.staging"}}' "2"
31
+
32
+ run_test "Block .env.development" \
33
+ '{"tool_input":{"file_path":"/app/.env.development"}}' "2"
34
+
35
+ run_test "Block .env.test" \
36
+ '{"tool_input":{"file_path":"/project/.env.test"}}' "2"
37
+
38
+ # Should allow non-.env files
39
+ run_test "Allow .env.example" \
40
+ '{"tool_input":{"file_path":"/project/.env.example"}}' "0"
41
+
42
+ run_test "Allow README.md" \
43
+ '{"tool_input":{"file_path":"/project/README.md"}}' "0"
44
+
45
+ run_test "Allow package.json" \
46
+ '{"tool_input":{"file_path":"/project/package.json"}}' "0"
47
+
48
+ run_test "Allow config.ts" \
49
+ '{"tool_input":{"file_path":"/src/config.ts"}}' "0"
50
+
51
+ run_test "Allow env.ts (not dotenv)" \
52
+ '{"tool_input":{"file_path":"/src/env.ts"}}' "0"
53
+
54
+ run_test "Allow .envrc (direnv)" \
55
+ '{"tool_input":{"file_path":"/project/.envrc"}}' "0"
56
+
57
+ # Edge cases
58
+ run_test "Empty input" '{}' "0"
59
+
60
+ run_test "No file_path" \
61
+ '{"tool_input":{}}' "0"
62
+
63
+ echo ""
64
+ echo "Results: $PASS passed, $FAIL failed"
65
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # Tests for auto-mode-safety-enforcer.sh
3
+ set -euo pipefail
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ HOOK="$(dirname "$0")/../examples/auto-mode-safety-enforcer.sh"
8
+
9
+ test_hook() {
10
+ local input="$1" expected_exit="$2" desc="$3"
11
+ local actual_exit=0
12
+ echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
13
+ if [ "$actual_exit" -eq "$expected_exit" ]; then
14
+ echo " PASS: $desc"
15
+ PASS=$((PASS + 1))
16
+ else
17
+ echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
18
+ FAIL=$((FAIL + 1))
19
+ fi
20
+ }
21
+
22
+ echo "auto-mode-safety-enforcer.sh tests"
23
+ echo ""
24
+
25
+ # --- Block: Critical rm operations ---
26
+ test_hook '{"tool_input":{"command":"rm -rf /"}}' 2 "Block rm -rf /"
27
+ test_hook '{"tool_input":{"command":"rm -rf ~"}}' 2 "Block rm -rf ~"
28
+ test_hook '{"tool_input":{"command":"rm -rf ~/."}}' 2 "Block rm -rf ~/."
29
+ test_hook '{"tool_input":{"command":"sudo rm -rf /home"}}' 2 "Block sudo rm -rf /home"
30
+ test_hook '{"tool_input":{"command":"rm -rf /etc"}}' 2 "Block rm -rf /etc"
31
+ test_hook '{"tool_input":{"command":"rm -rf /usr"}}' 2 "Block rm -rf /usr"
32
+ test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.ssh\"}}" 2 "Block rm -rf ~/.ssh"
33
+ test_hook "{\"tool_input\":{\"command\":\"rm $HOME/.git-credentials\"}}" 2 "Block rm ~/.git-credentials"
34
+ test_hook "{\"tool_input\":{\"command\":\"rm -f $HOME/.bashrc\"}}" 2 "Block rm ~/.bashrc"
35
+
36
+ # --- Block: Disk operations ---
37
+ test_hook '{"tool_input":{"command":"sudo dd if=/dev/zero of=/dev/sda"}}' 2 "Block dd to disk"
38
+ test_hook '{"tool_input":{"command":"sudo mkfs.ext4 /dev/sda1"}}' 2 "Block mkfs"
39
+ test_hook '{"tool_input":{"command":"sudo fdisk /dev/sda"}}' 2 "Block fdisk"
40
+
41
+ # --- Block: System process kill ---
42
+ test_hook '{"tool_input":{"command":"kill -9 1"}}' 2 "Block kill PID 1"
43
+ test_hook '{"tool_input":{"command":"killall systemd"}}' 2 "Block killall systemd"
44
+
45
+ # --- Allow: Safe operations ---
46
+ test_hook '{"tool_input":{"command":"rm -rf node_modules"}}' 0 "Allow rm node_modules"
47
+ test_hook '{"tool_input":{"command":"rm /tmp/test.txt"}}' 0 "Allow rm in /tmp"
48
+ test_hook '{"tool_input":{"command":"ls -la"}}' 0 "Allow ls"
49
+ test_hook '{"tool_input":{"command":"git status"}}' 0 "Allow git"
50
+ test_hook '{"tool_input":{"command":"npm install"}}' 0 "Allow npm install"
51
+ test_hook '{}' 0 "Allow empty input"
52
+
53
+ echo ""
54
+ echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
55
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,78 @@
1
+ #!/bin/bash
2
+ # Tests for case-insensitive-path-guard.sh
3
+ # Run: bash tests/test-case-insensitive-path-guard.sh
4
+ # NOTE: Full case-mismatch detection only works on macOS APFS.
5
+ # On Linux, the hook exits 0 for all inputs (no case-insensitive FS).
6
+ # These tests verify the non-macOS path (exit 0) and input parsing.
7
+ set -euo pipefail
8
+
9
+ PASS=0
10
+ FAIL=0
11
+ HOOK="$(dirname "$0")/../examples/case-insensitive-path-guard.sh"
12
+
13
+ test_hook() {
14
+ local input="$1" expected_exit="$2" desc="$3"
15
+ local actual_exit=0
16
+ echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
17
+ if [ "$actual_exit" -eq "$expected_exit" ]; then
18
+ echo " PASS: $desc"
19
+ PASS=$((PASS + 1))
20
+ else
21
+ echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
22
+ FAIL=$((FAIL + 1))
23
+ fi
24
+ }
25
+
26
+ echo "case-insensitive-path-guard.sh tests"
27
+ echo ""
28
+
29
+ # On Linux, all commands should pass through (exit 0)
30
+ # The hook only activates on macOS (uname == Darwin)
31
+ IS_LINUX=0
32
+ [ "$(uname)" != "Darwin" ] && IS_LINUX=1
33
+
34
+ if [ "$IS_LINUX" -eq 1 ]; then
35
+ echo "Running on Linux — all tests should pass through (exit 0)"
36
+ echo ""
37
+
38
+ # --- Pass-through on Linux ---
39
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~/Projects"}}' 0 "Linux: rm -rf passes through"
40
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf ~/Documents"}}' 0 "Linux: rm Documents passes through"
41
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"mv ~/old ~/new"}}' 0 "Linux: mv passes through"
42
+
43
+ # --- Non-destructive commands always pass ---
44
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"ls ~/Projects"}}' 0 "Linux: ls passes through"
45
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"git status"}}' 0 "Linux: git status passes through"
46
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"echo hello"}}' 0 "Linux: echo passes through"
47
+
48
+ # --- Safe paths always pass ---
49
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf node_modules"}}' 0 "Linux: rm node_modules passes"
50
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/test"}}' 0 "Linux: rm /tmp passes"
51
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf .cache"}}' 0 "Linux: rm .cache passes"
52
+
53
+ # --- Empty/missing inputs ---
54
+ test_hook '{"tool_name":"Bash","tool_input":{"command":""}}' 0 "Empty command"
55
+ test_hook '{"tool_name":"Bash","tool_input":{}}' 0 "No command"
56
+ test_hook '{}' 0 "Empty JSON"
57
+
58
+ else
59
+ echo "Running on macOS — testing case-mismatch detection"
60
+ echo ""
61
+
62
+ # On macOS, safe paths should still pass
63
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf node_modules"}}' 0 "macOS: rm node_modules passes"
64
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/test"}}' 0 "macOS: rm /tmp passes"
65
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"rm -rf __pycache__"}}' 0 "macOS: rm __pycache__ passes"
66
+
67
+ # Non-destructive commands pass
68
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"ls ~/Projects"}}' 0 "macOS: ls passes through"
69
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"git status"}}' 0 "macOS: git status passes"
70
+
71
+ # Empty inputs
72
+ test_hook '{"tool_name":"Bash","tool_input":{"command":""}}' 0 "Empty command"
73
+ test_hook '{"tool_name":"Bash","tool_input":{}}' 0 "No command"
74
+ fi
75
+
76
+ echo ""
77
+ echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL))"
78
+ [ "$FAIL" -eq 0 ] && echo "ALL TESTS PASSED" || exit 1
@@ -0,0 +1,134 @@
1
+ #!/bin/bash
2
+ # Tests for compact-circuit-breaker.sh
3
+ set -uo pipefail
4
+
5
+ HOOK="$(dirname "$0")/../examples/compact-circuit-breaker.sh"
6
+ STATE_DIR="/tmp/.cc-compact-circuit-breaker"
7
+ STATE_FILE="$STATE_DIR/compaction-log"
8
+ PASS=0; FAIL=0; TOTAL=0
9
+
10
+ run_test() {
11
+ local desc="$1"; shift
12
+ TOTAL=$((TOTAL + 1))
13
+ if "$@" 2>/dev/null; then
14
+ PASS=$((PASS + 1))
15
+ echo "✅ $desc"
16
+ else
17
+ local code=$?
18
+ if [ "$code" -eq 2 ]; then
19
+ # exit 2 = block, might be expected
20
+ FAIL=$((FAIL + 1))
21
+ echo "❌ $desc (exit $code)"
22
+ else
23
+ FAIL=$((FAIL + 1))
24
+ echo "❌ $desc (exit $code)"
25
+ fi
26
+ fi
27
+ }
28
+
29
+ run_test_blocked() {
30
+ local desc="$1"; shift
31
+ TOTAL=$((TOTAL + 1))
32
+ local code=0
33
+ "$@" 2>/dev/null || code=$?
34
+ if [ "$code" -eq 2 ]; then
35
+ PASS=$((PASS + 1))
36
+ echo "✅ $desc (correctly blocked)"
37
+ else
38
+ FAIL=$((FAIL + 1))
39
+ echo "❌ $desc (expected block, got exit $code)"
40
+ fi
41
+ }
42
+
43
+ cleanup() {
44
+ rm -rf "$STATE_DIR"
45
+ mkdir -p "$STATE_DIR"
46
+ }
47
+
48
+ # Test 1: First compaction should be allowed
49
+ cleanup
50
+ run_test "First compaction allowed" bash "$HOOK"
51
+
52
+ # Test 2: Second compaction within MIN_INTERVAL should be blocked (cooldown)
53
+ run_test_blocked "Cooldown blocks rapid compaction" bash "$HOOK"
54
+
55
+ # Test 3: After cooldown, compaction should be allowed
56
+ cleanup
57
+ echo "$(($(date +%s) - 200))" > "$STATE_FILE"
58
+ run_test "Compaction allowed after cooldown" bash "$HOOK"
59
+
60
+ # Test 4: Circuit breaker triggers after MAX_PER_HOUR
61
+ cleanup
62
+ NOW=$(date +%s)
63
+ for i in $(seq 1 3); do
64
+ echo "$((NOW - 300 + i * 10))" >> "$STATE_FILE"
65
+ done
66
+ run_test_blocked "Circuit breaker blocks after 3 compactions" bash "$HOOK"
67
+
68
+ # Test 5: Old entries are cleaned up
69
+ cleanup
70
+ ONE_HOUR_AGO=$(($(date +%s) - 3700))
71
+ for i in $(seq 1 5); do
72
+ echo "$((ONE_HOUR_AGO - i * 10))" >> "$STATE_FILE"
73
+ done
74
+ run_test "Old entries cleaned, compaction allowed" bash "$HOOK"
75
+
76
+ # Test 6: Custom MAX_PER_HOUR
77
+ cleanup
78
+ NOW=$(date +%s)
79
+ echo "$((NOW - 200))" > "$STATE_FILE"
80
+ run_test_blocked "Custom MAX_PER_HOUR=1 blocks second" env CC_COMPACT_MAX_PER_HOUR=1 bash "$HOOK"
81
+
82
+ # Test 7: Custom MIN_INTERVAL
83
+ cleanup
84
+ echo "$(date +%s)" > "$STATE_FILE"
85
+ run_test_blocked "Default MIN_INTERVAL blocks immediate retry" bash "$HOOK"
86
+
87
+ # Test 8: State directory created if missing
88
+ rm -rf "$STATE_DIR"
89
+ run_test "Creates state directory" bash "$HOOK"
90
+ [ -d "$STATE_DIR" ] && echo " ↳ State directory exists ✅" || echo " ↳ State directory missing ❌"
91
+
92
+ # Test 9: Empty state file handled
93
+ cleanup
94
+ mkdir -p "$STATE_DIR"
95
+ touch "$STATE_FILE"
96
+ run_test "Empty state file handled" bash "$HOOK"
97
+
98
+ # Test 10: Mixed old and new entries
99
+ cleanup
100
+ NOW=$(date +%s)
101
+ echo "$((NOW - 7200))" >> "$STATE_FILE" # 2 hours ago (old)
102
+ echo "$((NOW - 7100))" >> "$STATE_FILE" # old
103
+ echo "$((NOW - 200))" >> "$STATE_FILE" # recent (1)
104
+ run_test "Mixed entries: old cleaned, recent counted" bash "$HOOK"
105
+
106
+ # Test 11: Exactly at MAX_PER_HOUR boundary
107
+ cleanup
108
+ NOW=$(date +%s)
109
+ echo "$((NOW - 1800))" >> "$STATE_FILE"
110
+ echo "$((NOW - 900))" >> "$STATE_FILE"
111
+ echo "$((NOW - 200))" >> "$STATE_FILE"
112
+ run_test_blocked "Exactly at MAX=3 boundary blocked" bash "$HOOK"
113
+
114
+ # Test 12: Error message content
115
+ cleanup
116
+ NOW=$(date +%s)
117
+ for i in $(seq 1 3); do
118
+ echo "$((NOW - 300 + i * 10))" >> "$STATE_FILE"
119
+ done
120
+ OUTPUT=$(bash "$HOOK" 2>&1 || true)
121
+ TOTAL=$((TOTAL + 1))
122
+ if echo "$OUTPUT" | grep -q "CIRCUIT BREAKER"; then
123
+ PASS=$((PASS + 1))
124
+ echo "✅ Error message contains CIRCUIT BREAKER"
125
+ else
126
+ FAIL=$((FAIL + 1))
127
+ echo "❌ Error message missing CIRCUIT BREAKER: $OUTPUT"
128
+ fi
129
+
130
+ cleanup
131
+
132
+ echo ""
133
+ echo "Results: $PASS/$TOTAL passed, $FAIL failed"
134
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # Tests for context-usage-drift-alert.sh
3
+ HOOK="examples/context-usage-drift-alert.sh"
4
+ PASS=0 FAIL=0
5
+
6
+ assert_contains() { if echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (expected '$3')"; fi; }
7
+ assert_not_contains() { if ! echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (unexpected '$3')"; fi; }
8
+
9
+ # Use a test-specific counter file
10
+ COUNTER_FILE="/tmp/cc-context-usage-counter-$(date +%Y%m%d)"
11
+ rm -f "$COUNTER_FILE"
12
+
13
+ # Test 1: Calls 1-49 should not warn
14
+ for i in $(seq 1 49); do
15
+ echo '{}' | bash "$HOOK" 2>/dev/null
16
+ done
17
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
18
+ # Call 50 should give checkpoint
19
+ assert_contains "call 50 should checkpoint" "$OUT" "checkpoint"
20
+ assert_contains "should mention /cost" "$OUT" "/cost"
21
+
22
+ # Test 2: Calls 51-99 should not warn
23
+ for i in $(seq 51 99); do
24
+ echo '{}' | bash "$HOOK" 2>/dev/null
25
+ done
26
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
27
+ # Call 100 should give strong warning
28
+ assert_contains "call 100 should warn high" "$OUT" "HIGH CONTEXT"
29
+ assert_contains "should mention /compact" "$OUT" "/compact"
30
+ assert_contains "should reference issue" "$OUT" "#50204"
31
+
32
+ # Test 3: Calls 101-149
33
+ for i in $(seq 101 149); do
34
+ echo '{}' | bash "$HOOK" 2>/dev/null
35
+ done
36
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
37
+ # Call 150 should give critical warning
38
+ assert_contains "call 150 critical warning" "$OUT" "VERY HIGH"
39
+ assert_contains "should mention saving state" "$OUT" "Save"
40
+
41
+ # Test 4: Normal calls between thresholds should be silent
42
+ rm -f "$COUNTER_FILE"
43
+ echo "10" > "$COUNTER_FILE"
44
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
45
+ assert_not_contains "non-threshold call should be silent" "$OUT" "checkpoint"
46
+ assert_not_contains "non-threshold no warning" "$OUT" "HIGH"
47
+
48
+ # Cleanup
49
+ rm -f "$COUNTER_FILE"
50
+
51
+ echo "Results: $PASS passed, $FAIL failed"
52
+ [ "$FAIL" -eq 0 ]