cc-safe-setup 29.5.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 (79) 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 +85 -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/file-change-tracker.sh +49 -0
  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-secret-mask.sh +49 -0
  50. package/examples/output-token-env-check.sh +44 -0
  51. package/examples/package-lock-frozen.sh +25 -0
  52. package/examples/permission-audit-log.sh +77 -0
  53. package/examples/pip-venv-required.sh +40 -0
  54. package/examples/port-conflict-check.sh +62 -0
  55. package/examples/prefer-builtin-tools.sh +33 -0
  56. package/examples/python-import-check.sh +52 -0
  57. package/examples/python-ruff-on-edit.sh +51 -0
  58. package/examples/quoted-flag-approver.sh +51 -0
  59. package/examples/react-key-warn.sh +32 -0
  60. package/examples/rm-safety-net.sh +97 -0
  61. package/examples/rust-clippy-after-edit.sh +37 -0
  62. package/examples/session-quota-tracker.sh +44 -0
  63. package/examples/session-start-safety-check.sh +60 -0
  64. package/examples/session-summary-stop.sh +49 -0
  65. package/examples/session-time-limit.sh +34 -0
  66. package/examples/session-token-counter.sh +59 -0
  67. package/examples/temp-file-cleanup.sh +41 -0
  68. package/examples/test-before-push.sh +8 -1
  69. package/examples/test-coverage-reminder.sh +49 -0
  70. package/examples/test-exit-code-verify.sh +60 -0
  71. package/examples/tool-file-logger.sh +46 -0
  72. package/examples/typescript-lint-on-edit.sh +61 -0
  73. package/examples/typescript-strict-check.sh +35 -0
  74. package/examples/uncommitted-changes-stop.sh +16 -0
  75. package/examples/uncommitted-discard-guard.sh +72 -0
  76. package/examples/worktree-unmerged-guard.sh +85 -0
  77. package/examples/yaml-syntax-check.sh +50 -0
  78. package/index.mjs +3 -0
  79. package/package.json +2 -2
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+ # typescript-strict-check.sh — Warn when TypeScript strict mode is disabled
3
+ #
4
+ # Prevents: Claude silently setting "strict": false in tsconfig.json
5
+ # to bypass type errors instead of fixing them.
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
+ BASENAME=$(basename "$FILE")
15
+ [ "$BASENAME" != "tsconfig.json" ] && exit 0
16
+ [ ! -f "$FILE" ] && exit 0
17
+
18
+ # Check if strict is explicitly set to false
19
+ if python3 -c "
20
+ import json
21
+ with open('$FILE') as f:
22
+ config = json.load(f)
23
+ opts = config.get('compilerOptions', {})
24
+ if opts.get('strict') == False:
25
+ exit(1)
26
+ if opts.get('noImplicitAny') == False:
27
+ exit(1)
28
+ " 2>/dev/null; then
29
+ : # OK
30
+ else
31
+ echo "WARNING: TypeScript strict mode is disabled in $FILE." >&2
32
+ echo " Consider enabling 'strict: true' for better type safety." >&2
33
+ fi
34
+
35
+ exit 0
@@ -0,0 +1,16 @@
1
+ INPUT=$(cat)
2
+ if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
3
+ exit 0
4
+ fi
5
+ MODIFIED=$(git diff --name-only 2>/dev/null | wc -l)
6
+ STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l)
7
+ UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l)
8
+ TOTAL=$((MODIFIED + STAGED + UNTRACKED))
9
+ if [ "$TOTAL" -gt 0 ]; then
10
+ echo "⚠ WARNING: $TOTAL uncommitted changes:" >&2
11
+ [ "$MODIFIED" -gt 0 ] && echo " Modified: $MODIFIED files" >&2
12
+ [ "$STAGED" -gt 0 ] && echo " Staged: $STAGED files" >&2
13
+ [ "$UNTRACKED" -gt 0 ] && echo " Untracked: $UNTRACKED files" >&2
14
+ echo " Consider: git add -A && git commit -m 'session checkpoint'" >&2
15
+ fi
16
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # uncommitted-discard-guard.sh — Block commands that discard uncommitted changes
3
+ #
4
+ # Solves: Claude running "git checkout -- ." or "git restore ." to discard
5
+ # hours of uncommitted work. Real incident: #37888 — 30+ files of
6
+ # manual edits destroyed twice in one session.
7
+ #
8
+ # Detects:
9
+ # git checkout -- <files> (discards working tree changes)
10
+ # git checkout . (discards all changes)
11
+ # git restore <files> (same effect as checkout --)
12
+ # git restore . (discards all working tree changes)
13
+ # git stash drop (permanently deletes stashed changes)
14
+ #
15
+ # Does NOT block:
16
+ # git checkout <branch> (switching branches — safe)
17
+ # git checkout -b <branch> (creating branches — safe)
18
+ # git restore --staged (unstaging — non-destructive)
19
+ # git stash (saving changes — safe)
20
+ # git stash pop (restoring changes — safe)
21
+ #
22
+ # TRIGGER: PreToolUse MATCHER: "Bash"
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
+ # Block: git checkout -- <files> (discard working tree changes)
30
+ # The "--" separator followed by paths means "discard changes to these files"
31
+ if echo "$COMMAND" | grep -qE 'git\s+checkout\s+--\s+\S'; then
32
+ echo "BLOCKED: git checkout -- <files> discards uncommitted changes permanently." >&2
33
+ echo "" >&2
34
+ echo "Command: $COMMAND" >&2
35
+ echo "" >&2
36
+ echo "If you need to discard changes, commit or stash first:" >&2
37
+ echo " git stash # save changes for later" >&2
38
+ echo " git stash pop # restore saved changes" >&2
39
+ exit 2
40
+ fi
41
+
42
+ # Block: git checkout . (discard ALL changes)
43
+ if echo "$COMMAND" | grep -qE 'git\s+checkout\s+\.\s*$'; then
44
+ echo "BLOCKED: git checkout . discards ALL uncommitted changes." >&2
45
+ echo "" >&2
46
+ echo "This would destroy every uncommitted modification in the working tree." >&2
47
+ echo "Commit or stash your changes first." >&2
48
+ exit 2
49
+ fi
50
+
51
+ # Block: git restore <files> without --staged (discards working tree changes)
52
+ if echo "$COMMAND" | grep -qE 'git\s+restore\s+' && ! echo "$COMMAND" | grep -qE 'git\s+restore\s+--staged'; then
53
+ # Allow "git restore --staged" (just unstages, non-destructive)
54
+ # Block "git restore <files>" and "git restore ."
55
+ echo "BLOCKED: git restore discards uncommitted changes." >&2
56
+ echo "" >&2
57
+ echo "Command: $COMMAND" >&2
58
+ echo "" >&2
59
+ echo "Use 'git restore --staged <file>' to unstage without losing changes." >&2
60
+ echo "Use 'git stash' to save changes for later." >&2
61
+ exit 2
62
+ fi
63
+
64
+ # Block: git stash drop (permanently deletes stashed changes)
65
+ if echo "$COMMAND" | grep -qE 'git\s+stash\s+drop'; then
66
+ echo "BLOCKED: git stash drop permanently deletes stashed changes." >&2
67
+ echo "" >&2
68
+ echo "If you're sure, use 'git stash pop' to apply and remove in one step." >&2
69
+ exit 2
70
+ fi
71
+
72
+ exit 0
@@ -0,0 +1,85 @@
1
+ #!/bin/bash
2
+ # worktree-unmerged-guard.sh — Prevent worktree cleanup with unmerged commits
3
+ #
4
+ # Solves: Worktree sessions silently delete branches with unmerged/unpushed commits
5
+ # (#38287 — lost commits recoverable only via git fsck)
6
+ #
7
+ # How it works: Checks for unmerged commits before worktree removal.
8
+ # If the worktree branch has commits not in main/master, blocks cleanup.
9
+ #
10
+ # Usage: Add to settings.json as a PreToolUse hook
11
+ #
12
+ # {
13
+ # "hooks": {
14
+ # "PreToolUse": [{
15
+ # "matcher": "Bash",
16
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/worktree-unmerged-guard.sh" }]
17
+ # }]
18
+ # }
19
+ # }
20
+
21
+ INPUT=$(cat)
22
+ # jq with python3 fallback (macOS may not have jq)
23
+ if command -v jq &>/dev/null; then
24
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
25
+ else
26
+ COMMAND=$(echo "$INPUT" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('tool_input',{}).get('command',''))" 2>/dev/null)
27
+ fi
28
+
29
+ [ -z "$COMMAND" ] && exit 0
30
+
31
+ # Detect worktree removal commands
32
+ if ! echo "$COMMAND" | grep -qE 'git\s+worktree\s+(remove|prune)|rm\s+.*worktree'; then
33
+ exit 0
34
+ fi
35
+
36
+ # Extract worktree path
37
+ WORKTREE_PATH=$(echo "$COMMAND" | grep -oP 'git\s+worktree\s+remove\s+\K[^\s]+')
38
+
39
+ if [ -z "$WORKTREE_PATH" ]; then
40
+ # Maybe it's rm -rf on a worktree directory
41
+ exit 0
42
+ fi
43
+
44
+ # Check if the worktree exists and has a branch
45
+ if [ ! -d "$WORKTREE_PATH" ]; then
46
+ exit 0
47
+ fi
48
+
49
+ # Get the branch name for this worktree
50
+ BRANCH=$(git -C "$WORKTREE_PATH" rev-parse --abbrev-ref HEAD 2>/dev/null)
51
+
52
+ if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
53
+ exit 0
54
+ fi
55
+
56
+ # Find the default branch (portable: checks symbolic ref, then tries main/master)
57
+ DEFAULT_BRANCH=$(git -C "$WORKTREE_PATH" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
58
+ if [ -z "$DEFAULT_BRANCH" ]; then
59
+ for candidate in main master; do
60
+ git -C "$WORKTREE_PATH" rev-parse --verify "$candidate" &>/dev/null && DEFAULT_BRANCH="$candidate" && break
61
+ done
62
+ fi
63
+ [ -z "$DEFAULT_BRANCH" ] && exit 0
64
+
65
+ # Count unmerged commits
66
+ UNMERGED=$(git -C "$WORKTREE_PATH" log --oneline "$DEFAULT_BRANCH..$BRANCH" 2>/dev/null | wc -l)
67
+
68
+ if [ "$UNMERGED" -gt 0 ]; then
69
+ echo "BLOCKED: Worktree branch '$BRANCH' has $UNMERGED unmerged commit(s)" >&2
70
+ echo "Merge or push the branch before removing the worktree:" >&2
71
+ echo " git -C $WORKTREE_PATH push origin $BRANCH" >&2
72
+ echo " # or: git merge $BRANCH" >&2
73
+ exit 2
74
+ fi
75
+
76
+ # Check for unpushed commits
77
+ UNPUSHED=$(git -C "$WORKTREE_PATH" log --oneline "origin/$BRANCH..$BRANCH" 2>/dev/null | wc -l)
78
+
79
+ if [ "$UNPUSHED" -gt 0 ]; then
80
+ echo "BLOCKED: Worktree branch '$BRANCH' has $UNPUSHED unpushed commit(s)" >&2
81
+ echo "Push before removing: git -C $WORKTREE_PATH push origin $BRANCH" >&2
82
+ exit 2
83
+ fi
84
+
85
+ exit 0
@@ -0,0 +1,50 @@
1
+ #!/bin/bash
2
+ # yaml-syntax-check.sh — Validate YAML after editing
3
+ #
4
+ # Prevents: Broken YAML configs (docker-compose, CI pipelines, k8s manifests).
5
+ # YAML indentation errors are invisible until deployment fails.
6
+ #
7
+ # TRIGGER: PostToolUse
8
+ # MATCHER: "Write|Edit"
9
+ #
10
+ # Usage:
11
+ # {
12
+ # "hooks": {
13
+ # "PostToolUse": [{
14
+ # "matcher": "Write|Edit",
15
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/yaml-syntax-check.sh" }]
16
+ # }]
17
+ # }
18
+ # }
19
+
20
+ INPUT=$(cat)
21
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
22
+ [ -z "$FILE" ] && exit 0
23
+
24
+ # Only check YAML files
25
+ case "$FILE" in
26
+ *.yml|*.yaml) ;;
27
+ *) exit 0 ;;
28
+ esac
29
+
30
+ [ ! -f "$FILE" ] && exit 0
31
+
32
+ # Try python yaml parser
33
+ if command -v python3 >/dev/null 2>&1; then
34
+ ERROR=$(python3 -c "
35
+ import yaml, sys
36
+ try:
37
+ with open('$FILE') as f:
38
+ yaml.safe_load(f)
39
+ except yaml.YAMLError as e:
40
+ print(str(e)[:200])
41
+ sys.exit(1)
42
+ " 2>&1)
43
+ if [ $? -ne 0 ]; then
44
+ echo "YAML SYNTAX ERROR in $FILE:" >&2
45
+ echo " $ERROR" >&2
46
+ exit 2
47
+ fi
48
+ fi
49
+
50
+ exit 0
package/index.mjs CHANGED
@@ -456,6 +456,8 @@ function examples() {
456
456
  'package-script-guard.sh': 'Warn when package.json scripts change',
457
457
  'lockfile-guard.sh': 'Warn when lockfiles modified in commits',
458
458
  'git-lfs-guard.sh': 'Suggest Git LFS for large files',
459
+ 'python-ruff-on-edit.sh': 'PostToolUse: lint Python files after edit (ruff/flake8/pylint)',
460
+ 'typescript-lint-on-edit.sh': 'PostToolUse: type check TypeScript files after edit (tsc --noEmit)',
459
461
  },
460
462
  'Recovery': {
461
463
  'auto-checkpoint.sh': 'Auto-commit after edits for rollback protection',
@@ -465,6 +467,7 @@ function examples() {
465
467
  'UX': {
466
468
  'prompt-length-guard.sh': 'UserPromptSubmit: warn on long prompts (>5000 chars)',
467
469
  'prompt-injection-detector.sh': 'UserPromptSubmit: detect prompt injection patterns',
470
+ 'auto-answer-question.sh': 'PreToolUse: auto-answer AskUserQuestion for headless mode (v2.1.85)',
468
471
  'notify-waiting.sh': 'Desktop notification when Claude waits for input',
469
472
  'tmp-cleanup.sh': 'Clean up /tmp/claude-*-cwd files on session end',
470
473
  'hook-debug-wrapper.sh': 'Wrap any hook to log input/output/exit/timing',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "29.5.0",
4
- "description": "One command to make Claude Code safe. 335 example hooks + 8 built-in. 52 CLI commands. 1,760 tests. Works with Auto Mode.",
3
+ "version": "29.6.1",
4
+ "description": "One command to make Claude Code safe. 406 example hooks + 8 built-in. 52 CLI commands. 5564 tests. Works with Auto Mode.",
5
5
  "main": "index.mjs",
6
6
  "bin": {
7
7
  "cc-safe-setup": "index.mjs"