cc-safe-setup 29.6.0 → 29.6.2

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 (81) hide show
  1. package/COOKBOOK.md +70 -0
  2. package/README.md +43 -4
  3. package/TROUBLESHOOTING.md +30 -0
  4. package/examples/api-rate-limit-tracker.sh +51 -0
  5. package/examples/auto-answer-question.sh +67 -0
  6. package/examples/auto-approve-readonly-tools.sh +10 -0
  7. package/examples/aws-production-guard.sh +40 -0
  8. package/examples/banned-command-guard.sh +48 -0
  9. package/examples/bash-heuristic-approver.sh +59 -0
  10. package/examples/bash-trace-guard.sh +48 -0
  11. package/examples/block-database-wipe.sh +1 -1
  12. package/examples/classifier-fallback-allow.sh +70 -0
  13. package/examples/commit-message-check.sh +8 -1
  14. package/examples/commit-message-quality.sh +35 -0
  15. package/examples/credential-exfil-guard.sh +12 -0
  16. package/examples/cwd-reminder.sh +37 -0
  17. package/examples/dependency-install-guard.sh +84 -0
  18. package/examples/deploy-guard.sh +1 -1
  19. package/examples/detect-mixed-indentation.sh +33 -0
  20. package/examples/disk-space-check.sh +42 -0
  21. package/examples/docker-dangerous-guard.sh +47 -0
  22. package/examples/dockerfile-lint.sh +58 -0
  23. package/examples/edit-always-allow.sh +53 -0
  24. package/examples/env-file-gitignore-check.sh +39 -0
  25. package/examples/env-source-guard.sh +1 -1
  26. package/examples/git-stash-before-danger.sh +58 -0
  27. package/examples/github-actions-guard.sh +49 -0
  28. package/examples/gitignore-auto-add.sh +30 -0
  29. package/examples/go-vet-after-edit.sh +33 -0
  30. package/examples/hook-tamper-guard.sh +67 -0
  31. package/examples/kubernetes-guard.sh +2 -1
  32. package/examples/large-file-write-guard.sh +40 -0
  33. package/examples/main-branch-warn.sh +40 -0
  34. package/examples/max-edit-size-guard.sh +9 -15
  35. package/examples/mcp-server-guard.sh +70 -0
  36. package/examples/multiline-command-approver.sh +89 -0
  37. package/examples/no-base64-exfil.sh +27 -0
  38. package/examples/no-debug-commit.sh +60 -0
  39. package/examples/no-exposed-port-in-dockerfile.sh +32 -0
  40. package/examples/no-fixme-ship.sh +41 -0
  41. package/examples/no-hardcoded-ip.sh +26 -0
  42. package/examples/no-http-in-code.sh +19 -0
  43. package/examples/no-push-without-tests.sh +33 -0
  44. package/examples/no-self-signed-cert.sh +19 -0
  45. package/examples/no-star-import-python.sh +28 -0
  46. package/examples/no-wget-piped-bash.sh +22 -0
  47. package/examples/node-version-check.sh +40 -0
  48. package/examples/npm-publish-guard.sh +5 -2
  49. package/examples/output-token-env-check.sh +44 -0
  50. package/examples/package-lock-frozen.sh +25 -0
  51. package/examples/pip-venv-required.sh +40 -0
  52. package/examples/port-conflict-check.sh +62 -0
  53. package/examples/post-compact-safety.sh +61 -0
  54. package/examples/prefer-builtin-tools.sh +33 -0
  55. package/examples/python-import-check.sh +52 -0
  56. package/examples/python-ruff-on-edit.sh +51 -0
  57. package/examples/quoted-flag-approver.sh +51 -0
  58. package/examples/react-key-warn.sh +32 -0
  59. package/examples/read-budget-guard.sh +63 -0
  60. package/examples/rm-safety-net.sh +9 -0
  61. package/examples/rust-clippy-after-edit.sh +37 -0
  62. package/examples/session-drift-guard.sh +73 -0
  63. package/examples/session-quota-tracker.sh +44 -0
  64. package/examples/session-start-safety-check.sh +60 -0
  65. package/examples/session-summary-stop.sh +49 -0
  66. package/examples/session-time-limit.sh +34 -0
  67. package/examples/strip-coauthored-by.sh +46 -0
  68. package/examples/temp-file-cleanup.sh +41 -0
  69. package/examples/test-before-push.sh +8 -1
  70. package/examples/test-coverage-reminder.sh +49 -0
  71. package/examples/test-exit-code-verify.sh +60 -0
  72. package/examples/tool-file-logger.sh +46 -0
  73. package/examples/typescript-lint-on-edit.sh +61 -0
  74. package/examples/typescript-strict-check.sh +35 -0
  75. package/examples/uncommitted-changes-stop.sh +16 -0
  76. package/examples/uncommitted-discard-guard.sh +72 -0
  77. package/examples/variable-expansion-guard.sh +51 -0
  78. package/examples/worktree-unmerged-guard.sh +13 -3
  79. package/examples/yaml-syntax-check.sh +50 -0
  80. package/index.mjs +9 -0
  81. package/package.json +2 -2
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # output-token-env-check.sh — Warn if max output tokens is not configured
3
+ #
4
+ # Solves: "Response exceeded 32000 output token maximum" error
5
+ # (#24055 — 80 reactions)
6
+ #
7
+ # Claude Code defaults to 32,000 max output tokens. For complex tasks,
8
+ # this is often not enough. Setting CLAUDE_CODE_MAX_OUTPUT_TOKENS
9
+ # prevents the error, but many users don't know about this env var.
10
+ #
11
+ # This hook checks on session start (Notification/Stop) and warns
12
+ # if the env var is not set or is set to the default 32000.
13
+ #
14
+ # TRIGGER: Notification
15
+ # MATCHER: ""
16
+ #
17
+ # Usage:
18
+ # {
19
+ # "hooks": {
20
+ # "Notification": [{
21
+ # "matcher": "",
22
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/output-token-env-check.sh" }]
23
+ # }]
24
+ # }
25
+ # }
26
+
27
+ # Only run once per session (check if we already warned)
28
+ MARKER="/tmp/cc-output-token-warned-$$"
29
+ [ -f "$MARKER" ] && exit 0
30
+
31
+ MAX_TOKENS="${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-}"
32
+
33
+ if [ -z "$MAX_TOKENS" ]; then
34
+ echo "TIP: CLAUDE_CODE_MAX_OUTPUT_TOKENS is not set (default: 32,000)." >&2
35
+ echo " For complex tasks, set it higher to avoid truncated responses:" >&2
36
+ echo " export CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000" >&2
37
+ touch "$MARKER"
38
+ elif [ "$MAX_TOKENS" -le 32000 ] 2>/dev/null; then
39
+ echo "TIP: CLAUDE_CODE_MAX_OUTPUT_TOKENS=$MAX_TOKENS (low for complex tasks)." >&2
40
+ echo " Consider: export CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000" >&2
41
+ touch "$MARKER"
42
+ fi
43
+
44
+ exit 0
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+ # package-lock-frozen.sh — Block modifications to lockfiles
3
+ #
4
+ # Prevents: Unintended lockfile changes that cause merge conflicts
5
+ # and dependency drift. Claude should use npm ci, not npm install.
6
+ #
7
+ # Blocks: Edit/Write to package-lock.json, yarn.lock, pnpm-lock.yaml
8
+ #
9
+ # TRIGGER: PreToolUse
10
+ # MATCHER: "Edit|Write"
11
+
12
+ INPUT=$(cat)
13
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
14
+ [ -z "$FILE" ] && exit 0
15
+
16
+ BASENAME=$(basename "$FILE")
17
+ case "$BASENAME" in
18
+ package-lock.json|yarn.lock|pnpm-lock.yaml|Cargo.lock|poetry.lock|Gemfile.lock|composer.lock)
19
+ echo "BLOCKED: Direct modification of lockfile '$BASENAME'." >&2
20
+ echo " Use the package manager to update dependencies instead." >&2
21
+ exit 2
22
+ ;;
23
+ esac
24
+
25
+ exit 0
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # pip-venv-required.sh — Block pip install outside of a virtual environment
3
+ #
4
+ # Prevents: System-wide pip install that can break the OS Python.
5
+ # Only allows pip install when a virtualenv is active.
6
+ #
7
+ # TRIGGER: PreToolUse
8
+ # MATCHER: "Bash"
9
+ #
10
+ # Usage:
11
+ # {
12
+ # "hooks": {
13
+ # "PreToolUse": [{
14
+ # "matcher": "Bash",
15
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/pip-venv-required.sh" }]
16
+ # }]
17
+ # }
18
+ # }
19
+
20
+ INPUT=$(cat)
21
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
22
+ [ -z "$COMMAND" ] && exit 0
23
+
24
+ # Only check pip install commands
25
+ echo "$COMMAND" | grep -qE '^\s*(pip|pip3)\s+install' || exit 0
26
+
27
+ # Allow if -r requirements.txt (deterministic install)
28
+ echo "$COMMAND" | grep -qE 'pip3?\s+install\s+-r' && exit 0
29
+
30
+ # Allow if --user flag (user-level, not system)
31
+ echo "$COMMAND" | grep -qE 'pip3?\s+install\s+.*--user' && exit 0
32
+
33
+ # Check if virtualenv is active
34
+ if [ -z "$VIRTUAL_ENV" ] && [ -z "$CONDA_DEFAULT_ENV" ]; then
35
+ echo "BLOCKED: pip install outside of virtual environment." >&2
36
+ echo " Activate a venv first: python3 -m venv .venv && source .venv/bin/activate" >&2
37
+ exit 2
38
+ fi
39
+
40
+ exit 0
@@ -0,0 +1,62 @@
1
+ #!/bin/bash
2
+ # port-conflict-check.sh — Warn before starting a server on an occupied port
3
+ #
4
+ # Prevents: "EADDRINUSE" errors that confuse Claude into debugging
5
+ # phantom issues. Detects port conflicts before they happen.
6
+ #
7
+ # Detects: npm start, npm run dev, python -m http.server, node server.js,
8
+ # next dev, vite, etc.
9
+ #
10
+ # TRIGGER: PreToolUse
11
+ # MATCHER: "Bash"
12
+ #
13
+ # Usage:
14
+ # {
15
+ # "hooks": {
16
+ # "PreToolUse": [{
17
+ # "matcher": "Bash",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/port-conflict-check.sh" }]
19
+ # }]
20
+ # }
21
+ # }
22
+
23
+ INPUT=$(cat)
24
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
25
+ [ -z "$COMMAND" ] && exit 0
26
+
27
+ # Detect server-starting commands
28
+ echo "$COMMAND" | grep -qiE '(npm\s+(start|run\s+dev)|npx\s+(next|vite|nuxt)|python.*http\.server|node\s+.*server|flask\s+run|uvicorn|gunicorn|rails\s+s)' || exit 0
29
+
30
+ # Try to extract port from command
31
+ PORT=""
32
+ if echo "$COMMAND" | grep -qE '\-\-port[= ]+([0-9]+)'; then
33
+ PORT=$(echo "$COMMAND" | grep -oE '\-\-port[= ]+([0-9]+)' | grep -oE '[0-9]+')
34
+ elif echo "$COMMAND" | grep -qE '\-p[= ]+([0-9]+)'; then
35
+ PORT=$(echo "$COMMAND" | grep -oE '\-p[= ]+([0-9]+)' | grep -oE '[0-9]+')
36
+ fi
37
+
38
+ # Common default ports
39
+ if [ -z "$PORT" ]; then
40
+ if echo "$COMMAND" | grep -qiE 'next|vite|nuxt'; then PORT=3000
41
+ elif echo "$COMMAND" | grep -qiE 'flask|django'; then PORT=5000
42
+ elif echo "$COMMAND" | grep -qiE 'rails'; then PORT=3000
43
+ elif echo "$COMMAND" | grep -qiE 'http\.server'; then PORT=8000
44
+ else PORT=3000
45
+ fi
46
+ fi
47
+
48
+ # Check if port is in use
49
+ if command -v ss >/dev/null 2>&1; then
50
+ if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then
51
+ PID=$(ss -tlnp 2>/dev/null | grep ":${PORT} " | grep -oP 'pid=\K[0-9]+' | head -1)
52
+ echo "WARNING: Port $PORT is already in use (PID: ${PID:-unknown})." >&2
53
+ echo " Kill it: kill $PID or use a different port." >&2
54
+ fi
55
+ elif command -v lsof >/dev/null 2>&1; then
56
+ if lsof -i ":${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
57
+ echo "WARNING: Port $PORT is already in use." >&2
58
+ echo " Check: lsof -i :$PORT" >&2
59
+ fi
60
+ fi
61
+
62
+ exit 0
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # post-compact-safety.sh — Guard against autonomous actions after compaction
3
+ #
4
+ # Solves: After context compaction, Claude interprets the summary as
5
+ # authorization to push commits and make irreversible changes without
6
+ # user approval.
7
+ # See: https://github.com/anthropics/claude-code/issues/39912
8
+ #
9
+ # TRIGGER: PreToolUse
10
+ # MATCHER: Bash
11
+ #
12
+ # After compaction, blocks git push and other irreversible commands
13
+ # for the first N tool calls, requiring explicit user interaction first.
14
+ #
15
+ # Usage:
16
+ # {
17
+ # "hooks": {
18
+ # "PreToolUse": [{
19
+ # "matcher": "Bash",
20
+ # "hooks": [{
21
+ # "type": "command",
22
+ # "command": "~/.claude/hooks/post-compact-safety.sh"
23
+ # }]
24
+ # }]
25
+ # }
26
+ # }
27
+ #
28
+ # Config: CC_POST_COMPACT_GUARD=10 (block for first 10 calls after compact)
29
+
30
+ INPUT=$(cat)
31
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
32
+ [ -z "$COMMAND" ] && exit 0
33
+
34
+ GUARD_CALLS=${CC_POST_COMPACT_GUARD:-10}
35
+ MARKER="/tmp/cc-post-compact-$(whoami)"
36
+ COUNTER="/tmp/cc-post-compact-count-$(whoami)"
37
+
38
+ # Detect compaction (context_window field or session state change)
39
+ # After compaction, the session summary often contains these patterns
40
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
41
+
42
+ # Check if we're in post-compact guard mode
43
+ if [ -f "$MARKER" ]; then
44
+ COUNT=1
45
+ [ -f "$COUNTER" ] && COUNT=$(( $(cat "$COUNTER") + 1 ))
46
+ echo "$COUNT" > "$COUNTER"
47
+
48
+ if [ "$COUNT" -le "$GUARD_CALLS" ]; then
49
+ # Block irreversible commands during guard period
50
+ if echo "$COMMAND" | grep -qE '^\s*(git\s+push|git\s+reset|git\s+clean|npm\s+publish|docker\s+push)'; then
51
+ echo "BLOCKED: Irreversible command blocked (post-compaction safety)" >&2
52
+ echo " $COUNT/$GUARD_CALLS guard calls remaining. Confirm with user first." >&2
53
+ exit 2
54
+ fi
55
+ else
56
+ # Guard period over
57
+ rm -f "$MARKER" "$COUNTER"
58
+ fi
59
+ fi
60
+
61
+ exit 0
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # prefer-builtin-tools.sh — Deny bash commands that have dedicated built-in tool equivalents
3
+ # PreToolUse hook (matcher: Bash)
4
+ # Solves: https://github.com/anthropics/claude-code/issues/19649 (48+ reactions)
5
+ #
6
+ # Claude Code has built-in Read, Edit, Grep, Glob tools that are faster and safer
7
+ # than bash equivalents. But Claude often reaches for sed, grep, cat instead.
8
+ # This hook denies those commands with a pointer to the correct built-in tool.
9
+
10
+ INPUT=$(cat)
11
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
12
+ [ -z "$COMMAND" ] && exit 0
13
+
14
+ # Check all segments of piped/chained commands
15
+ while IFS= read -r segment; do
16
+ cmd=$(echo "$segment" | sed 's/^[[:space:]]*//' | sed 's/^[A-Za-z_][A-Za-z_0-9]*=[^ ]* //')
17
+ base=$(basename "$(echo "$cmd" | awk '{print $1}')" 2>/dev/null)
18
+ case "$base" in
19
+ cat) msg="Use the Read tool to read files, or Write to create them" ;;
20
+ head|tail) msg="Use the Read tool with offset/limit parameters" ;;
21
+ sed) msg="Use the Edit tool for modifications, or Read for viewing line ranges" ;;
22
+ awk) msg="Use Read, Grep, or Edit tools instead" ;;
23
+ grep|rg) msg="Use the built-in Grep tool (supports -A/-B/-C context, glob filters, output_mode)" ;;
24
+ find) msg="Use the built-in Glob tool for file pattern matching" ;;
25
+ *) continue ;;
26
+ esac
27
+ cat <<EOF
28
+ {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Do not use \`$base\`. $msg"}}
29
+ EOF
30
+ exit 0
31
+ done < <(echo "$COMMAND" | tr '|' '\n' | sed 's/[;&]\{1,2\}/\n/g')
32
+
33
+ exit 0
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # python-import-check.sh — Detect unused imports in Python files
3
+ #
4
+ # Prevents: Unused imports that trigger linter warnings and add
5
+ # unnecessary dependencies. Claude often adds imports
6
+ # during development and forgets to clean up.
7
+ #
8
+ # TRIGGER: PostToolUse
9
+ # MATCHER: "Write|Edit"
10
+
11
+ INPUT=$(cat)
12
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
13
+ [ -z "$FILE" ] && exit 0
14
+
15
+ case "$FILE" in
16
+ *.py) ;;
17
+ *) exit 0 ;;
18
+ esac
19
+
20
+ [ ! -f "$FILE" ] && exit 0
21
+
22
+ # Quick check: find import lines and see if the imported name appears elsewhere
23
+ python3 -c "
24
+ import re, sys
25
+
26
+ with open('$FILE') as f:
27
+ content = f.read()
28
+ lines = content.split('\n')
29
+
30
+ imports = []
31
+ for line in lines:
32
+ m = re.match(r'^import\s+(\w+)', line)
33
+ if m: imports.append(m.group(1))
34
+ m = re.match(r'^from\s+\S+\s+import\s+(.+)', line)
35
+ if m:
36
+ for name in m.group(1).split(','):
37
+ name = name.strip().split(' as ')[-1].strip()
38
+ if name and name != '*':
39
+ imports.append(name)
40
+
41
+ unused = []
42
+ for imp in imports:
43
+ # Count occurrences (excluding the import line itself)
44
+ count = len(re.findall(r'\b' + re.escape(imp) + r'\b', content))
45
+ if count <= 1: # Only appears in the import line
46
+ unused.append(imp)
47
+
48
+ if unused:
49
+ print(f'Possibly unused imports in $FILE: {', '.join(unused[:5])}', file=sys.stderr)
50
+ " 2>&1
51
+
52
+ exit 0
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # python-ruff-on-edit.sh — Run ruff lint after editing Python files
3
+ #
4
+ # TRIGGER: PostToolUse
5
+ # MATCHER: Edit
6
+ #
7
+ # Best with the v2.1.85 "if" field to avoid running on non-Python edits:
8
+ #
9
+ # {
10
+ # "hooks": {
11
+ # "PostToolUse": [{
12
+ # "matcher": "Edit",
13
+ # "hooks": [{
14
+ # "type": "command",
15
+ # "if": "Edit(*.py)",
16
+ # "command": "~/.claude/hooks/python-ruff-on-edit.sh"
17
+ # }]
18
+ # }]
19
+ # }
20
+ # }
21
+ #
22
+ # Without "if", the hook runs after every Edit and checks internally.
23
+
24
+ INPUT=$(cat)
25
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
26
+
27
+ # Skip non-Python files (redundant with "if" field, kept for backward compat)
28
+ [[ "$FILE" != *.py ]] && exit 0
29
+ [ ! -f "$FILE" ] && exit 0
30
+
31
+ # Prefer ruff, fall back to flake8, then pylint
32
+ if command -v ruff &>/dev/null; then
33
+ ISSUES=$(ruff check "$FILE" --quiet 2>/dev/null)
34
+ elif command -v flake8 &>/dev/null; then
35
+ ISSUES=$(flake8 "$FILE" --max-line-length=120 2>/dev/null)
36
+ elif command -v pylint &>/dev/null; then
37
+ ISSUES=$(pylint "$FILE" --errors-only --score=no 2>/dev/null)
38
+ else
39
+ exit 0 # No linter available
40
+ fi
41
+
42
+ if [ -n "$ISSUES" ]; then
43
+ COUNT=$(echo "$ISSUES" | wc -l)
44
+ echo "⚠ Lint: $COUNT issue(s) in $(basename "$FILE")" >&2
45
+ echo "$ISSUES" | head -5 >&2
46
+ if [ "$COUNT" -gt 5 ]; then
47
+ echo " ... and $((COUNT - 5)) more" >&2
48
+ fi
49
+ fi
50
+
51
+ exit 0
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # quoted-flag-approver.sh — Auto-approve commands with quoted flag values
3
+ #
4
+ # Solves: "Command contains quoted characters in flag names" false positives
5
+ # (#27957 — 70 reactions, breaks agentic workflows)
6
+ #
7
+ # After a Claude Code update, normal commands like:
8
+ # git commit -m "fix bug"
9
+ # bun run build --flag "value"
10
+ # trigger a confirmation prompt even when they match allowlist patterns.
11
+ #
12
+ # This PermissionRequest hook auto-approves these prompts when:
13
+ # 1. The base command is in a safe list
14
+ # 2. The only "issue" is quoted characters in flag values
15
+ #
16
+ # TRIGGER: PermissionRequest
17
+ # MATCHER: ""
18
+ #
19
+ # Usage:
20
+ # {
21
+ # "hooks": {
22
+ # "PermissionRequest": [{
23
+ # "matcher": "",
24
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/quoted-flag-approver.sh" }]
25
+ # }]
26
+ # }
27
+ # }
28
+
29
+ INPUT=$(cat)
30
+
31
+ # Only handle "quoted characters in flag names" prompts
32
+ MESSAGE=$(echo "$INPUT" | jq -r '.message // empty' 2>/dev/null)
33
+ echo "$MESSAGE" | grep -qi "quoted characters in flag" || exit 0
34
+
35
+ # Extract the command being checked
36
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
37
+ [ -z "$COMMAND" ] && exit 0
38
+
39
+ # Safe base commands — customize for your project
40
+ SAFE_COMMANDS="git|npm|npx|bun|yarn|pnpm|docker|make|cargo|go|pip|python3|node|tsc|eslint|prettier|jest|vitest|pytest|curl|wget|rsync|tar|zip|unzip|cp|mv|mkdir|cat|echo|grep|find|ls|chmod|sed|awk"
41
+
42
+ # Extract base command (first word, ignoring env vars and path)
43
+ BASE_CMD=$(echo "$COMMAND" | sed 's/^[A-Z_]*=[^ ]* //' | awk '{print $1}' | sed 's|.*/||')
44
+
45
+ if echo "$BASE_CMD" | grep -qE "^($SAFE_COMMANDS)$"; then
46
+ echo '{"permissionDecision":"allow"}'
47
+ exit 0
48
+ fi
49
+
50
+ # Unknown command — let the prompt through
51
+ exit 0
@@ -0,0 +1,32 @@
1
+ #!/bin/bash
2
+ # react-key-warn.sh — Warn about missing key props in JSX lists
3
+ #
4
+ # Prevents: "Each child in a list should have a unique key prop" errors.
5
+ # Claude often generates .map() without key props.
6
+ #
7
+ # TRIGGER: PostToolUse
8
+ # MATCHER: "Write|Edit"
9
+
10
+ INPUT=$(cat)
11
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
12
+ [ -z "$FILE" ] && exit 0
13
+
14
+ case "$FILE" in
15
+ *.jsx|*.tsx) ;;
16
+ *) exit 0 ;;
17
+ esac
18
+
19
+ [ ! -f "$FILE" ] && exit 0
20
+
21
+ # Check for .map( without key= in the return
22
+ if grep -qE '\.map\s*\(' "$FILE"; then
23
+ # Count map calls and key props
24
+ MAPS=$(grep -c '\.map\s*(' "$FILE" 2>/dev/null || echo 0)
25
+ KEYS=$(grep -c 'key=' "$FILE" 2>/dev/null || echo 0)
26
+ if [ "$MAPS" -gt "$KEYS" ]; then
27
+ echo "WARNING: $FILE has $MAPS .map() calls but only $KEYS key= props." >&2
28
+ echo " Add key props to list items to avoid React warnings." >&2
29
+ fi
30
+ fi
31
+
32
+ exit 0
@@ -0,0 +1,63 @@
1
+ #!/bin/bash
2
+ # read-budget-guard.sh — Limit excessive file reading to prevent token waste
3
+ #
4
+ # Solves: Claude reading far more files than necessary, consuming 25% of
5
+ # quota before any real work begins. Prevents duplicate reads.
6
+ # See: https://github.com/anthropics/claude-code/issues/38733
7
+ #
8
+ # TRIGGER: PreToolUse
9
+ # MATCHER: Read
10
+ #
11
+ # Usage:
12
+ # {
13
+ # "hooks": {
14
+ # "PreToolUse": [{
15
+ # "matcher": "Read",
16
+ # "hooks": [{
17
+ # "type": "command",
18
+ # "command": "~/.claude/hooks/read-budget-guard.sh"
19
+ # }]
20
+ # }]
21
+ # }
22
+ # }
23
+ #
24
+ # Config via env vars:
25
+ # CC_READ_BUDGET=100 — max unique files per session (default: 100)
26
+ # CC_READ_WARN=50 — warn threshold (default: 50)
27
+
28
+ INPUT=$(cat)
29
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
30
+ [ -z "$FILE" ] && exit 0
31
+
32
+ BUDGET=${CC_READ_BUDGET:-100}
33
+ WARN=${CC_READ_WARN:-50}
34
+ TRACKER="/tmp/cc-read-budget-$$"
35
+
36
+ # Create tracker if needed
37
+ [ -f "$TRACKER" ] || touch "$TRACKER"
38
+
39
+ # Check for duplicate read
40
+ if grep -qxF "$FILE" "$TRACKER" 2>/dev/null; then
41
+ echo "⚠ Duplicate read: $(basename "$FILE") was already read this session" >&2
42
+ echo " Consider using the cached content instead of re-reading." >&2
43
+ # Allow but warn (don't block duplicate reads, just flag them)
44
+ fi
45
+
46
+ # Track this read
47
+ echo "$FILE" >> "$TRACKER"
48
+ COUNT=$(wc -l < "$TRACKER")
49
+
50
+ # Check budget
51
+ if [ "$COUNT" -gt "$BUDGET" ]; then
52
+ echo "BLOCKED: Read budget exceeded ($COUNT/$BUDGET files)" >&2
53
+ echo "You've read $COUNT unique files this session." >&2
54
+ echo "Start working with what you have, or increase CC_READ_BUDGET." >&2
55
+ exit 2
56
+ fi
57
+
58
+ if [ "$COUNT" -eq "$WARN" ]; then
59
+ echo "⚠ Read budget warning: $COUNT/$BUDGET files read" >&2
60
+ echo " Focus on implementation rather than reading more files." >&2
61
+ fi
62
+
63
+ exit 0
@@ -27,6 +27,9 @@
27
27
  # }]
28
28
  # }
29
29
  # }
30
+ #
31
+ # Note: This hook checks rm, find -delete, and shred. Do NOT add an "if" field
32
+ # (v2.1.85) because "if" only supports one pattern and would miss the others.
30
33
 
31
34
  INPUT=$(cat)
32
35
  COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
@@ -41,6 +44,12 @@ if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?rm\s'; then
41
44
  # Extract the target (last argument after flags)
42
45
  TARGET=$(echo "$COMMAND" | grep -oP 'rm\s+[^;|&]*' | awk '{print $NF}')
43
46
 
47
+ # Block path traversal early
48
+ if echo "$TARGET" | grep -qF '..'; then
49
+ echo "BLOCKED: path traversal detected in rm target" >&2
50
+ exit 2
51
+ fi
52
+
44
53
  # Allow safe targets
45
54
  if echo "$TARGET" | grep -qE "^(\./)?(${SAFE_TARGETS})(/|$)"; then
46
55
  exit 0
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # rust-clippy-after-edit.sh — Run cargo clippy after editing Rust files
3
+ #
4
+ # Prevents: Common Rust anti-patterns and potential bugs.
5
+ # Clippy catches: needless borrows, inefficient patterns,
6
+ # suspicious operations.
7
+ #
8
+ # TRIGGER: PostToolUse
9
+ # MATCHER: "Write|Edit"
10
+
11
+ INPUT=$(cat)
12
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
13
+ [ -z "$FILE" ] && exit 0
14
+
15
+ case "$FILE" in
16
+ *.rs) ;;
17
+ *) exit 0 ;;
18
+ esac
19
+
20
+ [ ! -f "$FILE" ] && exit 0
21
+
22
+ # Find Cargo.toml
23
+ DIR=$(dirname "$FILE")
24
+ while [ "$DIR" != "/" ]; do
25
+ [ -f "$DIR/Cargo.toml" ] && break
26
+ DIR=$(dirname "$DIR")
27
+ done
28
+
29
+ if [ -f "$DIR/Cargo.toml" ] && command -v cargo >/dev/null 2>&1; then
30
+ WARNINGS=$(cd "$DIR" && cargo clippy --quiet 2>&1 | grep "^warning" | head -3)
31
+ if [ -n "$WARNINGS" ]; then
32
+ echo "Clippy warnings:" >&2
33
+ echo "$WARNINGS" | sed 's/^/ /' >&2
34
+ fi
35
+ fi
36
+
37
+ exit 0
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # session-drift-guard.sh — Progressive safety as session ages
3
+ #
4
+ # Solves: After ~6 hours, Claude starts ignoring CLAUDE.md rules,
5
+ # acting autonomously, creating duplicates, corrupting files.
6
+ # See: https://github.com/anthropics/claude-code/issues/32963
7
+ #
8
+ # TRIGGER: PreToolUse
9
+ # MATCHER: Bash,Edit,Write
10
+ #
11
+ # As tool call count grows, the hook progressively tightens:
12
+ # 0-200: Normal (no action)
13
+ # 200-500: Warn every 50 calls about drift risk
14
+ # 500+: Block destructive commands (rm, git push, git reset)
15
+ #
16
+ # Usage:
17
+ # {
18
+ # "hooks": {
19
+ # "PreToolUse": [{
20
+ # "matcher": "Bash,Edit,Write",
21
+ # "hooks": [{
22
+ # "type": "command",
23
+ # "command": "~/.claude/hooks/session-drift-guard.sh"
24
+ # }]
25
+ # }]
26
+ # }
27
+ # }
28
+ #
29
+ # Config: CC_DRIFT_WARN=200 CC_DRIFT_BLOCK=500
30
+
31
+ INPUT=$(cat)
32
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
33
+
34
+ WARN_THRESHOLD=${CC_DRIFT_WARN:-200}
35
+ BLOCK_THRESHOLD=${CC_DRIFT_BLOCK:-500}
36
+ COUNTER="/tmp/cc-drift-counter-$(whoami)"
37
+
38
+ # Increment counter
39
+ COUNT=1
40
+ if [ -f "$COUNTER" ]; then
41
+ COUNT=$(( $(cat "$COUNTER") + 1 ))
42
+ fi
43
+ echo "$COUNT" > "$COUNTER"
44
+
45
+ # Phase 1: Normal (no action)
46
+ if [ "$COUNT" -lt "$WARN_THRESHOLD" ]; then
47
+ exit 0
48
+ fi
49
+
50
+ # Phase 2: Warn periodically
51
+ if [ "$COUNT" -lt "$BLOCK_THRESHOLD" ]; then
52
+ if [ $(( COUNT % 50 )) -eq 0 ]; then
53
+ echo "⚠ Session drift warning: $COUNT tool calls" >&2
54
+ echo " Long sessions degrade AI judgment. Consider /compact or restarting." >&2
55
+ fi
56
+ exit 0
57
+ fi
58
+
59
+ # Phase 3: Block destructive commands
60
+ [ -z "$COMMAND" ] && exit 0
61
+ if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?(rm|git\s+push|git\s+reset|git\s+clean|git\s+checkout\s+--)\b'; then
62
+ echo "BLOCKED: Destructive command after $COUNT tool calls (drift risk)" >&2
63
+ echo "Session has exceeded $BLOCK_THRESHOLD tool calls." >&2
64
+ echo "Restart the session or use /compact before destructive operations." >&2
65
+ exit 2
66
+ fi
67
+
68
+ # Non-destructive commands still pass with warning
69
+ if [ $(( COUNT % 100 )) -eq 0 ]; then
70
+ echo "⚠ High drift risk: $COUNT tool calls. Consider restarting." >&2
71
+ fi
72
+
73
+ exit 0