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.
- package/.claude-plugin/marketplace.json +66 -0
- package/.claude-plugin/plugin.json +11 -0
- package/README.md +123 -12
- package/SETTINGS_REFERENCE.md +2 -0
- package/SKILL.md +47 -0
- package/examples/README.md +11 -1
- package/examples/auto-approve-compound-git.sh +3 -0
- package/examples/auto-compact-context-monitor.sh +35 -0
- package/examples/auto-mode-safety-enforcer.sh +57 -0
- package/examples/background-task-guard.sh +57 -0
- package/examples/broad-find-guard.sh +62 -0
- package/examples/cache-creation-spike-detector.sh +32 -0
- package/examples/case-insensitive-path-guard.sh +96 -0
- package/examples/cjk-punctuation-guard.sh +44 -0
- package/examples/clipboard-secret-guard.sh +29 -0
- package/examples/compact-circuit-breaker.sh +72 -0
- package/examples/context-size-alert.sh +38 -0
- package/examples/context-usage-drift-alert.sh +33 -0
- package/examples/dangerous-pip-flag-guard.sh +51 -0
- package/examples/deny-bypass-detector.sh +143 -0
- package/examples/dotenv-read-guard.sh +48 -0
- package/examples/dotfile-protection-guard.sh +60 -0
- package/examples/effort-tracking-logger.sh +30 -0
- package/examples/exploration-budget-guard.sh +77 -0
- package/examples/financial-operation-guard.sh +47 -0
- package/examples/full-rewrite-detector.sh +63 -0
- package/examples/home-critical-bash-guard.sh +56 -0
- package/examples/idle-session-cost-alert.sh +36 -0
- package/examples/model-version-alert.sh +18 -0
- package/examples/model-version-change-alert.sh +31 -0
- package/examples/move-delete-sequence-guard.sh +92 -0
- package/examples/pii-upload-guard.sh +72 -0
- package/examples/pr-duplicate-guard.sh +14 -0
- package/examples/production-port-kill-guard.sh +60 -0
- package/examples/quota-reset-cycle-monitor.sh +30 -0
- package/examples/repo-visibility-guard.sh +33 -0
- package/examples/sandbox-relative-path-audit.sh +51 -0
- package/examples/session-agent-cost-limiter.sh +43 -0
- package/examples/session-cost-alert.sh +62 -0
- package/examples/session-memory-watchdog.sh +9 -0
- package/examples/settings-integrity-monitor.sh +55 -0
- package/examples/settings-json-model-guard.sh +89 -0
- package/examples/shell-config-truncation-guard.sh +97 -0
- package/examples/shell-wrapper-guard.sh +4 -4
- package/examples/subagent-spawn-rate-monitor.sh +34 -0
- package/examples/subcommand-chain-guard.sh +44 -0
- package/examples/system-dir-protection-guard.sh +100 -0
- package/examples/thinking-display-enforcer.sh +25 -0
- package/examples/thinking-stall-detector.sh +61 -0
- package/examples/tool-retry-budget-guard.sh +59 -0
- package/examples/worktree-branch-pollution-detector.sh +35 -0
- package/examples/worktree-create-log.sh +6 -0
- package/examples/worktree-hook-linker.sh +72 -0
- package/examples/worktree-remove-uncommitted-guard.sh +20 -0
- package/hooks/hooks.json +60 -0
- package/index.mjs +92 -6
- package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
- package/package.json +2 -2
- package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
- package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
- package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
- package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
- package/skills/safety-setup/SKILL.md +47 -0
- package/tests/dotenv-read-guard.test.sh +65 -0
- package/tests/test-auto-mode-safety-enforcer.sh +55 -0
- package/tests/test-case-insensitive-path-guard.sh +78 -0
- package/tests/test-compact-circuit-breaker.sh +134 -0
- package/tests/test-context-usage-drift-alert.sh +52 -0
- package/tests/test-dangerous-pip-flag-guard.sh +56 -0
- package/tests/test-dotfile-protection-guard.sh +68 -0
- package/tests/test-effort-tracking-logger.sh +55 -0
- package/tests/test-exploration-budget-guard.sh +164 -0
- package/tests/test-financial-operation-guard.sh +59 -0
- package/tests/test-home-critical-bash-guard.sh +59 -0
- package/tests/test-model-version-change-alert.sh +55 -0
- package/tests/test-move-delete-sequence-guard.sh +63 -0
- package/tests/test-pr-duplicate-guard.sh +29 -0
- package/tests/test-quota-reset-cycle-monitor.sh +52 -0
- package/tests/test-shell-config-truncation-guard.sh +104 -0
- package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
- package/tests/test-system-dir-protection-guard.sh +81 -0
- package/tests/test-thinking-stall-detector.sh +151 -0
- package/tests/test-tool-retry-budget-guard.sh +75 -0
- package/tests/test-worktree-branch-pollution-detector.sh +50 -0
- 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 ]
|