cc-safe-setup 29.6.0 → 29.6.1

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 (75) 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/block-database-wipe.sh +1 -1
  11. package/examples/classifier-fallback-allow.sh +70 -0
  12. package/examples/commit-message-check.sh +8 -1
  13. package/examples/commit-message-quality.sh +35 -0
  14. package/examples/credential-exfil-guard.sh +12 -0
  15. package/examples/cwd-reminder.sh +37 -0
  16. package/examples/dependency-install-guard.sh +84 -0
  17. package/examples/deploy-guard.sh +1 -1
  18. package/examples/detect-mixed-indentation.sh +33 -0
  19. package/examples/disk-space-check.sh +42 -0
  20. package/examples/docker-dangerous-guard.sh +47 -0
  21. package/examples/dockerfile-lint.sh +58 -0
  22. package/examples/edit-always-allow.sh +53 -0
  23. package/examples/env-file-gitignore-check.sh +39 -0
  24. package/examples/env-source-guard.sh +1 -1
  25. package/examples/git-stash-before-danger.sh +58 -0
  26. package/examples/github-actions-guard.sh +49 -0
  27. package/examples/gitignore-auto-add.sh +30 -0
  28. package/examples/go-vet-after-edit.sh +33 -0
  29. package/examples/hook-tamper-guard.sh +67 -0
  30. package/examples/kubernetes-guard.sh +2 -1
  31. package/examples/large-file-write-guard.sh +40 -0
  32. package/examples/main-branch-warn.sh +40 -0
  33. package/examples/max-edit-size-guard.sh +9 -15
  34. package/examples/mcp-server-guard.sh +70 -0
  35. package/examples/multiline-command-approver.sh +89 -0
  36. package/examples/no-base64-exfil.sh +27 -0
  37. package/examples/no-debug-commit.sh +60 -0
  38. package/examples/no-exposed-port-in-dockerfile.sh +32 -0
  39. package/examples/no-fixme-ship.sh +41 -0
  40. package/examples/no-hardcoded-ip.sh +26 -0
  41. package/examples/no-http-in-code.sh +19 -0
  42. package/examples/no-push-without-tests.sh +33 -0
  43. package/examples/no-self-signed-cert.sh +19 -0
  44. package/examples/no-star-import-python.sh +28 -0
  45. package/examples/no-wget-piped-bash.sh +22 -0
  46. package/examples/node-version-check.sh +40 -0
  47. package/examples/npm-publish-guard.sh +5 -2
  48. package/examples/output-token-env-check.sh +44 -0
  49. package/examples/package-lock-frozen.sh +25 -0
  50. package/examples/pip-venv-required.sh +40 -0
  51. package/examples/port-conflict-check.sh +62 -0
  52. package/examples/prefer-builtin-tools.sh +33 -0
  53. package/examples/python-import-check.sh +52 -0
  54. package/examples/python-ruff-on-edit.sh +51 -0
  55. package/examples/quoted-flag-approver.sh +51 -0
  56. package/examples/react-key-warn.sh +32 -0
  57. package/examples/rm-safety-net.sh +9 -0
  58. package/examples/rust-clippy-after-edit.sh +37 -0
  59. package/examples/session-quota-tracker.sh +44 -0
  60. package/examples/session-start-safety-check.sh +60 -0
  61. package/examples/session-summary-stop.sh +49 -0
  62. package/examples/session-time-limit.sh +34 -0
  63. package/examples/temp-file-cleanup.sh +41 -0
  64. package/examples/test-before-push.sh +8 -1
  65. package/examples/test-coverage-reminder.sh +49 -0
  66. package/examples/test-exit-code-verify.sh +60 -0
  67. package/examples/tool-file-logger.sh +46 -0
  68. package/examples/typescript-lint-on-edit.sh +61 -0
  69. package/examples/typescript-strict-check.sh +35 -0
  70. package/examples/uncommitted-changes-stop.sh +16 -0
  71. package/examples/uncommitted-discard-guard.sh +72 -0
  72. package/examples/worktree-unmerged-guard.sh +13 -3
  73. package/examples/yaml-syntax-check.sh +50 -0
  74. package/index.mjs +3 -0
  75. package/package.json +2 -2
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # cwd-reminder.sh — Remind Claude of the current working directory
3
+ #
4
+ # Solves: Claude loses track of which directory it's in (#1669 — 71 reactions)
5
+ # Can lead to commands running in wrong directory, including
6
+ # destructive operations like git reset in the wrong repo.
7
+ #
8
+ # Emits the current working directory to stderr before every Bash command,
9
+ # making it visible in the tool output so Claude always knows where it is.
10
+ #
11
+ # TRIGGER: PreToolUse
12
+ # MATCHER: "Bash"
13
+ #
14
+ # Usage:
15
+ # {
16
+ # "hooks": {
17
+ # "PreToolUse": [{
18
+ # "matcher": "Bash",
19
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/cwd-reminder.sh" }]
20
+ # }]
21
+ # }
22
+ # }
23
+
24
+ INPUT=$(cat)
25
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
26
+
27
+ # Don't add noise to empty commands
28
+ [ -z "$COMMAND" ] && exit 0
29
+
30
+ # Get the working directory from the tool input if available,
31
+ # otherwise use the process's cwd
32
+ CWD=$(echo "$INPUT" | jq -r '.tool_input.working_directory // empty' 2>/dev/null)
33
+ [ -z "$CWD" ] && CWD=$(pwd)
34
+
35
+ echo "[cwd: $CWD]" >&2
36
+
37
+ exit 0
@@ -0,0 +1,84 @@
1
+ #!/bin/bash
2
+ # dependency-install-guard.sh — PreToolUse hook
3
+ # Trigger: PreToolUse
4
+ # Matcher: Bash
5
+ #
6
+ # Blocks unintended dependency installations (npm install, pip install,
7
+ # gem install, cargo add, go get). Prevents:
8
+ # - Supply chain attacks from unknown packages
9
+ # - Dependency bloat from unnecessary installations
10
+ # - Breaking lockfiles with unplanned additions
11
+ #
12
+ # Allowed:
13
+ # - npm install (no args) — installs from existing lockfile
14
+ # - npm ci — clean install from lockfile
15
+ # - pip install -r requirements.txt — from requirements file
16
+ # - Packages in ALLOWLIST (customize below)
17
+ #
18
+ # Usage: Add to settings.json as a PreToolUse hook on "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
+ # Normalize: collapse whitespace, extract first logical command
25
+ CMD=$(echo "$COMMAND" | tr '\n' ' ' | sed 's/ */ /g')
26
+
27
+ # --- Allowlist: packages you trust ---
28
+ # Customize this list for your project
29
+ ALLOWLIST="typescript|eslint|prettier|jest|vitest|@types/"
30
+
31
+ # npm install <package> — block unless allowlisted
32
+ if echo "$CMD" | grep -qiE 'npm\s+(install|i|add)\s+[a-z@]'; then
33
+ PKG=$(echo "$CMD" | grep -oiE 'npm\s+(install|i|add)\s+\S+' | awk '{print $NF}')
34
+ if echo "$PKG" | grep -qiE "^($ALLOWLIST)"; then
35
+ exit 0
36
+ fi
37
+ echo "🚫 Blocked: npm install $PKG (not in allowlist)" >&2
38
+ echo "Add to ALLOWLIST in dependency-install-guard.sh if intended." >&2
39
+ exit 2
40
+ fi
41
+
42
+ # npm install (no args) / npm ci — allowed (uses lockfile)
43
+ if echo "$CMD" | grep -qiE 'npm\s+(install|i|ci)\s*($|[&|;])'; then
44
+ exit 0
45
+ fi
46
+
47
+ # pip install <package> — block unless from requirements
48
+ if echo "$CMD" | grep -qiE 'pip3?\s+install\s+'; then
49
+ # Allow: pip install -r requirements.txt
50
+ if echo "$CMD" | grep -qiE 'pip3?\s+install\s+(-r|--requirement)\s+'; then
51
+ exit 0
52
+ fi
53
+ # Allow: pip install -e . (editable install)
54
+ if echo "$CMD" | grep -qiE 'pip3?\s+install\s+(-e|--editable)\s+'; then
55
+ exit 0
56
+ fi
57
+ PKG=$(echo "$CMD" | grep -oiE 'pip3?\s+install\s+\S+' | awk '{print $NF}')
58
+ echo "🚫 Blocked: pip install $PKG" >&2
59
+ echo "Use 'pip install -r requirements.txt' or add to allowlist." >&2
60
+ exit 2
61
+ fi
62
+
63
+ # gem install — block
64
+ if echo "$CMD" | grep -qiE 'gem\s+install\s+[a-z]'; then
65
+ PKG=$(echo "$CMD" | grep -oiE 'gem\s+install\s+\S+' | awk '{print $NF}')
66
+ echo "🚫 Blocked: gem install $PKG" >&2
67
+ exit 2
68
+ fi
69
+
70
+ # cargo add — block
71
+ if echo "$CMD" | grep -qiE 'cargo\s+add\s+[a-z]'; then
72
+ PKG=$(echo "$CMD" | grep -oiE 'cargo\s+add\s+\S+' | awk '{print $NF}')
73
+ echo "🚫 Blocked: cargo add $PKG" >&2
74
+ exit 2
75
+ fi
76
+
77
+ # go get — block
78
+ if echo "$CMD" | grep -qiE 'go\s+get\s+[a-z]'; then
79
+ PKG=$(echo "$CMD" | grep -oiE 'go\s+get\s+\S+' | awk '{print $NF}')
80
+ echo "🚫 Blocked: go get $PKG" >&2
81
+ exit 2
82
+ fi
83
+
84
+ exit 0
@@ -24,7 +24,7 @@ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
24
24
  [[ -z "$COMMAND" ]] && exit 0
25
25
 
26
26
  # Detect deploy commands
27
- if ! echo "$COMMAND" | grep -qiE '(rsync|scp|deploy|firebase\s+deploy|vercel|netlify\s+deploy|fly\s+deploy|railway\s+up|git\s+push\s+heroku)'; then
27
+ if ! echo "$COMMAND" | grep -qiE '(rsync|scp|deploy|firebase\s+deploy|vercel|netlify\s+deploy|fly\s+deploy|railway\s+up|git\s+push\s+heroku|kubectl\s+(apply|create|delete|rollout)|terraform\s+(apply|destroy))'; then
28
28
  exit 0
29
29
  fi
30
30
 
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # detect-mixed-indentation.sh — Warn about mixed tabs/spaces
3
+ #
4
+ # Prevents: Indentation errors from mixing tabs and spaces.
5
+ # Common when Claude pastes code from different sources.
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
+ [ ! -f "$FILE" ] && exit 0
14
+
15
+ # Skip binary files and makefiles (which require tabs)
16
+ case "$(basename "$FILE")" in
17
+ Makefile|makefile|GNUmakefile) exit 0 ;;
18
+ esac
19
+
20
+ case "$FILE" in
21
+ *.py|*.js|*.ts|*.tsx|*.jsx|*.yaml|*.yml|*.rb|*.go) ;;
22
+ *) exit 0 ;;
23
+ esac
24
+
25
+ HAS_TABS=$(grep -cP '^\t' "$FILE" 2>/dev/null || echo 0)
26
+ HAS_SPACES=$(grep -cP '^ {2,}' "$FILE" 2>/dev/null || echo 0)
27
+
28
+ if [ "$HAS_TABS" -gt 0 ] && [ "$HAS_SPACES" -gt 0 ]; then
29
+ echo "WARNING: Mixed tabs and spaces in $FILE ($HAS_TABS tab-lines, $HAS_SPACES space-lines)." >&2
30
+ echo " Standardize to one indentation style." >&2
31
+ fi
32
+
33
+ exit 0
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # disk-space-check.sh — Warn if disk space is low at session start
3
+ #
4
+ # Prevents: Autonomous sessions crashing due to disk full
5
+ # npm install, git operations, and file writes fail silently
6
+ # when disk space runs out during long-running sessions.
7
+ #
8
+ # Checks: root filesystem usage percentage
9
+ # Warns at: 80% (yellow), 90% (red), 95% (critical)
10
+ #
11
+ # TRIGGER: Notification
12
+ # MATCHER: ""
13
+ #
14
+ # Usage:
15
+ # {
16
+ # "hooks": {
17
+ # "Notification": [{
18
+ # "matcher": "",
19
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/disk-space-check.sh" }]
20
+ # }]
21
+ # }
22
+ # }
23
+
24
+ # Only run once per session
25
+ MARKER="/tmp/cc-disk-check-$$"
26
+ [ -f "$MARKER" ] && exit 0
27
+
28
+ # Get disk usage percentage for root filesystem
29
+ USAGE=$(df / 2>/dev/null | awk 'NR==2 {gsub(/%/,""); print $5}')
30
+ [ -z "$USAGE" ] && exit 0
31
+
32
+ if [ "$USAGE" -ge 95 ]; then
33
+ echo "CRITICAL: Disk usage at ${USAGE}%. Operations may fail." >&2
34
+ echo " Free space immediately: docker system prune, rm tmp files" >&2
35
+ elif [ "$USAGE" -ge 90 ]; then
36
+ echo "WARNING: Disk usage at ${USAGE}%. Consider freeing space." >&2
37
+ elif [ "$USAGE" -ge 80 ]; then
38
+ echo "NOTE: Disk usage at ${USAGE}%." >&2
39
+ fi
40
+
41
+ touch "$MARKER"
42
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ # docker-dangerous-guard.sh — Block dangerous Docker operations
3
+ #
4
+ # Prevents: docker system prune -a, docker rm -f on running containers,
5
+ # docker run --privileged, docker exec as root on production containers.
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/docker-dangerous-guard.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
+ # Block docker system prune -a (removes all images)
25
+ if echo "$COMMAND" | grep -qE 'docker\s+system\s+prune\s+.*-a'; then
26
+ echo "BLOCKED: docker system prune -a removes all unused images." >&2
27
+ echo " Use 'docker system prune' (without -a) to keep tagged images." >&2
28
+ exit 2
29
+ fi
30
+
31
+ # Block docker run --privileged
32
+ if echo "$COMMAND" | grep -qE 'docker\s+run\s+.*--privileged'; then
33
+ echo "BLOCKED: --privileged gives full host access to the container." >&2
34
+ exit 2
35
+ fi
36
+
37
+ # Warn on docker rm -f (force remove)
38
+ if echo "$COMMAND" | grep -qE 'docker\s+(rm|container\s+rm)\s+.*-f'; then
39
+ echo "WARNING: Force-removing container. Data in the container will be lost." >&2
40
+ fi
41
+
42
+ # Block docker run with host network and port 22/80/443
43
+ if echo "$COMMAND" | grep -qE 'docker\s+run.*--network\s+host'; then
44
+ echo "WARNING: --network host exposes all container ports on the host." >&2
45
+ fi
46
+
47
+ exit 0
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # dockerfile-lint.sh — Basic Dockerfile validation after editing
3
+ #
4
+ # Prevents: Common Dockerfile mistakes:
5
+ # - Missing FROM instruction
6
+ # - Using latest tag (non-reproducible builds)
7
+ # - Running as root without explicit USER
8
+ # - COPY/ADD before dependency install (cache invalidation)
9
+ #
10
+ # TRIGGER: PostToolUse
11
+ # MATCHER: "Write|Edit"
12
+ #
13
+ # Usage:
14
+ # {
15
+ # "hooks": {
16
+ # "PostToolUse": [{
17
+ # "matcher": "Write|Edit",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/dockerfile-lint.sh" }]
19
+ # }]
20
+ # }
21
+ # }
22
+
23
+ INPUT=$(cat)
24
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
25
+ [ -z "$FILE" ] && exit 0
26
+
27
+ # Only check Dockerfiles
28
+ BASENAME=$(basename "$FILE")
29
+ case "$BASENAME" in
30
+ Dockerfile|Dockerfile.*|*.dockerfile) ;;
31
+ *) exit 0 ;;
32
+ esac
33
+
34
+ [ ! -f "$FILE" ] && exit 0
35
+
36
+ WARNINGS=""
37
+
38
+ # Check for FROM instruction
39
+ if ! grep -qE '^FROM\s' "$FILE"; then
40
+ WARNINGS="${WARNINGS}\n Missing FROM instruction"
41
+ fi
42
+
43
+ # Check for :latest tag
44
+ if grep -qE '^FROM\s+\S+:latest' "$FILE"; then
45
+ WARNINGS="${WARNINGS}\n Using :latest tag (non-reproducible)"
46
+ fi
47
+
48
+ # Check for no USER instruction (running as root)
49
+ if ! grep -qE '^USER\s' "$FILE"; then
50
+ WARNINGS="${WARNINGS}\n No USER instruction (container runs as root)"
51
+ fi
52
+
53
+ if [ -n "$WARNINGS" ]; then
54
+ echo "Dockerfile warnings in $FILE:" >&2
55
+ echo -e "$WARNINGS" >&2
56
+ fi
57
+
58
+ exit 0
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # edit-always-allow.sh — Auto-approve all Edit prompts in configured directories
3
+ #
4
+ # Solves: --dangerously-skip-permissions doesn't bypass Edit prompts
5
+ # (#36192, #36168 — bypass permissions broken since v2.1.78)
6
+ #
7
+ # Claude Code v2.1.78+ prompts for Edit in .claude/, .git/, .vscode/
8
+ # even with bypassPermissions enabled. This PermissionRequest hook
9
+ # restores the pre-v2.1.78 behavior for specified directories.
10
+ #
11
+ # Configure allowed directories via CC_EDIT_ALLOW_DIRS env var:
12
+ # export CC_EDIT_ALLOW_DIRS=".claude/skills:.claude/commands"
13
+ # Default: .claude/skills
14
+ #
15
+ # TRIGGER: PermissionRequest
16
+ # MATCHER: "Edit|Write"
17
+ #
18
+ # Usage:
19
+ # {
20
+ # "hooks": {
21
+ # "PermissionRequest": [{
22
+ # "matcher": "Edit|Write",
23
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/edit-always-allow.sh" }]
24
+ # }]
25
+ # }
26
+ # }
27
+
28
+ INPUT=$(cat)
29
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
30
+
31
+ # Only handle Edit/Write
32
+ case "$TOOL" in
33
+ Edit|Write) ;;
34
+ *) exit 0 ;;
35
+ esac
36
+
37
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
38
+ [ -z "$FILE" ] && exit 0
39
+
40
+ # Configurable allowed directories (colon-separated)
41
+ ALLOW_DIRS="${CC_EDIT_ALLOW_DIRS:-.claude/skills}"
42
+
43
+ # Check if file is in an allowed directory
44
+ IFS=':' read -ra DIRS <<< "$ALLOW_DIRS"
45
+ for dir in "${DIRS[@]}"; do
46
+ if echo "$FILE" | grep -q "$dir"; then
47
+ echo '{"permissionDecision":"allow"}'
48
+ exit 0
49
+ fi
50
+ done
51
+
52
+ # Not in allowed directories — let the prompt through
53
+ exit 0
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ # env-file-gitignore-check.sh — Warn if .env is not in .gitignore
3
+ #
4
+ # Prevents: Accidental commit of .env files containing secrets.
5
+ # Checks on session start if .env exists but .gitignore
6
+ # doesn't exclude it.
7
+ #
8
+ # TRIGGER: Notification
9
+ # MATCHER: ""
10
+ #
11
+ # Usage:
12
+ # {
13
+ # "hooks": {
14
+ # "Notification": [{
15
+ # "matcher": "",
16
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/env-file-gitignore-check.sh" }]
17
+ # }]
18
+ # }
19
+ # }
20
+
21
+ # Only run once per session
22
+ MARKER="/tmp/cc-env-gitignore-$$"
23
+ [ -f "$MARKER" ] && exit 0
24
+
25
+ # Check if we're in a git repo
26
+ git rev-parse --git-dir >/dev/null 2>&1 || { touch "$MARKER"; exit 0; }
27
+
28
+ # Check if .env exists
29
+ [ -f ".env" ] || { touch "$MARKER"; exit 0; }
30
+
31
+ # Check if .env is in .gitignore
32
+ if ! git check-ignore -q .env 2>/dev/null; then
33
+ echo "WARNING: .env file exists but is not in .gitignore!" >&2
34
+ echo " Add '.env' to .gitignore to prevent accidental commit." >&2
35
+ echo " echo '.env' >> .gitignore" >&2
36
+ fi
37
+
38
+ touch "$MARKER"
39
+ exit 0
@@ -32,7 +32,7 @@ if [[ -z "$COMMAND" ]]; then
32
32
  fi
33
33
 
34
34
  # Block direct sourcing of .env files
35
- if echo "$COMMAND" | grep -qE '(source|\.\s)\s+\.env'; then
35
+ if echo "$COMMAND" | grep -qE '(source|\.)\s+\.env'; then
36
36
  echo "BLOCKED: Sourcing .env into shell environment." >&2
37
37
  echo "Command: $COMMAND" >&2
38
38
  echo "" >&2
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # git-stash-before-danger.sh — Auto-stash before risky git operations
3
+ #
4
+ # Solves: Losing uncommitted work when Claude runs git checkout, git reset, or git pull
5
+ # Related: data loss incidents reported in #36339, #37331
6
+ #
7
+ # How it works: PreToolUse hook that auto-runs `git stash push -m "cc-auto-stash"`
8
+ # before destructive git operations. The stash can be recovered with
9
+ # `git stash pop`.
10
+ #
11
+ # Usage: Add to settings.json as a PreToolUse hook
12
+ #
13
+ # {
14
+ # "hooks": {
15
+ # "PreToolUse": [{
16
+ # "matcher": "Bash",
17
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/git-stash-before-danger.sh" }]
18
+ # }]
19
+ # }
20
+ # }
21
+
22
+ INPUT=$(cat)
23
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
24
+
25
+ [ -z "$COMMAND" ] && exit 0
26
+
27
+ # Only act on risky git operations
28
+ RISKY=false
29
+ if echo "$COMMAND" | grep -qE 'git\s+(checkout|reset|pull|merge|rebase|cherry-pick)\s'; then
30
+ RISKY=true
31
+ fi
32
+
33
+ if [ "$RISKY" = false ]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Check if we're in a git repo with uncommitted changes
38
+ if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
39
+ exit 0
40
+ fi
41
+
42
+ # Check for uncommitted changes
43
+ if git diff --quiet HEAD 2>/dev/null && git diff --cached --quiet 2>/dev/null; then
44
+ # No changes — nothing to stash
45
+ exit 0
46
+ fi
47
+
48
+ # Auto-stash
49
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
50
+ git stash push -m "cc-auto-stash-$TIMESTAMP (before: $COMMAND)" > /dev/null 2>&1
51
+
52
+ if [ $? -eq 0 ]; then
53
+ echo "INFO: Auto-stashed uncommitted changes before risky operation" >&2
54
+ echo "Recovery: git stash pop" >&2
55
+ fi
56
+
57
+ # Don't block — just stash and let the command proceed
58
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # github-actions-guard.sh — Validate GitHub Actions workflow changes
3
+ #
4
+ # Prevents: Broken CI/CD pipelines from workflow syntax errors.
5
+ # Claude sometimes generates invalid workflow YAML.
6
+ #
7
+ # Checks:
8
+ # - Workflow must have 'on' trigger
9
+ # - Job names must exist
10
+ # - 'uses' actions should have version pins
11
+ #
12
+ # TRIGGER: PostToolUse
13
+ # MATCHER: "Write|Edit"
14
+
15
+ INPUT=$(cat)
16
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
17
+ [ -z "$FILE" ] && exit 0
18
+
19
+ # Only check GitHub Actions workflow files
20
+ echo "$FILE" | grep -qE '\.github/workflows/.*\.ya?ml$' || exit 0
21
+ [ ! -f "$FILE" ] && exit 0
22
+
23
+ WARNINGS=""
24
+
25
+ # Check for 'on' trigger
26
+ if ! grep -qE '^on:' "$FILE"; then
27
+ WARNINGS="${WARNINGS}\n Missing 'on:' trigger definition"
28
+ fi
29
+
30
+ # Check for unpinned actions (uses: without @sha or @v)
31
+ UNPINNED=$(grep -E '^\s*uses:\s+\S+' "$FILE" | grep -v '@' | head -3)
32
+ if [ -n "$UNPINNED" ]; then
33
+ WARNINGS="${WARNINGS}\n Unpinned action versions (use @v or @sha):"
34
+ echo "$UNPINNED" | while read -r line; do
35
+ WARNINGS="${WARNINGS}\n $line"
36
+ done
37
+ fi
38
+
39
+ # Check for 'runs-on' in jobs
40
+ if grep -qE '^\s+jobs:' "$FILE" && ! grep -qE 'runs-on:' "$FILE"; then
41
+ WARNINGS="${WARNINGS}\n Jobs missing 'runs-on' runner specification"
42
+ fi
43
+
44
+ if [ -n "$WARNINGS" ]; then
45
+ echo "GitHub Actions warnings in $FILE:" >&2
46
+ echo -e "$WARNINGS" >&2
47
+ fi
48
+
49
+ exit 0
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ # gitignore-auto-add.sh — Suggest .gitignore entries for common patterns
3
+ #
4
+ # Prevents: Committing build artifacts, cache dirs, env files.
5
+ # When Claude creates new directories or files that should
6
+ # be gitignored, this hook warns.
7
+ #
8
+ # TRIGGER: PostToolUse
9
+ # MATCHER: "Bash"
10
+
11
+ INPUT=$(cat)
12
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
13
+ [ -z "$COMMAND" ] && exit 0
14
+
15
+ # Only check mkdir and touch commands
16
+ echo "$COMMAND" | grep -qE '^\s*(mkdir|touch)\s' || exit 0
17
+
18
+ # Patterns that should typically be gitignored
19
+ GITIGNORE_PATTERNS="node_modules|__pycache__|\.cache|dist/|build/|\.next|\.nuxt|\.env\.|coverage|\.pytest_cache|\.mypy_cache|\.tox|\.venv|venv|\.eggs"
20
+
21
+ # Extract the target path
22
+ TARGET=$(echo "$COMMAND" | awk '{print $NF}')
23
+
24
+ if echo "$TARGET" | grep -qiE "$GITIGNORE_PATTERNS"; then
25
+ if ! git check-ignore -q "$TARGET" 2>/dev/null; then
26
+ echo "TIP: '$TARGET' should probably be in .gitignore." >&2
27
+ fi
28
+ fi
29
+
30
+ exit 0
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # go-vet-after-edit.sh — Run go vet after editing Go files
3
+ #
4
+ # Prevents: Common Go mistakes that compile but fail at runtime.
5
+ # go vet catches: printf format mismatches, unreachable code,
6
+ # struct tag errors, and more.
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
+ *.go) ;;
17
+ *) exit 0 ;;
18
+ esac
19
+
20
+ [ ! -f "$FILE" ] && exit 0
21
+
22
+ # Run go vet on the package containing the file
23
+ DIR=$(dirname "$FILE")
24
+ if command -v go >/dev/null 2>&1; then
25
+ ERRORS=$(cd "$DIR" && go vet ./... 2>&1)
26
+ if [ $? -ne 0 ]; then
27
+ echo "go vet found issues:" >&2
28
+ echo "$ERRORS" | head -5 | sed 's/^/ /' >&2
29
+ exit 2
30
+ fi
31
+ fi
32
+
33
+ exit 0
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # hook-tamper-guard.sh — Prevent Claude from modifying its own hooks
3
+ #
4
+ # Solves: Claude can rewrite its own hooks to weaken enforcement
5
+ # (#32376 — "Who watches the watchmen?")
6
+ #
7
+ # Blocks Edit/Write to:
8
+ # ~/.claude/hooks/ (hook scripts)
9
+ # ~/.claude/settings.json (hook registration)
10
+ # .claude/hooks/ (project-level hooks)
11
+ #
12
+ # Also blocks Bash commands that modify these paths:
13
+ # mv/cp/rm on hook files
14
+ # sed/awk that edit hook files
15
+ # echo/cat/tee that overwrite hook files
16
+ #
17
+ # TRIGGER: PreToolUse
18
+ # MATCHER: "Edit|Write|Bash"
19
+ #
20
+ # Usage:
21
+ # {
22
+ # "hooks": {
23
+ # "PreToolUse": [{
24
+ # "matcher": "Edit|Write|Bash",
25
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/hook-tamper-guard.sh" }]
26
+ # }]
27
+ # }
28
+ # }
29
+
30
+ INPUT=$(cat)
31
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
32
+
33
+ # --- Check Edit/Write tools ---
34
+ if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then
35
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
36
+ [ -z "$FILE" ] && exit 0
37
+
38
+ # Expand ~ to $HOME
39
+ FILE=$(echo "$FILE" | sed "s|^~|$HOME|")
40
+
41
+ # Block writes to hook directories and settings
42
+ if echo "$FILE" | grep -qE '\.claude/hooks/|\.claude/settings\.json|\.claude/settings\.local\.json'; then
43
+ echo "BLOCKED: Cannot modify hook files or settings. This protects the integrity of your safety hooks." >&2
44
+ echo "If you need to modify hooks, do it manually outside Claude Code." >&2
45
+ exit 2
46
+ fi
47
+ fi
48
+
49
+ # --- Check Bash commands ---
50
+ if [ "$TOOL" = "Bash" ]; then
51
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
52
+ [ -z "$CMD" ] && exit 0
53
+
54
+ # Block commands that modify hook files
55
+ if echo "$CMD" | grep -qE '(mv|cp|rm|sed|awk|tee|cat\s*>)\s.*\.claude/(hooks/|settings\.json|settings\.local\.json)'; then
56
+ echo "BLOCKED: Cannot modify hook files via shell commands." >&2
57
+ exit 2
58
+ fi
59
+
60
+ # Block chmod on hook files (could remove execute permission)
61
+ if echo "$CMD" | grep -qE 'chmod\s.*\.claude/hooks/'; then
62
+ echo "BLOCKED: Cannot change hook file permissions." >&2
63
+ exit 2
64
+ fi
65
+ fi
66
+
67
+ exit 0
@@ -8,6 +8,7 @@ if echo "$COMMAND" | grep -qE '\bkubectl\s+delete\s+(namespace|ns|node)\b'; then
8
8
  exit 2
9
9
  fi
10
10
  if echo "$COMMAND" | grep -qE '\bkubectl\s+delete\s+.*--all\b'; then
11
- echo "WARNING: kubectl delete --all affects all resources in scope" >&2
11
+ echo "BLOCKED: kubectl delete --all affects all resources in scope" >&2
12
+ exit 2
12
13
  fi
13
14
  exit 0