cc-safe-setup 29.6.0 → 29.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/COOKBOOK.md +70 -0
  2. package/README.md +43 -4
  3. package/TROUBLESHOOTING.md +30 -0
  4. package/examples/api-rate-limit-tracker.sh +51 -0
  5. package/examples/auto-answer-question.sh +67 -0
  6. package/examples/auto-approve-readonly-tools.sh +10 -0
  7. package/examples/aws-production-guard.sh +40 -0
  8. package/examples/banned-command-guard.sh +48 -0
  9. package/examples/bash-heuristic-approver.sh +59 -0
  10. package/examples/bash-trace-guard.sh +48 -0
  11. package/examples/block-database-wipe.sh +1 -1
  12. package/examples/classifier-fallback-allow.sh +70 -0
  13. package/examples/commit-message-check.sh +8 -1
  14. package/examples/commit-message-quality.sh +35 -0
  15. package/examples/credential-exfil-guard.sh +12 -0
  16. package/examples/cwd-reminder.sh +37 -0
  17. package/examples/dependency-install-guard.sh +84 -0
  18. package/examples/deploy-guard.sh +1 -1
  19. package/examples/detect-mixed-indentation.sh +33 -0
  20. package/examples/disk-space-check.sh +42 -0
  21. package/examples/docker-dangerous-guard.sh +47 -0
  22. package/examples/dockerfile-lint.sh +58 -0
  23. package/examples/edit-always-allow.sh +53 -0
  24. package/examples/env-file-gitignore-check.sh +39 -0
  25. package/examples/env-source-guard.sh +1 -1
  26. package/examples/git-stash-before-danger.sh +58 -0
  27. package/examples/github-actions-guard.sh +49 -0
  28. package/examples/gitignore-auto-add.sh +30 -0
  29. package/examples/go-vet-after-edit.sh +33 -0
  30. package/examples/hook-tamper-guard.sh +67 -0
  31. package/examples/kubernetes-guard.sh +2 -1
  32. package/examples/large-file-write-guard.sh +40 -0
  33. package/examples/main-branch-warn.sh +40 -0
  34. package/examples/max-edit-size-guard.sh +9 -15
  35. package/examples/mcp-server-guard.sh +70 -0
  36. package/examples/multiline-command-approver.sh +89 -0
  37. package/examples/no-base64-exfil.sh +27 -0
  38. package/examples/no-debug-commit.sh +60 -0
  39. package/examples/no-exposed-port-in-dockerfile.sh +32 -0
  40. package/examples/no-fixme-ship.sh +41 -0
  41. package/examples/no-hardcoded-ip.sh +26 -0
  42. package/examples/no-http-in-code.sh +19 -0
  43. package/examples/no-push-without-tests.sh +33 -0
  44. package/examples/no-self-signed-cert.sh +19 -0
  45. package/examples/no-star-import-python.sh +28 -0
  46. package/examples/no-wget-piped-bash.sh +22 -0
  47. package/examples/node-version-check.sh +40 -0
  48. package/examples/npm-publish-guard.sh +5 -2
  49. package/examples/output-token-env-check.sh +44 -0
  50. package/examples/package-lock-frozen.sh +25 -0
  51. package/examples/pip-venv-required.sh +40 -0
  52. package/examples/port-conflict-check.sh +62 -0
  53. package/examples/post-compact-safety.sh +61 -0
  54. package/examples/prefer-builtin-tools.sh +33 -0
  55. package/examples/python-import-check.sh +52 -0
  56. package/examples/python-ruff-on-edit.sh +51 -0
  57. package/examples/quoted-flag-approver.sh +51 -0
  58. package/examples/react-key-warn.sh +32 -0
  59. package/examples/read-budget-guard.sh +63 -0
  60. package/examples/rm-safety-net.sh +9 -0
  61. package/examples/rust-clippy-after-edit.sh +37 -0
  62. package/examples/session-drift-guard.sh +73 -0
  63. package/examples/session-quota-tracker.sh +44 -0
  64. package/examples/session-start-safety-check.sh +60 -0
  65. package/examples/session-summary-stop.sh +49 -0
  66. package/examples/session-time-limit.sh +34 -0
  67. package/examples/strip-coauthored-by.sh +46 -0
  68. package/examples/temp-file-cleanup.sh +41 -0
  69. package/examples/test-before-push.sh +8 -1
  70. package/examples/test-coverage-reminder.sh +49 -0
  71. package/examples/test-exit-code-verify.sh +60 -0
  72. package/examples/tool-file-logger.sh +46 -0
  73. package/examples/typescript-lint-on-edit.sh +61 -0
  74. package/examples/typescript-strict-check.sh +35 -0
  75. package/examples/uncommitted-changes-stop.sh +16 -0
  76. package/examples/uncommitted-discard-guard.sh +72 -0
  77. package/examples/variable-expansion-guard.sh +51 -0
  78. package/examples/worktree-unmerged-guard.sh +13 -3
  79. package/examples/yaml-syntax-check.sh +50 -0
  80. package/index.mjs +9 -0
  81. package/package.json +2 -2
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # session-quota-tracker.sh — Track cumulative tool calls per session
3
+ #
4
+ # Solves: Token consumption spiraling without warning (#23706, #38335)
5
+ # Users hit Max plan limits unexpectedly. This hook tracks
6
+ # tool call count per session and warns at thresholds.
7
+ #
8
+ # Tracks: cumulative tool calls in a session file
9
+ # Warns at: 50, 100, 200, 500 tool calls
10
+ #
11
+ # TRIGGER: PostToolUse
12
+ # MATCHER: ""
13
+ #
14
+ # Usage:
15
+ # {
16
+ # "hooks": {
17
+ # "PostToolUse": [{
18
+ # "matcher": "",
19
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-quota-tracker.sh" }]
20
+ # }]
21
+ # }
22
+ # }
23
+
24
+ # Session tracking file
25
+ SESSION_FILE="/tmp/cc-quota-tracker-$$"
26
+
27
+ # Increment counter
28
+ if [ -f "$SESSION_FILE" ]; then
29
+ COUNT=$(cat "$SESSION_FILE")
30
+ COUNT=$((COUNT + 1))
31
+ else
32
+ COUNT=1
33
+ fi
34
+ echo "$COUNT" > "$SESSION_FILE"
35
+
36
+ # Warn at thresholds
37
+ case "$COUNT" in
38
+ 50) echo "[Session: 50 tool calls. Consider saving work.]" >&2 ;;
39
+ 100) echo "[Session: 100 tool calls. Token usage may be high.]" >&2 ;;
40
+ 200) echo "[Session: 200 tool calls. Check your usage dashboard.]" >&2 ;;
41
+ 500) echo "[Session: 500 tool calls. Consider starting a new session.]" >&2 ;;
42
+ esac
43
+
44
+ exit 0
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # session-start-safety-check.sh — Warn about uncommitted changes on session start
3
+ #
4
+ # Solves: Claude Code running destructive git commands on session startup
5
+ # that destroy uncommitted work (#34327, #39394)
6
+ #
7
+ # How it works:
8
+ # On SessionStart, checks for:
9
+ # 1. Uncommitted changes (modified/new files)
10
+ # 2. Unpushed commits
11
+ # 3. Stashed changes that may need attention
12
+ #
13
+ # Prints warnings but does NOT block (exit 0 always).
14
+ # The goal is awareness, not prevention.
15
+ #
16
+ # TRIGGER: SessionStart
17
+ # MATCHER: ""
18
+ #
19
+ # Usage:
20
+ # {
21
+ # "hooks": {
22
+ # "SessionStart": [{
23
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-start-safety-check.sh" }]
24
+ # }]
25
+ # }
26
+ # }
27
+
28
+ # Only run in git repos
29
+ git rev-parse --git-dir > /dev/null 2>&1 || exit 0
30
+
31
+ WARNINGS=0
32
+
33
+ # Check for uncommitted changes
34
+ CHANGES=$(git status --porcelain 2>/dev/null | wc -l)
35
+ if [ "$CHANGES" -gt 0 ]; then
36
+ echo "⚠ WARNING: $CHANGES uncommitted changes detected." >&2
37
+ echo " Consider: git stash (before destructive operations)" >&2
38
+ WARNINGS=$((WARNINGS + 1))
39
+ fi
40
+
41
+ # Check for unpushed commits
42
+ UNPUSHED=$(git log --oneline @{upstream}..HEAD 2>/dev/null | wc -l)
43
+ if [ "$UNPUSHED" -gt 0 ]; then
44
+ echo "⚠ WARNING: $UNPUSHED unpushed commits." >&2
45
+ echo " Consider: git push (to protect against local data loss)" >&2
46
+ WARNINGS=$((WARNINGS + 1))
47
+ fi
48
+
49
+ # Check for stashes
50
+ STASHES=$(git stash list 2>/dev/null | wc -l)
51
+ if [ "$STASHES" -gt 0 ]; then
52
+ echo "ℹ NOTE: $STASHES stashed changes exist." >&2
53
+ echo " Review: git stash list" >&2
54
+ fi
55
+
56
+ if [ "$WARNINGS" -eq 0 ]; then
57
+ echo "✓ Working tree clean, all commits pushed." >&2
58
+ fi
59
+
60
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # session-summary-stop.sh — Print session change summary on stop
3
+ #
4
+ # Solves: No quick way to see what Claude changed during a session.
5
+ # Git diff shows code changes but not the full picture.
6
+ #
7
+ # How it works: Stop hook that runs `git diff --stat` and outputs
8
+ # a summary of all modified files since the session started.
9
+ #
10
+ # Usage: Add to settings.json as a Stop hook
11
+ #
12
+ # {
13
+ # "hooks": {
14
+ # "Stop": [{
15
+ # "matcher": "",
16
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-summary-stop.sh" }]
17
+ # }]
18
+ # }
19
+ # }
20
+
21
+ INPUT=$(cat)
22
+
23
+ # Only run if in a git repo
24
+ if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
25
+ exit 0
26
+ fi
27
+
28
+ # Get change summary
29
+ CHANGES=$(git diff --stat HEAD 2>/dev/null)
30
+ STAGED=$(git diff --cached --stat 2>/dev/null)
31
+ UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l)
32
+
33
+ if [ -n "$CHANGES" ] || [ -n "$STAGED" ] || [ "$UNTRACKED" -gt 0 ]; then
34
+ echo "--- Session Change Summary ---" >&2
35
+ if [ -n "$CHANGES" ]; then
36
+ echo "Modified:" >&2
37
+ echo "$CHANGES" | head -20 >&2
38
+ fi
39
+ if [ -n "$STAGED" ]; then
40
+ echo "Staged:" >&2
41
+ echo "$STAGED" | head -10 >&2
42
+ fi
43
+ if [ "$UNTRACKED" -gt 0 ]; then
44
+ echo "Untracked files: $UNTRACKED" >&2
45
+ fi
46
+ echo "---" >&2
47
+ fi
48
+
49
+ exit 0
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+ # session-time-limit.sh — Warn when session exceeds time limit
3
+ #
4
+ # Prevents: Unbounded autonomous sessions that consume excessive tokens.
5
+ # Default: warn at 2 hours, configurable via CC_SESSION_LIMIT_HOURS.
6
+ #
7
+ # TRIGGER: PostToolUse
8
+ # MATCHER: ""
9
+
10
+ INPUT=$(cat)
11
+
12
+ # Track session start
13
+ MARKER="/tmp/cc-session-start-$$"
14
+ NOW=$(date +%s)
15
+
16
+ if [ ! -f "$MARKER" ]; then
17
+ echo "$NOW" > "$MARKER"
18
+ exit 0
19
+ fi
20
+
21
+ START=$(cat "$MARKER")
22
+ ELAPSED=$(( (NOW - START) / 60 )) # minutes
23
+ LIMIT_HOURS="${CC_SESSION_LIMIT_HOURS:-2}"
24
+ LIMIT_MIN=$((LIMIT_HOURS * 60))
25
+ WARN_MIN=$((LIMIT_MIN * 3 / 4)) # warn at 75%
26
+
27
+ if [ "$ELAPSED" -ge "$LIMIT_MIN" ]; then
28
+ echo "SESSION TIME LIMIT: ${ELAPSED}min elapsed (limit: ${LIMIT_HOURS}h)." >&2
29
+ echo " Consider saving work and starting a new session." >&2
30
+ elif [ "$ELAPSED" -ge "$WARN_MIN" ]; then
31
+ echo "[Session: ${ELAPSED}min / ${LIMIT_HOURS}h limit]" >&2
32
+ fi
33
+
34
+ exit 0
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # strip-coauthored-by.sh — Remove or warn about Co-Authored-By trailers
3
+ #
4
+ # Solves: Claude auto-appending Co-Authored-By to every commit without
5
+ # user consent. 489 commits branded without the user wanting it.
6
+ # See: https://github.com/anthropics/claude-code/issues/29999
7
+ #
8
+ # TRIGGER: PreToolUse
9
+ # MATCHER: Bash
10
+ #
11
+ # Usage:
12
+ # {
13
+ # "hooks": {
14
+ # "PreToolUse": [{
15
+ # "matcher": "Bash",
16
+ # "hooks": [{
17
+ # "type": "command",
18
+ # "if": "Bash(git commit *)",
19
+ # "command": "~/.claude/hooks/strip-coauthored-by.sh"
20
+ # }]
21
+ # }]
22
+ # }
23
+ # }
24
+ #
25
+ # Config: CC_ALLOW_COAUTHOR=1 to allow (default: 0 = block/warn)
26
+
27
+ INPUT=$(cat)
28
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
29
+ [ -z "$COMMAND" ] && exit 0
30
+
31
+ # Only check git commit commands
32
+ echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
33
+
34
+ # Check if Co-Authored-By is in the commit message
35
+ if echo "$COMMAND" | grep -qiE 'Co-Authored-By.*Claude\|Co-Authored-By.*Anthropic\|Co-Authored-By.*noreply@anthropic'; then
36
+ if [ "${CC_ALLOW_COAUTHOR:-0}" = "1" ]; then
37
+ exit 0 # User explicitly allows
38
+ fi
39
+ echo "⚠ Co-Authored-By trailer detected in commit message" >&2
40
+ echo " Set CC_ALLOW_COAUTHOR=1 to allow, or remove the trailer." >&2
41
+ echo " See: https://github.com/anthropics/claude-code/issues/29999" >&2
42
+ # Warn but don't block — user can decide
43
+ exit 0
44
+ fi
45
+
46
+ exit 0
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ # temp-file-cleanup.sh — Stop hook
3
+ # Trigger: Stop
4
+ # Matcher: (empty — runs on session end)
5
+ #
6
+ # Cleans up temporary files created by Claude Code sessions.
7
+ # Claude Code creates /tmp/claude-*-cwd files for directory tracking
8
+ # but never deletes them, accumulating 500+ files/day.
9
+ #
10
+ # See: https://github.com/anthropics/claude-code/issues/8856
11
+ #
12
+ # Usage: Add to settings.json as a Stop hook
13
+ #
14
+ # {
15
+ # "hooks": {
16
+ # "Stop": [{
17
+ # "matcher": "",
18
+ # "hooks": [{ "type": "command", "command": "bash /path/to/temp-file-cleanup.sh" }]
19
+ # }]
20
+ # }
21
+ # }
22
+
23
+ # Count before cleanup
24
+ COUNT=$(find /tmp -maxdepth 1 -name "claude-*" -type f 2>/dev/null | wc -l)
25
+
26
+ if [ "$COUNT" -eq 0 ]; then
27
+ exit 0
28
+ fi
29
+
30
+ # Clean up Claude Code temp files older than 1 hour
31
+ find /tmp -maxdepth 1 -name "claude-*-cwd" -type f -mmin +60 -delete 2>/dev/null
32
+ find /tmp -maxdepth 1 -name "claude-*" -type f -mmin +60 -delete 2>/dev/null
33
+
34
+ REMAINING=$(find /tmp -maxdepth 1 -name "claude-*" -type f 2>/dev/null | wc -l)
35
+ CLEANED=$((COUNT - REMAINING))
36
+
37
+ if [ "$CLEANED" -gt 0 ]; then
38
+ echo "Cleaned $CLEANED Claude temp files (${REMAINING} recent files kept)" >&2
39
+ fi
40
+
41
+ exit 0
@@ -12,10 +12,17 @@
12
12
  # "hooks": {
13
13
  # "PreToolUse": [{
14
14
  # "matcher": "Bash",
15
- # "hooks": [{ "type": "command", "command": "~/.claude/hooks/test-before-push.sh" }]
15
+ # "hooks": [{
16
+ # "type": "command",
17
+ # "if": "Bash(git push *)",
18
+ # "command": "~/.claude/hooks/test-before-push.sh"
19
+ # }]
16
20
  # }]
17
21
  # }
18
22
  # }
23
+ #
24
+ # The "if" field (v2.1.85+) eliminates process spawning for non-push commands.
25
+ # Without "if", the hook still works — it checks internally and exits early.
19
26
 
20
27
  INPUT=$(cat)
21
28
  COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # test-coverage-reminder.sh — Remind to run tests after code changes
3
+ #
4
+ # Prevents: Pushing untested code. Claude often edits files
5
+ # without running the test suite afterward.
6
+ #
7
+ # Tracks: number of Edit/Write calls since last test run.
8
+ # Warns at: 5 edits without tests, blocks at 10.
9
+ #
10
+ # TRIGGER: PostToolUse
11
+ # MATCHER: "Write|Edit|Bash"
12
+ #
13
+ # Usage:
14
+ # {
15
+ # "hooks": {
16
+ # "PostToolUse": [{
17
+ # "matcher": "Write|Edit|Bash",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/test-coverage-reminder.sh" }]
19
+ # }]
20
+ # }
21
+ # }
22
+
23
+ COUNTER_FILE="/tmp/cc-edit-since-test-$$"
24
+ INPUT=$(cat)
25
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
26
+
27
+ case "$TOOL" in
28
+ Write|Edit)
29
+ # Increment edit counter
30
+ COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
31
+ COUNT=$((COUNT + 1))
32
+ echo "$COUNT" > "$COUNTER_FILE"
33
+
34
+ if [ "$COUNT" -eq 5 ]; then
35
+ echo "REMINDER: 5 files changed since last test run. Consider running tests." >&2
36
+ elif [ "$COUNT" -ge 10 ]; then
37
+ echo "WARNING: $COUNT files changed without running tests. Run tests now." >&2
38
+ fi
39
+ ;;
40
+ Bash)
41
+ # Reset counter if a test command was run
42
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
43
+ if echo "$CMD" | grep -qiE '(npm\s+test|npx\s+jest|npx\s+vitest|pytest|go\s+test|cargo\s+test|make\s+test|bash\s+test)'; then
44
+ echo "0" > "$COUNTER_FILE"
45
+ fi
46
+ ;;
47
+ esac
48
+
49
+ exit 0
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # test-exit-code-verify.sh — Verify test command exit codes match results
3
+ #
4
+ # Problem: Claude reports "tests passed" even when they didn't run or failed.
5
+ # This PostToolUse hook checks the actual exit code of test commands and
6
+ # emits a warning if the exit code indicates failure.
7
+ #
8
+ # GitHub Issue: #1501 (Claude reports false test results)
9
+ #
10
+ # Usage: Add to settings.json as a PostToolUse hook on "Bash"
11
+ #
12
+ # How it works:
13
+ # 1. Detects test-like commands (npm test, pytest, jest, go test, etc.)
14
+ # 2. Checks the actual exit code from tool output
15
+ # 3. If exit code != 0, warns Claude via stderr so it cannot claim success
16
+ # 4. If no output was captured, warns about phantom test runs
17
+ #
18
+ # Why stderr: PostToolUse hook stderr is shown to Claude as feedback.
19
+ # This forces Claude to acknowledge test failures instead of fabricating results.
20
+
21
+ INPUT=$(cat)
22
+
23
+ # Extract command and exit code
24
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
25
+ EXIT_CODE=$(echo "$INPUT" | jq -r '.tool_result.exitCode // .tool_result.exit_code // empty' 2>/dev/null)
26
+ STDOUT=$(echo "$INPUT" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
27
+
28
+ [ -z "$COMMAND" ] && exit 0
29
+
30
+ # Detect test commands
31
+ is_test_command() {
32
+ local cmd="$1"
33
+ echo "$cmd" | grep -qiE '(npm\s+test|npx\s+jest|npx\s+vitest|pytest|python\s+-m\s+pytest|go\s+test|cargo\s+test|bundle\s+exec\s+rspec|mix\s+test|dotnet\s+test|mvn\s+test|gradle\s+test|make\s+test|bash\s+test\.sh)'
34
+ }
35
+
36
+ if ! is_test_command "$COMMAND"; then
37
+ exit 0
38
+ fi
39
+
40
+ # Check exit code
41
+ if [ -n "$EXIT_CODE" ] && [ "$EXIT_CODE" != "0" ]; then
42
+ echo "⚠️ TEST FAILURE DETECTED" >&2
43
+ echo "Command: $(echo "$COMMAND" | head -c 100)" >&2
44
+ echo "Exit code: $EXIT_CODE" >&2
45
+ echo "Do NOT report these tests as passing. The exit code proves failure." >&2
46
+ echo "Re-read the output above and fix the failing tests." >&2
47
+ exit 0
48
+ fi
49
+
50
+ # Check for empty output (phantom test run)
51
+ if [ -z "$STDOUT" ] || [ ${#STDOUT} -lt 10 ]; then
52
+ echo "⚠️ TEST OUTPUT SUSPICIOUSLY SHORT" >&2
53
+ echo "Command: $(echo "$COMMAND" | head -c 100)" >&2
54
+ echo "Output length: ${#STDOUT} chars" >&2
55
+ echo "Verify tests actually ran. Short output may indicate no tests executed." >&2
56
+ exit 0
57
+ fi
58
+
59
+ # Tests appear to have run and passed
60
+ exit 0
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # tool-file-logger.sh — Log file paths from Read/Write/Edit to stderr
3
+ #
4
+ # Solves: "No indication of WHICH file for READ tool" (#21151 — 180 reactions)
5
+ # Users must expand every Read/Write/Edit to see the file path.
6
+ # This hook shows the file path in the collapsed view.
7
+ #
8
+ # Output format:
9
+ # [Read: src/components/App.tsx]
10
+ # [Write: package.json]
11
+ # [Edit: src/utils/helpers.ts]
12
+ #
13
+ # TRIGGER: PostToolUse
14
+ # MATCHER: "Read|Write|Edit"
15
+ #
16
+ # Usage:
17
+ # {
18
+ # "hooks": {
19
+ # "PostToolUse": [{
20
+ # "matcher": "Read|Write|Edit",
21
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/tool-file-logger.sh" }]
22
+ # }]
23
+ # }
24
+ # }
25
+
26
+ INPUT=$(cat)
27
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
28
+ [ -z "$TOOL" ] && exit 0
29
+
30
+ # Extract file path from tool input
31
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
32
+ [ -z "$FILE" ] && exit 0
33
+
34
+ # Show just the filename for brevity, full path available in expanded view
35
+ BASENAME=$(basename "$FILE")
36
+ DIR=$(dirname "$FILE")
37
+
38
+ # For paths inside home directory, show relative path
39
+ if echo "$DIR" | grep -q "^$HOME"; then
40
+ RELDIR=$(echo "$DIR" | sed "s|^$HOME|~|")
41
+ echo "[$TOOL: $RELDIR/$BASENAME]" >&2
42
+ else
43
+ echo "[$TOOL: $FILE]" >&2
44
+ fi
45
+
46
+ exit 0
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # typescript-lint-on-edit.sh — Run TypeScript type check after editing .ts/.tsx files
3
+ #
4
+ # TRIGGER: PostToolUse
5
+ # MATCHER: Edit
6
+ #
7
+ # Best with v2.1.85 "if" field:
8
+ # {
9
+ # "hooks": {
10
+ # "PostToolUse": [{
11
+ # "matcher": "Edit",
12
+ # "hooks": [{
13
+ # "type": "command",
14
+ # "if": "Edit(*.ts)",
15
+ # "command": "~/.claude/hooks/typescript-lint-on-edit.sh"
16
+ # }]
17
+ # }]
18
+ # }
19
+ # }
20
+ #
21
+ # Without "if", the hook checks file extension internally.
22
+
23
+ INPUT=$(cat)
24
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
25
+
26
+ # Skip non-TypeScript files
27
+ case "$FILE" in
28
+ *.ts|*.tsx) ;;
29
+ *) exit 0 ;;
30
+ esac
31
+
32
+ [ ! -f "$FILE" ] && exit 0
33
+
34
+ # Find tsconfig.json in parent directories
35
+ DIR=$(dirname "$FILE")
36
+ TSCONFIG=""
37
+ while [ "$DIR" != "/" ]; do
38
+ if [ -f "$DIR/tsconfig.json" ]; then
39
+ TSCONFIG="$DIR/tsconfig.json"
40
+ break
41
+ fi
42
+ DIR=$(dirname "$DIR")
43
+ done
44
+
45
+ # No tsconfig = no type checking possible
46
+ [ -z "$TSCONFIG" ] && exit 0
47
+
48
+ # Run tsc --noEmit on the specific file
49
+ PROJECT_DIR=$(dirname "$TSCONFIG")
50
+ ISSUES=$(cd "$PROJECT_DIR" && npx tsc --noEmit --pretty false 2>&1 | grep "$(basename "$FILE")" | head -10)
51
+
52
+ if [ -n "$ISSUES" ]; then
53
+ COUNT=$(echo "$ISSUES" | wc -l)
54
+ echo "⚠ TypeScript: $COUNT error(s) in $(basename "$FILE")" >&2
55
+ echo "$ISSUES" | head -5 >&2
56
+ if [ "$COUNT" -gt 5 ]; then
57
+ echo " ... and $((COUNT - 5)) more" >&2
58
+ fi
59
+ fi
60
+
61
+ exit 0
@@ -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