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.
- package/README.md +3 -2
- package/examples/ansible-vault-guard.sh +8 -0
- package/examples/api-rate-limit-guard.sh +55 -0
- package/examples/bash-timeout-guard.sh +63 -0
- package/examples/bulk-file-delete-guard.sh +88 -0
- package/examples/cargo-publish-guard.sh +10 -0
- package/examples/check-test-exists.sh +82 -0
- package/examples/chmod-guard.sh +45 -0
- package/examples/chown-guard.sh +57 -0
- package/examples/composer-guard.sh +35 -0
- package/examples/console-log-count.sh +11 -0
- package/examples/django-migrate-guard.sh +39 -0
- package/examples/dockerfile-latest-guard.sh +12 -0
- package/examples/dotnet-build-on-edit.sh +49 -0
- package/examples/drizzle-migrate-guard.sh +32 -0
- package/examples/edit-error-counter.sh +54 -0
- package/examples/env-inherit-guard.sh +66 -0
- package/examples/expo-eject-guard.sh +10 -0
- package/examples/file-change-monitor.sh +32 -0
- package/examples/file-reference-check.sh +66 -0
- package/examples/five-hundred-milestone.sh +8 -0
- package/examples/flask-debug-guard.sh +33 -0
- package/examples/gem-push-guard.sh +10 -0
- package/examples/git-stash-before-checkout.sh +53 -0
- package/examples/go-mod-tidy-warn.sh +8 -0
- package/examples/hallucination-url-check.sh +68 -0
- package/examples/hardcoded-ip-guard.sh +12 -0
- package/examples/helm-install-guard.sh +8 -0
- package/examples/java-compile-on-edit.sh +39 -0
- package/examples/laravel-artisan-guard.sh +11 -0
- package/examples/long-session-reminder.sh +49 -0
- package/examples/magic-number-warn.sh +12 -0
- package/examples/max-function-length.sh +8 -4
- package/examples/monorepo-scope-guard.sh +70 -0
- package/examples/nextjs-env-guard.sh +58 -0
- package/examples/no-any-typescript.sh +12 -0
- package/examples/no-ask-human.sh +51 -0
- package/examples/no-console-log-commit.sh +43 -0
- package/examples/no-cors-wildcard.sh +10 -0
- package/examples/no-dangling-await.sh +11 -0
- package/examples/no-deep-relative-import.sh +12 -0
- package/examples/no-eval-template.sh +11 -0
- package/examples/no-global-install.sh +61 -0
- package/examples/no-hardcoded-port.sh +11 -4
- package/examples/no-http-url.sh +12 -0
- package/examples/no-inline-styles.sh +11 -0
- package/examples/no-root-user-docker.sh +10 -0
- package/examples/no-secrets-in-args.sh +9 -0
- package/examples/no-star-import-python.sh +8 -24
- package/examples/no-todo-in-production.sh +10 -0
- package/examples/no-wget-piped-bash.sh +1 -1
- package/examples/nuxt-config-guard.sh +7 -0
- package/examples/parallel-session-guard.sh +58 -0
- package/examples/php-lint-on-edit.sh +36 -0
- package/examples/pip-publish-guard.sh +10 -0
- package/examples/plan-repo-sync.sh +86 -0
- package/examples/prisma-migrate-guard.sh +41 -0
- package/examples/rails-migration-guard.sh +42 -0
- package/examples/redis-flushall-guard.sh +9 -0
- package/examples/ruby-lint-on-edit.sh +36 -0
- package/examples/sensitive-log-guard.sh +12 -0
- package/examples/spring-profile-guard.sh +8 -0
- package/examples/staged-secret-scan.sh +91 -0
- package/examples/svelte-lint-on-edit.sh +9 -0
- package/examples/swift-build-on-edit.sh +36 -0
- package/examples/system-package-guard.sh +42 -0
- package/examples/test-after-edit.sh +48 -0
- package/examples/turbo-cache-guard.sh +36 -0
- package/examples/vue-lint-on-edit.sh +11 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/cc-safe-setup)
|
|
5
5
|
[](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
|
|
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
|