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,52 @@
1
+ #!/bin/bash
2
+ # Tests for quota-reset-cycle-monitor.sh
3
+ HOOK="examples/quota-reset-cycle-monitor.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
+ assert_exit() { if [ "$2" -eq "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (exit $2, expected $3)"; fi; }
9
+
10
+ HISTORY="/tmp/cc-quota-reset-history"
11
+ rm -f "$HISTORY"
12
+
13
+ # Test 1: First run creates history
14
+ OUT=$(bash "$HOOK" 2>&1)
15
+ RC=$?
16
+ assert_exit "exit 0" "$RC" 0
17
+ if [ -f "$HISTORY" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: history not created"; fi
18
+
19
+ # Test 2: History contains today's date
20
+ TODAY=$(date +%Y-%m-%d)
21
+ CONTENT=$(cat "$HISTORY")
22
+ assert_contains "has today's date" "$CONTENT" "$TODAY"
23
+
24
+ # Test 3: Second run same day — no duplicate entry
25
+ bash "$HOOK" 2>/dev/null
26
+ LINES=$(wc -l < "$HISTORY")
27
+ if [ "$LINES" -eq 1 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: duplicate entry ($LINES lines)"; fi
28
+
29
+ # Test 4: History format is correct (date|weekday)
30
+ WEEKDAY=$(date +%u)
31
+ assert_contains "correct format" "$CONTENT" "$TODAY|$WEEKDAY"
32
+
33
+ # Test 5: With 7+ entries, should give info message
34
+ rm -f "$HISTORY"
35
+ for i in $(seq 1 7); do
36
+ echo "2026-04-$(printf '%02d' $((i+10)))|$i" >> "$HISTORY"
37
+ done
38
+ # Remove today's entry so the hook will run
39
+ sed -i "/$TODAY/d" "$HISTORY"
40
+ OUT=$(bash "$HOOK" 2>&1)
41
+ assert_contains "7+ entries shows info" "$OUT" "tracking"
42
+ assert_contains "references issue" "$OUT" "#49599"
43
+
44
+ # Test 6: Exit code always 0
45
+ RC=$?
46
+ assert_exit "always exit 0" "$RC" 0
47
+
48
+ # Cleanup
49
+ rm -f "$HISTORY"
50
+
51
+ echo "quota-reset-cycle-monitor: $PASS passed, $FAIL failed"
52
+ exit $FAIL
@@ -0,0 +1,104 @@
1
+ #!/bin/bash
2
+ # Tests for shell-config-truncation-guard.sh
3
+ # Run: bash tests/test-shell-config-truncation-guard.sh
4
+ set -euo pipefail
5
+
6
+ PASS=0
7
+ FAIL=0
8
+ HOOK="$(dirname "$0")/../examples/shell-config-truncation-guard.sh"
9
+
10
+ # Create temp dir for test files
11
+ TEST_DIR=$(mktemp -d)
12
+ trap 'rm -rf "$TEST_DIR"' EXIT
13
+
14
+ # Create a fake home with shell config files
15
+ export HOME="$TEST_DIR"
16
+ echo '# My bash profile
17
+ export PATH="$HOME/bin:$PATH"
18
+ export EDITOR=vim
19
+ alias ll="ls -la"
20
+ eval "$(pyenv init -)"
21
+ source ~/.bash_completion
22
+ ' > "$TEST_DIR/.bash_profile"
23
+
24
+ echo '# My zshrc
25
+ export PATH="$HOME/bin:$PATH"
26
+ autoload -Uz compinit
27
+ compinit
28
+ source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
29
+ ' > "$TEST_DIR/.zshrc"
30
+
31
+ echo '# My bashrc
32
+ if [ -f /etc/bashrc ]; then . /etc/bashrc; fi
33
+ export PATH="$HOME/bin:$PATH"
34
+ ' > "$TEST_DIR/.bashrc"
35
+
36
+ test_hook() {
37
+ local input="$1" expected_exit="$2" desc="$3"
38
+ local actual_exit=0
39
+ echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
40
+ if [ "$actual_exit" -eq "$expected_exit" ]; then
41
+ echo " PASS: $desc"
42
+ PASS=$((PASS + 1))
43
+ else
44
+ echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
45
+ FAIL=$((FAIL + 1))
46
+ fi
47
+ }
48
+
49
+ echo "shell-config-truncation-guard.sh tests"
50
+ echo ""
51
+
52
+ # --- Block: Write tool truncating to empty ---
53
+ echo "= Write tool: truncation blocking ="
54
+ test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"\"}}" 2 "Block empty Write to .bash_profile"
55
+ test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.zshrc\",\"content\":\"\"}}" 2 "Block empty Write to .zshrc"
56
+ test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bashrc\",\"content\":\"\"}}" 2 "Block empty Write to .bashrc"
57
+
58
+ # --- Block: Write tool with near-empty content ---
59
+ test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"#\n\"}}" 2 "Block near-empty Write to .bash_profile"
60
+
61
+ # --- Block: Write tool with >60% size reduction ---
62
+ test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"export PATH\"}}" 2 "Block >60% reduction of .bash_profile"
63
+
64
+ # --- Allow: Write tool with reasonable content ---
65
+ test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.bash_profile\",\"content\":\"# Updated bash profile\nexport PATH=\\\"$TEST_DIR/bin:\$PATH\\\"\nexport EDITOR=vim\nalias ll=\\\"ls -la\\\"\neval \\\"\$(pyenv init -)\\\"\nsource ~/.bash_completion\n# Added new alias\nalias gs=\\\"git status\\\"\n\"}}" 0 "Allow reasonable Write to .bash_profile"
66
+
67
+ # --- Allow: Write to unprotected files ---
68
+ test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test.txt","content":""}}' 0 "Allow empty Write to unprotected file"
69
+ test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/config.sh","content":"echo hello"}}' 0 "Allow Write to unprotected config"
70
+
71
+ # --- Block: Bash truncation commands ---
72
+ echo ""
73
+ echo "= Bash tool: truncation blocking ="
74
+ test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"> $TEST_DIR/.bashrc\"}}" 2 "Block > redirect truncation of .bashrc"
75
+ test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"truncate -s 0 $TEST_DIR/.zshrc\"}}" 2 "Block truncate command on .zshrc"
76
+ test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\": > $TEST_DIR/.bash_profile\"}}" 2 "Block : > truncation of .bash_profile"
77
+
78
+ # --- Allow: Bash reading shell config ---
79
+ test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"cat $TEST_DIR/.bashrc\"}}" 0 "Allow cat .bashrc"
80
+ test_hook "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"grep PATH $TEST_DIR/.zshrc\"}}" 0 "Allow grep .zshrc"
81
+
82
+ # --- Allow: Bash with unrelated commands ---
83
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"echo hello"}}' 0 "Allow echo"
84
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' 0 "Allow ls"
85
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"git status"}}' 0 "Allow git status"
86
+
87
+ # --- Allow: Non-matching tools ---
88
+ echo ""
89
+ echo "= Non-matching tools ="
90
+ test_hook '{"tool_name":"Read","tool_input":{"file_path":"/tmp/test"}}' 0 "Allow Read tool"
91
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test"}}' 0 "Allow Edit tool"
92
+
93
+ # --- Edge cases ---
94
+ echo ""
95
+ echo "= Edge cases ="
96
+ test_hook '{}' 0 "Handle empty input"
97
+ test_hook '{"tool_name":"Bash","tool_input":{"command":""}}' 0 "Handle empty command"
98
+
99
+ # Write to a file that doesn't exist yet (should allow)
100
+ test_hook "{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$TEST_DIR/.new_profile\",\"content\":\"\"}}" 0 "Allow Write to non-existent profile"
101
+
102
+ echo ""
103
+ echo "Results: $PASS passed, $FAIL failed"
104
+ [ "$FAIL" -eq 0 ] && echo "ALL TESTS PASSED" || exit 1
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # Tests for subagent-spawn-rate-monitor.sh
3
+ HOOK="examples/subagent-spawn-rate-monitor.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
+ # Cleanup state
10
+ rm -f /tmp/cc-subagent-spawn-counter /tmp/cc-subagent-spawn-window
11
+
12
+ # Test 1: First spawn should not warn
13
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
14
+ assert_not_contains "first spawn no warn" "$OUT" "HIGH SUBAGENT"
15
+
16
+ # Test 2-5: Spawns 2-5 should not warn
17
+ for i in 2 3 4 5; do
18
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
19
+ assert_not_contains "spawn $i no warn" "$OUT" "HIGH SUBAGENT"
20
+ done
21
+
22
+ # Test 6: 6th spawn (>5 threshold) should warn
23
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
24
+ assert_contains "6th spawn should warn" "$OUT" "HIGH SUBAGENT"
25
+ assert_contains "should mention token cost" "$OUT" "4.7K"
26
+ assert_contains "should reference issue" "$OUT" "#50213"
27
+
28
+ # Test 7: Reset counter, verify no warning after reset
29
+ rm -f /tmp/cc-subagent-spawn-counter /tmp/cc-subagent-spawn-window
30
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
31
+ assert_not_contains "after reset no warn" "$OUT" "HIGH SUBAGENT"
32
+
33
+ # Test 8: Window expiry reset
34
+ echo "1" > /tmp/cc-subagent-spawn-counter
35
+ echo "$(($(date +%s) - 400))" > /tmp/cc-subagent-spawn-window
36
+ OUT=$(echo '{}' | bash "$HOOK" 2>&1)
37
+ assert_not_contains "expired window resets count" "$OUT" "HIGH SUBAGENT"
38
+
39
+ # Cleanup
40
+ rm -f /tmp/cc-subagent-spawn-counter /tmp/cc-subagent-spawn-window
41
+
42
+ echo "Results: $PASS passed, $FAIL failed"
43
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # Tests for system-dir-protection-guard.sh
3
+ # Run: bash tests/test-system-dir-protection-guard.sh
4
+ set -euo pipefail
5
+
6
+ PASS=0
7
+ FAIL=0
8
+ HOOK="$(dirname "$0")/../examples/system-dir-protection-guard.sh"
9
+
10
+ test_hook() {
11
+ local input="$1" expected_exit="$2" desc="$3"
12
+ local actual_exit=0
13
+ echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
14
+ if [ "$actual_exit" -eq "$expected_exit" ]; then
15
+ echo " PASS: $desc"
16
+ PASS=$((PASS + 1))
17
+ else
18
+ echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
19
+ FAIL=$((FAIL + 1))
20
+ fi
21
+ }
22
+
23
+ echo "system-dir-protection-guard.sh tests"
24
+ echo ""
25
+
26
+ # --- Block: rm on system directories ---
27
+ test_hook '{"tool_input":{"command":"rm -rf /etc"}}' 2 "Block rm -rf /etc"
28
+ test_hook '{"tool_input":{"command":"rm -rf /usr"}}' 2 "Block rm -rf /usr"
29
+ test_hook '{"tool_input":{"command":"rm -rf /var"}}' 2 "Block rm -rf /var"
30
+ test_hook '{"tool_input":{"command":"rm -rf /opt"}}' 2 "Block rm -rf /opt"
31
+ test_hook '{"tool_input":{"command":"rm -rf /boot"}}' 2 "Block rm -rf /boot"
32
+ test_hook '{"tool_input":{"command":"rm -rf /root"}}' 2 "Block rm -rf /root"
33
+ test_hook '{"tool_input":{"command":"rm -rf /srv"}}' 2 "Block rm -rf /srv"
34
+ test_hook '{"tool_input":{"command":"rm -rf /home"}}' 2 "Block rm -rf /home"
35
+ test_hook '{"tool_input":{"command":"rm -rf /home/username"}}' 2 "Block rm -rf /home/username"
36
+ test_hook '{"tool_input":{"command":"sudo rm -rf /etc/nginx"}}' 2 "Block sudo rm -rf /etc/nginx"
37
+ test_hook '{"tool_input":{"command":"rm -rf /usr/local"}}' 2 "Block rm -rf /usr/local"
38
+
39
+ # --- Block: rm on critical home directories ---
40
+ test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.ssh\"}}" 2 "Block rm -rf ~/.ssh"
41
+ test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.config\"}}" 2 "Block rm -rf ~/.config"
42
+ test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.local\"}}" 2 "Block rm -rf ~/.local"
43
+ test_hook "{\"tool_input\":{\"command\":\"rm -rf $HOME/.gnupg\"}}" 2 "Block rm -rf ~/.gnupg"
44
+
45
+ # --- Block: mv on system directories (#49554) ---
46
+ test_hook '{"tool_input":{"command":"mv /etc /tmp/etc_backup"}}' 2 "Block mv /etc"
47
+ test_hook '{"tool_input":{"command":"mv /usr/local /tmp/"}}' 2 "Block mv /usr/local"
48
+ test_hook '{"tool_input":{"command":"mv /home/user /tmp/"}}' 2 "Block mv /home/user"
49
+ test_hook '{"tool_input":{"command":"sudo mv /var/lib /tmp/"}}' 2 "Block sudo mv /var/lib"
50
+ test_hook "{\"tool_input\":{\"command\":\"mv $HOME/.ssh /tmp/\"}}" 2 "Block mv ~/.ssh"
51
+
52
+ # --- Block: chmod -R on system directories ---
53
+ test_hook '{"tool_input":{"command":"chmod -R 777 /etc"}}' 2 "Block chmod -R 777 /etc"
54
+ test_hook '{"tool_input":{"command":"sudo chmod -R 755 /usr"}}' 2 "Block sudo chmod -R /usr"
55
+
56
+ # --- Block: chown -R on system directories ---
57
+ test_hook '{"tool_input":{"command":"chown -R user:user /var"}}' 2 "Block chown -R /var"
58
+ test_hook '{"tool_input":{"command":"sudo chown -R root:root /opt"}}' 2 "Block sudo chown -R /opt"
59
+
60
+ # --- Allow: Safe operations ---
61
+ test_hook '{"tool_input":{"command":"rm -rf /tmp/junk"}}' 0 "Allow rm -rf /tmp/junk"
62
+ test_hook '{"tool_input":{"command":"rm node_modules"}}' 0 "Allow rm node_modules"
63
+ test_hook '{"tool_input":{"command":"rm /home/user/project/dist/bundle.js"}}' 0 "Allow rm specific file in project"
64
+ test_hook '{"tool_input":{"command":"ls /etc/hosts"}}' 0 "Allow read-only access to /etc"
65
+ test_hook '{"tool_input":{"command":"cat /usr/local/bin/script"}}' 0 "Allow cat on /usr"
66
+ test_hook '{"tool_input":{"command":"mv /tmp/a.txt /tmp/b.txt"}}' 0 "Allow mv in /tmp"
67
+ test_hook '{"tool_input":{"command":"chmod 644 myfile.txt"}}' 0 "Allow chmod on project file"
68
+
69
+ # --- Allow: Empty/missing input ---
70
+ test_hook '{}' 0 "Allow empty input"
71
+ test_hook '{"tool_input":{}}' 0 "Allow missing command"
72
+ test_hook '{"tool_input":{"command":""}}' 0 "Allow empty command"
73
+
74
+ # --- Allow: Non-destructive system commands ---
75
+ test_hook '{"tool_input":{"command":"grep -r pattern /etc/nginx/"}}' 0 "Allow grep in /etc"
76
+ test_hook '{"tool_input":{"command":"find /usr -name \"*.so\""}}' 0 "Allow find in /usr (no delete)"
77
+ test_hook '{"tool_input":{"command":"cp /etc/hosts /tmp/"}}' 0 "Allow cp from /etc"
78
+
79
+ echo ""
80
+ echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
81
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,151 @@
1
+ #!/bin/bash
2
+ # Tests for thinking-stall-detector.sh
3
+ set -euo pipefail
4
+
5
+ HOOK="$(dirname "$0")/../examples/thinking-stall-detector.sh"
6
+ PASS=0
7
+ FAIL=0
8
+ STATE_FILE="/tmp/cc-thinking-stall-last-call"
9
+ LOG_FILE="/tmp/cc-thinking-stalls.log"
10
+
11
+ setup() {
12
+ rm -f "$STATE_FILE" "$LOG_FILE"
13
+ }
14
+
15
+ run_hook() {
16
+ echo "$1" | CC_STALL_WARN_SECS="${2:-300}" bash "$HOOK" 2>&1 || true
17
+ }
18
+
19
+ # --- Test 1: First call (no previous state) should not warn ---
20
+ setup
21
+ output=$(echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=300 bash "$HOOK" 2>&1) || true
22
+ if echo "$output" | grep -q "stall"; then
23
+ echo " FAIL: first call should not warn"
24
+ FAIL=$((FAIL + 1))
25
+ else
26
+ echo " PASS: first call does not warn"
27
+ PASS=$((PASS + 1))
28
+ fi
29
+
30
+ # --- Test 2: Quick successive calls should not warn ---
31
+ setup
32
+ echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=300 bash "$HOOK" 2>/dev/null || true
33
+ sleep 1
34
+ output=$(echo '{"tool_name":"Write"}' | CC_STALL_WARN_SECS=300 bash "$HOOK" 2>&1) || true
35
+ if echo "$output" | grep -q "stall"; then
36
+ echo " FAIL: quick succession should not warn"
37
+ FAIL=$((FAIL + 1))
38
+ else
39
+ echo " PASS: quick succession does not warn"
40
+ PASS=$((PASS + 1))
41
+ fi
42
+
43
+ # --- Test 3: Stall detected with low threshold ---
44
+ setup
45
+ echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null || true
46
+ sleep 2
47
+ output=$(echo '{"tool_name":"Bash"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>&1) || true
48
+ if echo "$output" | grep -qi "stall"; then
49
+ echo " PASS: stall detected after threshold"
50
+ PASS=$((PASS + 1))
51
+ else
52
+ echo " FAIL: should have detected stall after 2s with 1s threshold"
53
+ echo " output: $output"
54
+ FAIL=$((FAIL + 1))
55
+ fi
56
+
57
+ # --- Test 4: Stall is logged to file ---
58
+ setup
59
+ echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null || true
60
+ sleep 2
61
+ echo '{"tool_name":"Edit"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null || true
62
+ if [ -f "$LOG_FILE" ] && grep -q "STALL" "$LOG_FILE"; then
63
+ echo " PASS: stall logged to file"
64
+ PASS=$((PASS + 1))
65
+ else
66
+ echo " FAIL: stall not logged to $LOG_FILE"
67
+ FAIL=$((FAIL + 1))
68
+ fi
69
+
70
+ # --- Test 5: Log contains tool name ---
71
+ if [ -f "$LOG_FILE" ] && grep -q "tool=Edit" "$LOG_FILE"; then
72
+ echo " PASS: log contains tool name"
73
+ PASS=$((PASS + 1))
74
+ else
75
+ echo " FAIL: log should contain tool=Edit"
76
+ FAIL=$((FAIL + 1))
77
+ fi
78
+
79
+ # --- Test 6: Warning mentions issue number ---
80
+ setup
81
+ echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null || true
82
+ sleep 2
83
+ output=$(echo '{"tool_name":"Glob"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>&1) || true
84
+ if echo "$output" | grep -q "51092"; then
85
+ echo " PASS: warning references #51092"
86
+ PASS=$((PASS + 1))
87
+ else
88
+ echo " FAIL: warning should reference #51092"
89
+ FAIL=$((FAIL + 1))
90
+ fi
91
+
92
+ # --- Test 7: After stall, next quick call should not warn ---
93
+ setup
94
+ echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null || true
95
+ sleep 2
96
+ echo '{"tool_name":"Edit"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null || true
97
+ output=$(echo '{"tool_name":"Write"}' | CC_STALL_WARN_SECS=300 bash "$HOOK" 2>&1) || true
98
+ if echo "$output" | grep -q "stall"; then
99
+ echo " FAIL: should not warn on quick follow-up after stall"
100
+ FAIL=$((FAIL + 1))
101
+ else
102
+ echo " PASS: no false positive after stall recovery"
103
+ PASS=$((PASS + 1))
104
+ fi
105
+
106
+ # --- Test 8: Hook always exits 0 (never blocks) ---
107
+ setup
108
+ echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null || true
109
+ sleep 2
110
+ echo '{"tool_name":"Bash"}' | CC_STALL_WARN_SECS=1 bash "$HOOK" 2>/dev/null
111
+ rc=$?
112
+ if [ "$rc" -eq 0 ]; then
113
+ echo " PASS: hook exits 0 (warn-only, never blocks)"
114
+ PASS=$((PASS + 1))
115
+ else
116
+ echo " FAIL: hook should always exit 0, got $rc"
117
+ FAIL=$((FAIL + 1))
118
+ fi
119
+
120
+ # --- Test 9: Empty tool name handled gracefully ---
121
+ setup
122
+ output=$(echo '{}' | CC_STALL_WARN_SECS=300 bash "$HOOK" 2>&1) || true
123
+ rc=$?
124
+ if [ "$rc" -eq 0 ]; then
125
+ echo " PASS: empty tool name handled"
126
+ PASS=$((PASS + 1))
127
+ else
128
+ echo " FAIL: should handle empty tool name gracefully"
129
+ FAIL=$((FAIL + 1))
130
+ fi
131
+
132
+ # --- Test 10: Custom log path ---
133
+ setup
134
+ CUSTOM_LOG="/tmp/cc-stall-custom-test.log"
135
+ rm -f "$CUSTOM_LOG"
136
+ echo '{"tool_name":"Read"}' | CC_STALL_WARN_SECS=1 CC_STALL_LOG="$CUSTOM_LOG" bash "$HOOK" 2>/dev/null || true
137
+ sleep 2
138
+ echo '{"tool_name":"Write"}' | CC_STALL_WARN_SECS=1 CC_STALL_LOG="$CUSTOM_LOG" bash "$HOOK" 2>/dev/null || true
139
+ if [ -f "$CUSTOM_LOG" ] && grep -q "STALL" "$CUSTOM_LOG"; then
140
+ echo " PASS: custom log path works"
141
+ PASS=$((PASS + 1))
142
+ else
143
+ echo " FAIL: custom log path not working"
144
+ FAIL=$((FAIL + 1))
145
+ fi
146
+ rm -f "$CUSTOM_LOG"
147
+
148
+ # --- Summary ---
149
+ echo ""
150
+ echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL))"
151
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # Tests for tool-retry-budget-guard.sh
3
+ set -euo pipefail
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ HOOK="$(dirname "$0")/../examples/tool-retry-budget-guard.sh"
8
+
9
+ # Clean state before tests
10
+ rm -rf /tmp/.cc-retry-budget
11
+
12
+ test_hook() {
13
+ local input="$1" expected_exit="$2" desc="$3"
14
+ local actual_exit=0
15
+ echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
16
+ if [ "$actual_exit" -eq "$expected_exit" ]; then
17
+ echo " PASS: $desc"
18
+ PASS=$((PASS + 1))
19
+ else
20
+ echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
21
+ FAIL=$((FAIL + 1))
22
+ fi
23
+ }
24
+
25
+ echo "tool-retry-budget-guard.sh tests"
26
+ echo ""
27
+
28
+ # Clean state
29
+ rm -rf /tmp/.cc-retry-budget
30
+
31
+ # --- Allow: First few edits ---
32
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 1st edit to file A"
33
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 2nd edit to file A"
34
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 3rd edit to file A"
35
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow 4th edit to file A"
36
+
37
+ # --- Allow: Warning at 5th (exit 0 but with warning) ---
38
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Warn at 5th edit (still allow)"
39
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Warn at 6th edit (still allow)"
40
+
41
+ # --- Block: 7th attempt ---
42
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 2 "Block at 7th consecutive edit"
43
+
44
+ # --- After block, counter resets ---
45
+ rm -rf /tmp/.cc-retry-budget
46
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-a.txt"}}' 0 "Allow after counter reset"
47
+
48
+ # --- Different files don't interfere ---
49
+ rm -rf /tmp/.cc-retry-budget
50
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-b.txt"}}' 0 "Allow edit to file B"
51
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-c.txt"}}' 0 "Allow edit to file C"
52
+ test_hook '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/test-retry-b.txt"}}' 0 "Allow 2nd edit to file B"
53
+
54
+ # --- Write tool also tracked ---
55
+ rm -rf /tmp/.cc-retry-budget
56
+ for i in $(seq 1 6); do
57
+ test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test-retry-w.txt"}}' 0 "Write attempt $i (allow)"
58
+ done
59
+ test_hook '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test-retry-w.txt"}}' 2 "Block Write at 7th attempt"
60
+
61
+ # --- Non-Edit/Write tools pass through ---
62
+ test_hook '{"tool_name":"Bash","tool_input":{"command":"ls"}}' 0 "Allow Bash (not tracked)"
63
+ test_hook '{"tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' 0 "Allow Read (not tracked)"
64
+ test_hook '{"tool_name":"Grep","tool_input":{"pattern":"test"}}' 0 "Allow Grep (not tracked)"
65
+
66
+ # --- Empty input ---
67
+ test_hook '{}' 0 "Allow empty input"
68
+ test_hook '{"tool_name":"Edit","tool_input":{}}' 0 "Allow Edit with no file_path"
69
+
70
+ # Clean up
71
+ rm -rf /tmp/.cc-retry-budget
72
+
73
+ echo ""
74
+ echo "Results: $PASS passed, $FAIL failed"
75
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,50 @@
1
+ #!/bin/bash
2
+ # Tests for worktree-branch-pollution-detector.sh
3
+ HOOK="examples/worktree-branch-pollution-detector.sh"
4
+ PASS=0 FAIL=0
5
+
6
+ assert_pass() { if [ $? -eq 0 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1"; fi; }
7
+ assert_contains() { if echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (expected '$3')"; fi; }
8
+ assert_not_contains() { if ! echo "$2" | grep -q "$3"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (unexpected '$3')"; fi; }
9
+
10
+ # Setup temp repo
11
+ TMPDIR=$(mktemp -d)
12
+ cd "$TMPDIR"
13
+ git init -q
14
+ git commit --allow-empty -m "init" -q 2>/dev/null
15
+
16
+ # Clean up expected branch files
17
+ HASH=$(echo "$TMPDIR" | md5sum | cut -c1-8)
18
+ rm -f "/tmp/cc-expected-branch-$HASH"
19
+
20
+ # Test 1: First run records branch (no warning)
21
+ OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
22
+ assert_not_contains "first run should not warn" "$OUT" "BRANCH CHANGED"
23
+ assert_pass "first run exits 0"
24
+
25
+ # Test 2: Same branch (no warning)
26
+ OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
27
+ assert_not_contains "same branch should not warn" "$OUT" "BRANCH CHANGED"
28
+
29
+ # Test 3: Switch branch triggers warning
30
+ DEFAULT_BRANCH=$(git branch --show-current)
31
+ git checkout -b feature-x -q 2>/dev/null
32
+ OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
33
+ assert_contains "branch change should warn" "$OUT" "BRANCH CHANGED"
34
+ assert_contains "should mention expected branch" "$OUT" "$DEFAULT_BRANCH"
35
+
36
+ # Test 4: After warning, new branch is accepted
37
+ OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
38
+ assert_not_contains "after update should not warn" "$OUT" "BRANCH CHANGED"
39
+
40
+ # Test 5: Non-git directory exits silently
41
+ cd /tmp
42
+ OUT=$(echo '{}' | bash "$OLDPWD/$HOOK" 2>&1)
43
+ assert_not_contains "non-git should not warn" "$OUT" "BRANCH CHANGED"
44
+
45
+ # Cleanup
46
+ rm -rf "$TMPDIR"
47
+ rm -f "/tmp/cc-expected-branch-$HASH"
48
+
49
+ echo "Results: $PASS passed, $FAIL failed"
50
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,29 @@
1
+ set -euo pipefail
2
+ PASS=0
3
+ FAIL=0
4
+ test_hook() {
5
+ local hook="$1" input="$2" expected_exit="$3" desc="$4"
6
+ local actual_exit=0
7
+ echo "$input" | bash "$hook" > /dev/null 2>/dev/null || actual_exit=$?
8
+ if [ "$actual_exit" -eq "$expected_exit" ]; then
9
+ echo " PASS: $desc"
10
+ PASS=$((PASS + 1))
11
+ else
12
+ echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
13
+ FAIL=$((FAIL + 1))
14
+ fi
15
+ }
16
+ HOOK_CREATE="$(dirname "$0")/../examples/worktree-create-log.sh"
17
+ HOOK_REMOVE="$(dirname "$0")/../examples/worktree-remove-uncommitted-guard.sh"
18
+ echo "=== worktree-create-log.sh ==="
19
+ test_hook "$HOOK_CREATE" '{"branch":"feature/test","path":"/tmp/wt-test"}' 0 "logs creation and passes"
20
+ test_hook "$HOOK_CREATE" '{}' 0 "empty input passes"
21
+ test_hook "$HOOK_CREATE" '' 0 "blank input passes"
22
+ echo ""
23
+ echo "=== worktree-remove-uncommitted-guard.sh ==="
24
+ test_hook "$HOOK_REMOVE" '{"path":"/nonexistent/path"}' 0 "nonexistent path passes"
25
+ test_hook "$HOOK_REMOVE" '{}' 0 "empty input passes"
26
+ test_hook "$HOOK_REMOVE" '' 0 "blank input passes"
27
+ echo ""
28
+ echo "Results: $PASS passed, $FAIL failed"
29
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1