cc-safe-setup 29.6.40 → 29.7.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 (79) 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/context-size-alert.sh +38 -0
  17. package/examples/context-usage-drift-alert.sh +33 -0
  18. package/examples/dangerous-pip-flag-guard.sh +51 -0
  19. package/examples/deny-bypass-detector.sh +143 -0
  20. package/examples/dotenv-read-guard.sh +48 -0
  21. package/examples/dotfile-protection-guard.sh +60 -0
  22. package/examples/effort-tracking-logger.sh +30 -0
  23. package/examples/financial-operation-guard.sh +47 -0
  24. package/examples/full-rewrite-detector.sh +63 -0
  25. package/examples/home-critical-bash-guard.sh +56 -0
  26. package/examples/idle-session-cost-alert.sh +36 -0
  27. package/examples/model-version-alert.sh +18 -0
  28. package/examples/model-version-change-alert.sh +31 -0
  29. package/examples/move-delete-sequence-guard.sh +92 -0
  30. package/examples/pii-upload-guard.sh +72 -0
  31. package/examples/pr-duplicate-guard.sh +14 -0
  32. package/examples/production-port-kill-guard.sh +60 -0
  33. package/examples/quota-reset-cycle-monitor.sh +30 -0
  34. package/examples/repo-visibility-guard.sh +33 -0
  35. package/examples/sandbox-relative-path-audit.sh +51 -0
  36. package/examples/session-agent-cost-limiter.sh +43 -0
  37. package/examples/session-cost-alert.sh +62 -0
  38. package/examples/session-memory-watchdog.sh +9 -0
  39. package/examples/settings-integrity-monitor.sh +55 -0
  40. package/examples/settings-json-model-guard.sh +89 -0
  41. package/examples/shell-config-truncation-guard.sh +97 -0
  42. package/examples/shell-wrapper-guard.sh +4 -4
  43. package/examples/subagent-spawn-rate-monitor.sh +34 -0
  44. package/examples/subcommand-chain-guard.sh +44 -0
  45. package/examples/system-dir-protection-guard.sh +100 -0
  46. package/examples/thinking-display-enforcer.sh +25 -0
  47. package/examples/tool-retry-budget-guard.sh +59 -0
  48. package/examples/worktree-branch-pollution-detector.sh +35 -0
  49. package/examples/worktree-create-log.sh +6 -0
  50. package/examples/worktree-hook-linker.sh +72 -0
  51. package/examples/worktree-remove-uncommitted-guard.sh +20 -0
  52. package/hooks/hooks.json +60 -0
  53. package/index.mjs +92 -6
  54. package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
  55. package/package.json +2 -2
  56. package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
  57. package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
  58. package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
  59. package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
  60. package/skills/safety-setup/SKILL.md +47 -0
  61. package/tests/dotenv-read-guard.test.sh +65 -0
  62. package/tests/test-auto-mode-safety-enforcer.sh +55 -0
  63. package/tests/test-case-insensitive-path-guard.sh +78 -0
  64. package/tests/test-context-usage-drift-alert.sh +52 -0
  65. package/tests/test-dangerous-pip-flag-guard.sh +56 -0
  66. package/tests/test-dotfile-protection-guard.sh +68 -0
  67. package/tests/test-effort-tracking-logger.sh +55 -0
  68. package/tests/test-financial-operation-guard.sh +59 -0
  69. package/tests/test-home-critical-bash-guard.sh +59 -0
  70. package/tests/test-model-version-change-alert.sh +55 -0
  71. package/tests/test-move-delete-sequence-guard.sh +63 -0
  72. package/tests/test-pr-duplicate-guard.sh +29 -0
  73. package/tests/test-quota-reset-cycle-monitor.sh +52 -0
  74. package/tests/test-shell-config-truncation-guard.sh +104 -0
  75. package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
  76. package/tests/test-system-dir-protection-guard.sh +81 -0
  77. package/tests/test-tool-retry-budget-guard.sh +75 -0
  78. package/tests/test-worktree-branch-pollution-detector.sh +50 -0
  79. package/tests/test-worktree-lifecycle-hooks.sh +29 -0
@@ -0,0 +1,63 @@
1
+ #!/bin/bash
2
+ # Tests for move-delete-sequence-guard.sh
3
+ # Run: bash tests/test-move-delete-sequence-guard.sh
4
+ set -euo pipefail
5
+
6
+ PASS=0
7
+ FAIL=0
8
+ HOOK="$(dirname "$0")/../examples/move-delete-sequence-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 "move-delete-sequence-guard.sh tests"
24
+ echo ""
25
+
26
+ # --- Block: mv + rm -rf on parent directory (#49129 pattern) ---
27
+ test_hook '{"tool_input":{"command":"mv /home/user/project/important.txt /tmp/ && rm -rf /home/user/project"}}' 2 "Block mv file then rm -rf parent (&&)"
28
+ test_hook '{"tool_input":{"command":"mv /home/user/project/src/main.py /tmp/backup/ ; rm -rf /home/user/project/src"}}' 2 "Block mv file then rm -rf parent (;)"
29
+ test_hook '{"tool_input":{"command":"mv /data/app/config.yml /tmp/ || rm -rf /data/app"}}' 2 "Block mv file then rm -rf parent (||)"
30
+
31
+ # --- Block: mv + rm -rf on same path ---
32
+ test_hook '{"tool_input":{"command":"mv /home/user/mydir /tmp/backup && rm -rf /home/user/mydir"}}' 2 "Block mv dir then rm -rf same dir"
33
+
34
+ # --- Block: mv + rm -rf on ancestor directory ---
35
+ test_hook '{"tool_input":{"command":"mv /home/user/project/src/lib/util.py /tmp/ && rm -rf /home/user/project"}}' 2 "Block mv file then rm -rf ancestor"
36
+ test_hook '{"tool_input":{"command":"mv /var/data/app/logs/today.log /tmp/ ; rm -rf /var/data"}}' 2 "Block mv file then rm -rf distant ancestor"
37
+
38
+ # --- Allow: mv and rm on unrelated paths ---
39
+ test_hook '{"tool_input":{"command":"mv /tmp/old.txt /tmp/new.txt && rm /var/log/app.log"}}' 0 "Allow mv and rm on unrelated paths"
40
+ test_hook '{"tool_input":{"command":"mv file.txt backup/ && rm other_file.txt"}}' 0 "Allow mv and rm on different files"
41
+
42
+ # --- Allow: mv only (no rm) ---
43
+ test_hook '{"tool_input":{"command":"mv /home/user/file.txt /tmp/"}}' 0 "Allow mv without rm"
44
+
45
+ # --- Allow: rm only (no mv) ---
46
+ test_hook '{"tool_input":{"command":"rm -rf /tmp/junk"}}' 0 "Allow rm without mv"
47
+
48
+ # --- Allow: Empty/missing input ---
49
+ test_hook '{}' 0 "Allow empty input"
50
+ test_hook '{"tool_input":{}}' 0 "Allow missing command"
51
+ test_hook '{"tool_input":{"command":""}}' 0 "Allow empty command"
52
+
53
+ # --- Allow: Safe operations ---
54
+ test_hook '{"tool_input":{"command":"ls -la && echo done"}}' 0 "Allow non-destructive compound command"
55
+ test_hook '{"tool_input":{"command":"git mv old.txt new.txt"}}' 0 "Allow git mv (no rm)"
56
+
57
+ # --- Block: Real-world attack patterns ---
58
+ test_hook '{"tool_input":{"command":"mv /home/user/projects/webapp/src/index.js /tmp/safe/ && rm -rf /home/user/projects/webapp/src"}}' 2 "Block real-world: save one file, delete rest of src"
59
+ test_hook '{"tool_input":{"command":"mv /home/user/data/important.db /tmp/ ; rm -r /home/user/data"}}' 2 "Block real-world: save DB, delete data dir"
60
+
61
+ echo ""
62
+ echo "Results: $PASS passed, $FAIL failed out of $((PASS + FAIL)) tests"
63
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,29 @@
1
+ set -euo pipefail
2
+ PASS=0
3
+ FAIL=0
4
+ HOOK="$(dirname "$0")/../examples/pr-duplicate-guard.sh"
5
+ test_hook() {
6
+ local input="$1" expected_exit="$2" desc="$3"
7
+ local actual_exit=0
8
+ echo "$input" | bash "$HOOK" > /dev/null 2>/dev/null || actual_exit=$?
9
+ if [ "$actual_exit" -eq "$expected_exit" ]; then
10
+ echo " PASS: $desc"
11
+ PASS=$((PASS + 1))
12
+ else
13
+ echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
14
+ FAIL=$((FAIL + 1))
15
+ fi
16
+ }
17
+ echo "=== pr-duplicate-guard.sh ==="
18
+ test_hook '{"tool_input":{"command":"git push origin main"}}' 0 "git push passes through"
19
+ test_hook '{"tool_input":{"command":"npm publish"}}' 0 "npm publish passes through"
20
+ test_hook '{"tool_input":{"command":"echo hello"}}' 0 "echo passes through"
21
+ test_hook '{"tool_input":{"command":"gh pr list"}}' 0 "gh pr list passes through"
22
+ test_hook '{"tool_input":{"command":"gh pr view 123"}}' 0 "gh pr view passes through"
23
+ test_hook '{"tool_input":{"command":"gh issue create --title test"}}' 0 "gh issue create passes through"
24
+ test_hook '{"tool_input":{"command":"gh pr create --title \"test\" --body \"test\""}}' 0 "gh pr create on unique branch passes"
25
+ test_hook '{}' 0 "empty input passes"
26
+ test_hook '' 0 "blank input passes"
27
+ echo ""
28
+ echo "Results: $PASS passed, $FAIL failed"
29
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -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,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