cc-safe-setup 29.6.25 → 29.6.27

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 (70) hide show
  1. package/README.md +3 -2
  2. package/examples/ansible-vault-guard.sh +8 -0
  3. package/examples/api-rate-limit-guard.sh +55 -0
  4. package/examples/bash-timeout-guard.sh +63 -0
  5. package/examples/bulk-file-delete-guard.sh +88 -0
  6. package/examples/cargo-publish-guard.sh +10 -0
  7. package/examples/check-test-exists.sh +82 -0
  8. package/examples/chmod-guard.sh +45 -0
  9. package/examples/chown-guard.sh +57 -0
  10. package/examples/composer-guard.sh +35 -0
  11. package/examples/console-log-count.sh +11 -0
  12. package/examples/django-migrate-guard.sh +39 -0
  13. package/examples/dockerfile-latest-guard.sh +12 -0
  14. package/examples/dotnet-build-on-edit.sh +49 -0
  15. package/examples/drizzle-migrate-guard.sh +32 -0
  16. package/examples/edit-error-counter.sh +54 -0
  17. package/examples/env-inherit-guard.sh +66 -0
  18. package/examples/expo-eject-guard.sh +10 -0
  19. package/examples/file-change-monitor.sh +32 -0
  20. package/examples/file-reference-check.sh +66 -0
  21. package/examples/five-hundred-milestone.sh +8 -0
  22. package/examples/flask-debug-guard.sh +33 -0
  23. package/examples/gem-push-guard.sh +10 -0
  24. package/examples/git-stash-before-checkout.sh +53 -0
  25. package/examples/go-mod-tidy-warn.sh +8 -0
  26. package/examples/hallucination-url-check.sh +68 -0
  27. package/examples/hardcoded-ip-guard.sh +12 -0
  28. package/examples/helm-install-guard.sh +8 -0
  29. package/examples/java-compile-on-edit.sh +39 -0
  30. package/examples/laravel-artisan-guard.sh +11 -0
  31. package/examples/long-session-reminder.sh +49 -0
  32. package/examples/magic-number-warn.sh +12 -0
  33. package/examples/max-function-length.sh +8 -4
  34. package/examples/monorepo-scope-guard.sh +70 -0
  35. package/examples/nextjs-env-guard.sh +58 -0
  36. package/examples/no-any-typescript.sh +12 -0
  37. package/examples/no-ask-human.sh +51 -0
  38. package/examples/no-console-log-commit.sh +43 -0
  39. package/examples/no-cors-wildcard.sh +10 -0
  40. package/examples/no-dangling-await.sh +11 -0
  41. package/examples/no-deep-relative-import.sh +12 -0
  42. package/examples/no-eval-template.sh +11 -0
  43. package/examples/no-global-install.sh +61 -0
  44. package/examples/no-hardcoded-port.sh +11 -4
  45. package/examples/no-http-url.sh +12 -0
  46. package/examples/no-inline-styles.sh +11 -0
  47. package/examples/no-root-user-docker.sh +10 -0
  48. package/examples/no-secrets-in-args.sh +9 -0
  49. package/examples/no-star-import-python.sh +8 -24
  50. package/examples/no-todo-in-production.sh +10 -0
  51. package/examples/no-wget-piped-bash.sh +1 -1
  52. package/examples/nuxt-config-guard.sh +7 -0
  53. package/examples/parallel-session-guard.sh +58 -0
  54. package/examples/php-lint-on-edit.sh +36 -0
  55. package/examples/pip-publish-guard.sh +10 -0
  56. package/examples/plan-repo-sync.sh +86 -0
  57. package/examples/prisma-migrate-guard.sh +41 -0
  58. package/examples/rails-migration-guard.sh +42 -0
  59. package/examples/redis-flushall-guard.sh +9 -0
  60. package/examples/ruby-lint-on-edit.sh +36 -0
  61. package/examples/sensitive-log-guard.sh +12 -0
  62. package/examples/spring-profile-guard.sh +8 -0
  63. package/examples/staged-secret-scan.sh +91 -0
  64. package/examples/svelte-lint-on-edit.sh +9 -0
  65. package/examples/swift-build-on-edit.sh +36 -0
  66. package/examples/system-package-guard.sh +42 -0
  67. package/examples/test-after-edit.sh +48 -0
  68. package/examples/turbo-cache-guard.sh +36 -0
  69. package/examples/vue-lint-on-edit.sh +11 -0
  70. package/package.json +2 -2
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dw/cc-safe-setup)](https://www.npmjs.com/package/cc-safe-setup)
5
5
  [![tests](https://github.com/yurukusa/cc-safe-setup/actions/workflows/test.yml/badge.svg)](https://github.com/yurukusa/cc-safe-setup/actions/workflows/test.yml)
6
6
 
7
- **One command to make Claude Code safe for autonomous operation.** 1,000+ installs/day · [日本語](docs/README.ja.md)
7
+ **One command to make Claude Code safe for autonomous operation.** 507 example hooks · 7,341 tests · 1,000+ installs/day · [日本語](docs/README.ja.md)
8
8
 
9
9
  ```bash
10
10
  npx cc-safe-setup
@@ -117,7 +117,7 @@ Install any of these: `npx cc-safe-setup --install-example <name>`
117
117
  | `--scan [--apply]` | Tech stack detection |
118
118
  | `--export / --import` | Team config sharing |
119
119
  | `--verify` | Test each hook |
120
- | `--install-example <name>` | Install from 446 examples |
120
+ | `--install-example <name>` | Install from 507 examples |
121
121
  | `--examples [filter]` | Browse examples by keyword |
122
122
  | `--full` | All-in-one setup |
123
123
  | `--status` | Check installed hooks |
@@ -184,6 +184,7 @@ Install any of these: `npx cc-safe-setup --install-example <name>`
184
184
  | Multiline commands skip pattern matching | [#11932](https://github.com/anthropics/claude-code/issues/11932) (47👍) | Use hooks instead of allowlist patterns for complex commands |
185
185
  | No notification when Claude asks a question | [#13024](https://github.com/anthropics/claude-code/issues/13024) (52👍) | `npx cc-safe-setup --install-example notify-waiting` |
186
186
  | `allow` overrides `ask` in permissions | [#6527](https://github.com/anthropics/claude-code/issues/6527) (17👍) | Use hooks to block dangerous commands instead of `ask` rules |
187
+ | Plans stored in `~/.claude/` with random names | [#12619](https://github.com/anthropics/claude-code/issues/12619) (163👍) | `npx cc-safe-setup --install-example plan-repo-sync` |
187
188
 
188
189
  ## How It Works
189
190
 
@@ -0,0 +1,8 @@
1
+ INPUT=$(cat)
2
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
3
+ [[ -z "$COMMAND" ]] && exit 0
4
+ if echo "$COMMAND" | grep -qE 'ansible-vault\s+decrypt\b'; then
5
+ echo "WARNING: Decrypting Ansible vault." >&2
6
+ echo "Remember to re-encrypt before committing." >&2
7
+ fi
8
+ exit 0
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # api-rate-limit-guard.sh — Throttle rapid API calls to prevent rate limiting
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Claude often makes rapid successive curl/API calls that trigger
7
+ # rate limits (429 Too Many Requests). This hook tracks the last
8
+ # call time and enforces a minimum interval between API requests.
9
+ #
10
+ # Default: 1 second between curl/wget/httpie calls.
11
+ # Customize MIN_INTERVAL_MS for your API's rate limit.
12
+ #
13
+ # TRIGGER: PreToolUse MATCHER: "Bash"
14
+ #
15
+ # Usage:
16
+ # {
17
+ # "hooks": {
18
+ # "PreToolUse": [{
19
+ # "matcher": "Bash",
20
+ # "hooks": [{
21
+ # "type": "command",
22
+ # "if": "Bash(curl *)",
23
+ # "command": "~/.claude/hooks/api-rate-limit-guard.sh"
24
+ # }]
25
+ # }]
26
+ # }
27
+ # }
28
+ # ================================================================
29
+
30
+ INPUT=$(cat)
31
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
32
+ [ -z "$COMMAND" ] && exit 0
33
+
34
+ # Only check HTTP client commands
35
+ echo "$COMMAND" | grep -qE '^\s*(curl|wget|http|https)\s' || exit 0
36
+
37
+ # Configurable minimum interval (milliseconds)
38
+ MIN_INTERVAL_MS="${CC_API_RATE_LIMIT_MS:-1000}"
39
+
40
+ TIMESTAMP_FILE="/tmp/.cc-api-rate-limit-$$"
41
+ NOW_MS=$(date +%s%N | cut -b1-13 2>/dev/null || date +%s)
42
+
43
+ if [ -f "$TIMESTAMP_FILE" ]; then
44
+ LAST_MS=$(cat "$TIMESTAMP_FILE" 2>/dev/null || echo "0")
45
+ DIFF=$((NOW_MS - LAST_MS))
46
+ if [ "$DIFF" -lt "$MIN_INTERVAL_MS" ] 2>/dev/null; then
47
+ WAIT=$((MIN_INTERVAL_MS - DIFF))
48
+ echo "⚠ Rate limit guard: ${WAIT}ms cooldown remaining." >&2
49
+ echo " Set CC_API_RATE_LIMIT_MS to adjust (current: ${MIN_INTERVAL_MS}ms)." >&2
50
+ # Note: exit 0 = warn only. Change to exit 2 to hard-block.
51
+ fi
52
+ fi
53
+
54
+ echo "$NOW_MS" > "$TIMESTAMP_FILE"
55
+ exit 0
@@ -0,0 +1,63 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # bash-timeout-guard.sh — Warn on commands likely to hang or run
4
+ # indefinitely without a timeout
5
+ #
6
+ # Solves: Claude running commands that hang forever (e.g., servers,
7
+ # watchers, interactive tools) causing the session to stall.
8
+ # Common pattern: `npm start`, `python app.py`, `tail -f`,
9
+ # `docker logs -f`, or `while true` loops.
10
+ #
11
+ # This hook warns (but doesn't block) when a command looks like
12
+ # it will run indefinitely, suggesting `timeout` prefix.
13
+ #
14
+ # Usage: Add to settings.json as a PreToolUse hook
15
+ #
16
+ # {
17
+ # "hooks": {
18
+ # "PreToolUse": [{
19
+ # "matcher": "Bash",
20
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/bash-timeout-guard.sh" }]
21
+ # }]
22
+ # }
23
+ # }
24
+ # ================================================================
25
+
26
+ INPUT=$(cat)
27
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
28
+
29
+ [[ -z "$COMMAND" ]] && exit 0
30
+
31
+ # Already has timeout — good
32
+ if echo "$COMMAND" | grep -qE '^\s*timeout\s'; then
33
+ exit 0
34
+ fi
35
+
36
+ # Detect commands that typically run forever
37
+ INFINITE=0
38
+ REASON=""
39
+
40
+ # Server/watcher start commands
41
+ if echo "$COMMAND" | grep -qE '(npm|yarn|pnpm)\s+(start|run\s+dev|run\s+serve)'; then
42
+ INFINITE=1; REASON="dev server (runs indefinitely)"
43
+ elif echo "$COMMAND" | grep -qE 'python\s+.*\b(app|server|manage\.py\s+runserver|flask\s+run|uvicorn|gunicorn)'; then
44
+ INFINITE=1; REASON="Python server"
45
+ elif echo "$COMMAND" | grep -qE 'node\s+.*\b(server|app|index)\b'; then
46
+ INFINITE=1; REASON="Node.js server"
47
+ elif echo "$COMMAND" | grep -qE '(tail|docker\s+logs)\s+-f'; then
48
+ INFINITE=1; REASON="follow mode (runs indefinitely)"
49
+ elif echo "$COMMAND" | grep -qE 'while\s+(true|:|\[\s*1\s*\])'; then
50
+ INFINITE=1; REASON="infinite loop"
51
+ elif echo "$COMMAND" | grep -qE '(nc|netcat|ncat)\s+.*-l'; then
52
+ INFINITE=1; REASON="network listener"
53
+ elif echo "$COMMAND" | grep -qE 'inotifywait|fswatch|watchman'; then
54
+ INFINITE=1; REASON="file watcher"
55
+ fi
56
+
57
+ if [[ "$INFINITE" -eq 1 ]]; then
58
+ echo "WARNING: This command may run indefinitely ($REASON)." >&2
59
+ echo "Command: $COMMAND" >&2
60
+ echo "Consider: timeout 30 $COMMAND" >&2
61
+ fi
62
+
63
+ exit 0
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+ # bulk-file-delete-guard.sh — Block commands that delete many files at once
3
+ #
4
+ # Solves: Agent deleting thousands of untracked files without user consent
5
+ # (#23913 — 2,229 files deleted with rm -rf and Remove-Item)
6
+ #
7
+ # How it works: Detects recursive delete patterns and estimates the number
8
+ # of files that would be affected. Blocks if above threshold.
9
+ #
10
+ # Default threshold: 10 files. Change THRESHOLD below.
11
+ #
12
+ # Patterns detected:
13
+ # - rm -rf / rm -r with wildcards or broad paths
14
+ # - find ... -delete / find ... -exec rm
15
+ # - Remove-Item -Recurse (PowerShell)
16
+ # - git clean -fd (removes untracked files)
17
+ #
18
+ # TRIGGER: PreToolUse MATCHER: "Bash"
19
+
20
+ INPUT=$(cat)
21
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
22
+ [[ -z "$COMMAND" ]] && exit 0
23
+
24
+ THRESHOLD=10
25
+
26
+ # Check for recursive delete patterns
27
+ IS_BULK=0
28
+ TARGET=""
29
+
30
+ # rm -rf / rm -r with wildcards or broad directory paths
31
+ if echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f?|(-[a-zA-Z]*f[a-zA-Z]*r))\s'; then
32
+ # Extract the target path
33
+ TARGET=$(echo "$COMMAND" | grep -oE 'rm\s+-[a-zA-Z]+\s+(.+)' | sed 's/rm\s\+-[a-zA-Z]\+\s\+//')
34
+ IS_BULK=1
35
+ fi
36
+
37
+ # find ... -delete
38
+ if echo "$COMMAND" | grep -qE 'find\s+.*-delete'; then
39
+ TARGET=$(echo "$COMMAND" | grep -oE 'find\s+(\S+)' | sed 's/find\s\+//')
40
+ IS_BULK=1
41
+ fi
42
+
43
+ # find ... -exec rm
44
+ if echo "$COMMAND" | grep -qE 'find\s+.*-exec\s+rm'; then
45
+ TARGET=$(echo "$COMMAND" | grep -oE 'find\s+(\S+)' | sed 's/find\s\+//')
46
+ IS_BULK=1
47
+ fi
48
+
49
+ # Remove-Item -Recurse (PowerShell)
50
+ if echo "$COMMAND" | grep -qiE 'Remove-Item.*-Recurse|Remove-Item.*-r\b'; then
51
+ IS_BULK=1
52
+ fi
53
+
54
+ # git clean -fd (removes untracked files)
55
+ if echo "$COMMAND" | grep -qE 'git\s+clean\s+-[a-zA-Z]*[fd]'; then
56
+ IS_BULK=1
57
+ # Count untracked files
58
+ UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l)
59
+ if [[ "$UNTRACKED" -gt "$THRESHOLD" ]]; then
60
+ echo "BLOCKED: git clean would delete $UNTRACKED untracked files (threshold: $THRESHOLD)" >&2
61
+ echo "Command: $COMMAND" >&2
62
+ echo "" >&2
63
+ echo "Review files first: git ls-files --others --exclude-standard" >&2
64
+ exit 2
65
+ fi
66
+ exit 0
67
+ fi
68
+
69
+ if [[ "$IS_BULK" -eq 1 ]]; then
70
+ # Try to count affected files
71
+ if [[ -n "$TARGET" ]] && [[ -d "$TARGET" ]]; then
72
+ COUNT=$(find "$TARGET" -type f 2>/dev/null | head -$((THRESHOLD + 1)) | wc -l)
73
+ if [[ "$COUNT" -gt "$THRESHOLD" ]]; then
74
+ echo "BLOCKED: Recursive delete would affect $COUNT+ files (threshold: $THRESHOLD)" >&2
75
+ echo "Command: $COMMAND" >&2
76
+ echo "Target: $TARGET" >&2
77
+ echo "" >&2
78
+ echo "Delete specific files instead of using recursive patterns." >&2
79
+ exit 2
80
+ fi
81
+ else
82
+ # Can't count files (target doesn't exist or is a glob), warn
83
+ echo "WARNING: Recursive delete detected but can't estimate impact." >&2
84
+ echo "Command: $COMMAND" >&2
85
+ fi
86
+ fi
87
+
88
+ exit 0
@@ -0,0 +1,10 @@
1
+ INPUT=$(cat)
2
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
3
+ [[ -z "$COMMAND" ]] && exit 0
4
+ if echo "$COMMAND" | grep -qE 'cargo\s+publish\b' && ! echo "$COMMAND" | grep -q "\-\-dry-run"; then
5
+ echo "BLOCKED: cargo publish to crates.io." >&2
6
+ echo "Command: $COMMAND" >&2
7
+ echo "Use: cargo publish --dry-run (to test first)" >&2
8
+ exit 2
9
+ fi
10
+ exit 0
@@ -0,0 +1,82 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # check-test-exists.sh — Warn when editing code without a test file
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # When Claude edits a source file, check if a corresponding test
7
+ # file exists. If not, warn that the change is untested. This
8
+ # catches the common pattern where Claude modifies code but skips
9
+ # adding or updating tests.
10
+ #
11
+ # Supports: JS/TS (*.test.*, *.spec.*), Python (*_test.py, test_*),
12
+ # Go (*_test.go), Ruby (*_spec.rb), Java (*Test.java)
13
+ #
14
+ # TRIGGER: PostToolUse MATCHER: "Edit|Write"
15
+ #
16
+ # Usage:
17
+ # {
18
+ # "hooks": {
19
+ # "PostToolUse": [{
20
+ # "matcher": "Edit|Write",
21
+ # "hooks": [{
22
+ # "type": "command",
23
+ # "command": "~/.claude/hooks/check-test-exists.sh"
24
+ # }]
25
+ # }]
26
+ # }
27
+ # }
28
+ # ================================================================
29
+
30
+ INPUT=$(cat)
31
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
32
+ [ -z "$FILE" ] && exit 0
33
+
34
+ # Skip test files themselves, configs, docs
35
+ case "$FILE" in
36
+ *.test.*|*.spec.*|*_test.*|test_*|*Test.java|*_spec.rb) exit 0 ;;
37
+ *.md|*.json|*.yaml|*.yml|*.toml|*.cfg|*.ini|*.env*) exit 0 ;;
38
+ *.css|*.scss|*.html|*.svg|*.png|*.jpg) exit 0 ;;
39
+ esac
40
+
41
+ DIR=$(dirname "$FILE")
42
+ BASE=$(basename "$FILE")
43
+ NAME="${BASE%.*}"
44
+ EXT="${BASE##*.}"
45
+
46
+ # Check for corresponding test file
47
+ FOUND=0
48
+ case "$EXT" in
49
+ js|jsx|ts|tsx|mjs)
50
+ for pattern in "$DIR/$NAME.test.$EXT" "$DIR/$NAME.spec.$EXT" "$DIR/__tests__/$NAME.$EXT" "$DIR/../__tests__/$BASE"; do
51
+ [ -f "$pattern" ] && FOUND=1 && break
52
+ done
53
+ ;;
54
+ py)
55
+ for pattern in "$DIR/test_$BASE" "$DIR/${NAME}_test.py" "$DIR/tests/test_$BASE" "$DIR/../tests/test_$BASE"; do
56
+ [ -f "$pattern" ] && FOUND=1 && break
57
+ done
58
+ ;;
59
+ go)
60
+ [ -f "$DIR/${NAME}_test.go" ] && FOUND=1
61
+ ;;
62
+ rb)
63
+ for pattern in "$DIR/${NAME}_spec.rb" "$DIR/../spec/${NAME}_spec.rb"; do
64
+ [ -f "$pattern" ] && FOUND=1 && break
65
+ done
66
+ ;;
67
+ java)
68
+ for pattern in "$DIR/${NAME}Test.java" "$DIR/../test/${NAME}Test.java"; do
69
+ [ -f "$pattern" ] && FOUND=1 && break
70
+ done
71
+ ;;
72
+ *)
73
+ exit 0 # Unknown language, skip
74
+ ;;
75
+ esac
76
+
77
+ if [ "$FOUND" -eq 0 ]; then
78
+ echo "⚠ No test file found for $BASE" >&2
79
+ echo " Consider adding tests before committing this change." >&2
80
+ fi
81
+
82
+ exit 0
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # chmod-guard.sh — Block overly permissive chmod commands
4
+ #
5
+ # Solves: Claude running chmod 777 or chmod a+rwx on project files,
6
+ # creating security vulnerabilities. World-writable files are a
7
+ # common attack vector and violate least-privilege principles.
8
+ #
9
+ # Blocks: chmod 777, chmod 666, chmod a+w, chmod o+w
10
+ # Allows: chmod +x (make executable), chmod 755, chmod 644
11
+ #
12
+ # Usage: Add to settings.json as a PreToolUse hook
13
+ #
14
+ # {
15
+ # "hooks": {
16
+ # "PreToolUse": [{
17
+ # "matcher": "Bash",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/chmod-guard.sh" }]
19
+ # }]
20
+ # }
21
+ # }
22
+ # ================================================================
23
+
24
+ INPUT=$(cat)
25
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
26
+
27
+ [[ -z "$COMMAND" ]] && exit 0
28
+
29
+ # Only check actual chmod commands (not inside echo/printf/comments)
30
+ ACTUAL_CMD=$(echo "$COMMAND" | sed 's/echo .*//; s/printf .*//; s/#.*//')
31
+ if ! echo "$ACTUAL_CMD" | grep -qE '\bchmod\b'; then
32
+ exit 0
33
+ fi
34
+
35
+ # Block world-writable permissions
36
+ if echo "$ACTUAL_CMD" | grep -qE 'chmod\s+(777|666|a\+[rwx]*w|o\+[rwx]*w)'; then
37
+ echo "BLOCKED: World-writable permissions detected." >&2
38
+ echo "Command: $COMMAND" >&2
39
+ echo "" >&2
40
+ echo "chmod 777/666 creates security vulnerabilities." >&2
41
+ echo "Use instead: chmod 755 (dirs) or chmod 644 (files)." >&2
42
+ exit 2
43
+ fi
44
+
45
+ exit 0
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # chown-guard.sh — Block dangerous ownership changes
4
+ #
5
+ # Solves: Claude running chown root or recursive chown on system
6
+ # directories, which can break file permissions and lock the user
7
+ # out of their own files.
8
+ #
9
+ # Blocks: chown root, chown -R on system paths, chown on /etc /var
10
+ # Allows: chown on project files
11
+ #
12
+ # Usage: Add to settings.json as a PreToolUse hook
13
+ #
14
+ # {
15
+ # "hooks": {
16
+ # "PreToolUse": [{
17
+ # "matcher": "Bash",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/chown-guard.sh" }]
19
+ # }]
20
+ # }
21
+ # }
22
+ # ================================================================
23
+
24
+ INPUT=$(cat)
25
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
26
+
27
+ [[ -z "$COMMAND" ]] && exit 0
28
+
29
+ # Only check actual chown commands (not inside echo/printf/comments)
30
+ ACTUAL_CMD=$(echo "$COMMAND" | sed 's/echo .*//; s/printf .*//; s/#.*//')
31
+ if ! echo "$ACTUAL_CMD" | grep -qE '\bchown\b'; then
32
+ exit 0
33
+ fi
34
+
35
+ # Block chown to root
36
+ if echo "$ACTUAL_CMD" | grep -qE 'chown\s+(-R\s+)?root[: ]'; then
37
+ echo "BLOCKED: Changing ownership to root." >&2
38
+ echo "Command: $COMMAND" >&2
39
+ echo "This can lock you out of your files." >&2
40
+ exit 2
41
+ fi
42
+
43
+ # Block recursive chown on system directories
44
+ if echo "$ACTUAL_CMD" | grep -qE 'chown\s+-R.*\s+/(etc|var|usr|bin|sbin|lib|boot|sys|proc|dev)\b'; then
45
+ echo "BLOCKED: Recursive chown on system directory." >&2
46
+ echo "Command: $COMMAND" >&2
47
+ exit 2
48
+ fi
49
+
50
+ # Block chown on home directory root
51
+ if echo "$ACTUAL_CMD" | grep -qE 'chown\s+-R.*\s+(~|/home/\w+)\s*$'; then
52
+ echo "BLOCKED: Recursive chown on entire home directory." >&2
53
+ echo "Command: $COMMAND" >&2
54
+ exit 2
55
+ fi
56
+
57
+ exit 0
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # composer-guard.sh — Block dangerous Composer operations
4
+ #
5
+ # Blocks: composer global require (affects system PHP),
6
+ # composer remove (accidental dependency removal)
7
+ # Warns: composer require without --dev flag
8
+ #
9
+ # Usage: Add to settings.json as a PreToolUse hook
10
+ #
11
+ # {
12
+ # "hooks": {
13
+ # "PreToolUse": [{
14
+ # "matcher": "Bash",
15
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/composer-guard.sh" }]
16
+ # }]
17
+ # }
18
+ # }
19
+ # ================================================================
20
+
21
+ INPUT=$(cat)
22
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
23
+
24
+ [[ -z "$COMMAND" ]] && exit 0
25
+
26
+ # Block global require
27
+ if echo "$COMMAND" | grep -qE 'composer\s+global\s+require'; then
28
+ echo "BLOCKED: Global Composer package installation." >&2
29
+ echo "Command: $COMMAND" >&2
30
+ echo "Global packages affect the entire system." >&2
31
+ echo "Use: composer require <package> (local project only)" >&2
32
+ exit 2
33
+ fi
34
+
35
+ exit 0
@@ -0,0 +1,11 @@
1
+ INPUT=$(cat)
2
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
3
+ [[ -z "$FILE" ]] && exit 0
4
+ [[ ! -f "$FILE" ]] && exit 0
5
+ case "$FILE" in *.ts|*.tsx|*.js|*.jsx) ;; *) exit 0 ;; esac
6
+ COUNT=$(grep -c 'console\.log' "$FILE" 2>/dev/null)
7
+ if [[ "$COUNT" -gt 5 ]]; then
8
+ echo "WARNING: $COUNT console.log statements in $(basename "$FILE")." >&2
9
+ echo "Consider cleaning up debug logs before committing." >&2
10
+ fi
11
+ exit 0
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # django-migrate-guard.sh — Block destructive Django DB operations
4
+ #
5
+ # Blocks: manage.py flush, manage.py sqlflush, manage.py reset
6
+ # Warns: manage.py migrate --fake
7
+ # Allows: manage.py migrate, manage.py makemigrations
8
+ #
9
+ # Usage: Add to settings.json as a PreToolUse hook
10
+ #
11
+ # {
12
+ # "hooks": {
13
+ # "PreToolUse": [{
14
+ # "matcher": "Bash",
15
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/django-migrate-guard.sh" }]
16
+ # }]
17
+ # }
18
+ # }
19
+ # ================================================================
20
+
21
+ INPUT=$(cat)
22
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
23
+
24
+ [[ -z "$COMMAND" ]] && exit 0
25
+
26
+ # Block destructive Django commands
27
+ if echo "$COMMAND" | grep -qE 'manage\.py\s+(flush|sqlflush)\b'; then
28
+ echo "BLOCKED: Django flush destroys all data." >&2
29
+ echo "Command: $COMMAND" >&2
30
+ exit 2
31
+ fi
32
+
33
+ # Warn on fake migrations
34
+ if echo "$COMMAND" | grep -qE 'manage\.py\s+migrate\s+.*--fake'; then
35
+ echo "WARNING: Fake migration — database schema won't change." >&2
36
+ echo "This can leave DB and migration history out of sync." >&2
37
+ fi
38
+
39
+ exit 0
@@ -0,0 +1,12 @@
1
+ INPUT=$(cat)
2
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
3
+ [[ -z "$FILE" ]] && exit 0
4
+ [[ ! -f "$FILE" ]] && exit 0
5
+ if ! echo "$FILE" | grep -qiE 'Dockerfile'; then exit 0; fi
6
+ LATEST=$(grep -nP '^FROM\s+\S+:latest\b' "$FILE" 2>/dev/null | head -3)
7
+ if [[ -n "$LATEST" ]]; then
8
+ echo "WARNING: :latest tag in Dockerfile:" >&2
9
+ echo "$LATEST" >&2
10
+ echo "Pin to a specific version for reproducible builds." >&2
11
+ fi
12
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # dotnet-build-on-edit.sh — Run dotnet build after C#/F# edits
4
+ #
5
+ # Checks compilation after editing .cs or .fs files.
6
+ # Warns on build errors but doesn't block.
7
+ #
8
+ # Usage: Add to settings.json as a PostToolUse hook
9
+ #
10
+ # {
11
+ # "hooks": {
12
+ # "PostToolUse": [{
13
+ # "matcher": "Edit|Write",
14
+ # "if": "Edit(*.cs) || Edit(*.fs) || Write(*.cs) || Write(*.fs)",
15
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/dotnet-build-on-edit.sh" }]
16
+ # }]
17
+ # }
18
+ # }
19
+ # ================================================================
20
+
21
+ INPUT=$(cat)
22
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
23
+
24
+ [[ -z "$FILE" ]] && exit 0
25
+
26
+ # Only check C#/F# files
27
+ case "$FILE" in
28
+ *.cs|*.fs) ;;
29
+ *) exit 0 ;;
30
+ esac
31
+
32
+ # Check if dotnet is available and we're in a .NET project
33
+ if command -v dotnet &>/dev/null; then
34
+ # Find nearest .csproj or .fsproj
35
+ DIR=$(dirname "$FILE")
36
+ while [[ "$DIR" != "/" ]]; do
37
+ if ls "$DIR"/*.csproj "$DIR"/*.fsproj 2>/dev/null | head -1 | grep -q .; then
38
+ RESULT=$(cd "$DIR" && dotnet build --no-restore -q 2>&1 | tail -5)
39
+ if [[ $? -ne 0 ]]; then
40
+ echo "Build error after editing $(basename "$FILE"):" >&2
41
+ echo "$RESULT" | head -5 >&2
42
+ fi
43
+ break
44
+ fi
45
+ DIR=$(dirname "$DIR")
46
+ done
47
+ fi
48
+
49
+ exit 0
@@ -0,0 +1,32 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # drizzle-migrate-guard.sh — Block destructive Drizzle ORM operations
4
+ #
5
+ # Blocks: drizzle-kit drop, drizzle-kit push with --force
6
+ # Allows: drizzle-kit generate, drizzle-kit migrate
7
+ #
8
+ # Usage: Add to settings.json as a PreToolUse hook
9
+ #
10
+ # {
11
+ # "hooks": {
12
+ # "PreToolUse": [{
13
+ # "matcher": "Bash",
14
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/drizzle-migrate-guard.sh" }]
15
+ # }]
16
+ # }
17
+ # }
18
+ # ================================================================
19
+
20
+ INPUT=$(cat)
21
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
22
+
23
+ [[ -z "$COMMAND" ]] && exit 0
24
+
25
+ # Block destructive Drizzle commands
26
+ if echo "$COMMAND" | grep -qE 'drizzle-kit\s+drop'; then
27
+ echo "BLOCKED: drizzle-kit drop destroys migration files." >&2
28
+ echo "Command: $COMMAND" >&2
29
+ exit 2
30
+ fi
31
+
32
+ exit 0