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.
- package/COOKBOOK.md +70 -0
- package/README.md +43 -4
- package/TROUBLESHOOTING.md +30 -0
- package/examples/api-rate-limit-tracker.sh +51 -0
- package/examples/auto-answer-question.sh +67 -0
- package/examples/auto-approve-readonly-tools.sh +10 -0
- package/examples/aws-production-guard.sh +40 -0
- package/examples/banned-command-guard.sh +48 -0
- package/examples/bash-heuristic-approver.sh +59 -0
- package/examples/block-database-wipe.sh +1 -1
- package/examples/classifier-fallback-allow.sh +70 -0
- package/examples/commit-message-check.sh +8 -1
- package/examples/commit-message-quality.sh +35 -0
- package/examples/credential-exfil-guard.sh +85 -0
- package/examples/cwd-reminder.sh +37 -0
- package/examples/dependency-install-guard.sh +84 -0
- package/examples/deploy-guard.sh +1 -1
- package/examples/detect-mixed-indentation.sh +33 -0
- package/examples/disk-space-check.sh +42 -0
- package/examples/docker-dangerous-guard.sh +47 -0
- package/examples/dockerfile-lint.sh +58 -0
- package/examples/edit-always-allow.sh +53 -0
- package/examples/env-file-gitignore-check.sh +39 -0
- package/examples/env-source-guard.sh +1 -1
- package/examples/file-change-tracker.sh +49 -0
- package/examples/git-stash-before-danger.sh +58 -0
- package/examples/github-actions-guard.sh +49 -0
- package/examples/gitignore-auto-add.sh +30 -0
- package/examples/go-vet-after-edit.sh +33 -0
- package/examples/hook-tamper-guard.sh +67 -0
- package/examples/kubernetes-guard.sh +2 -1
- package/examples/large-file-write-guard.sh +40 -0
- package/examples/main-branch-warn.sh +40 -0
- package/examples/max-edit-size-guard.sh +9 -15
- package/examples/mcp-server-guard.sh +70 -0
- package/examples/multiline-command-approver.sh +89 -0
- package/examples/no-base64-exfil.sh +27 -0
- package/examples/no-debug-commit.sh +60 -0
- package/examples/no-exposed-port-in-dockerfile.sh +32 -0
- package/examples/no-fixme-ship.sh +41 -0
- package/examples/no-hardcoded-ip.sh +26 -0
- package/examples/no-http-in-code.sh +19 -0
- package/examples/no-push-without-tests.sh +33 -0
- package/examples/no-self-signed-cert.sh +19 -0
- package/examples/no-star-import-python.sh +28 -0
- package/examples/no-wget-piped-bash.sh +22 -0
- package/examples/node-version-check.sh +40 -0
- package/examples/npm-publish-guard.sh +5 -2
- package/examples/output-secret-mask.sh +49 -0
- package/examples/output-token-env-check.sh +44 -0
- package/examples/package-lock-frozen.sh +25 -0
- package/examples/permission-audit-log.sh +77 -0
- package/examples/pip-venv-required.sh +40 -0
- package/examples/port-conflict-check.sh +62 -0
- package/examples/prefer-builtin-tools.sh +33 -0
- package/examples/python-import-check.sh +52 -0
- package/examples/python-ruff-on-edit.sh +51 -0
- package/examples/quoted-flag-approver.sh +51 -0
- package/examples/react-key-warn.sh +32 -0
- package/examples/rm-safety-net.sh +97 -0
- package/examples/rust-clippy-after-edit.sh +37 -0
- package/examples/session-quota-tracker.sh +44 -0
- package/examples/session-start-safety-check.sh +60 -0
- package/examples/session-summary-stop.sh +49 -0
- package/examples/session-time-limit.sh +34 -0
- package/examples/session-token-counter.sh +59 -0
- package/examples/temp-file-cleanup.sh +41 -0
- package/examples/test-before-push.sh +8 -1
- package/examples/test-coverage-reminder.sh +49 -0
- package/examples/test-exit-code-verify.sh +60 -0
- package/examples/tool-file-logger.sh +46 -0
- package/examples/typescript-lint-on-edit.sh +61 -0
- package/examples/typescript-strict-check.sh +35 -0
- package/examples/uncommitted-changes-stop.sh +16 -0
- package/examples/uncommitted-discard-guard.sh +72 -0
- package/examples/worktree-unmerged-guard.sh +85 -0
- package/examples/yaml-syntax-check.sh +50 -0
- package/index.mjs +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# rm-safety-net.sh — Extra layer of rm protection beyond destructive-guard
|
|
3
|
+
#
|
|
4
|
+
# Solves: rm commands executing without permission prompts even when not in allow list
|
|
5
|
+
# (#38607 — rm bypasses settings.json permission system)
|
|
6
|
+
#
|
|
7
|
+
# Difference from destructive-guard:
|
|
8
|
+
# destructive-guard blocks: rm -rf /, rm -rf ~/, rm -rf ../, sudo rm -rf
|
|
9
|
+
# This hook blocks: ALL rm commands on important paths, even non-recursive
|
|
10
|
+
#
|
|
11
|
+
# What it blocks:
|
|
12
|
+
# rm (any flags) on: /, ~, .., /home, /etc, /usr, /var, .git, .env
|
|
13
|
+
# find -delete (any path)
|
|
14
|
+
# shred (any file)
|
|
15
|
+
# unlink on critical paths
|
|
16
|
+
#
|
|
17
|
+
# What it allows:
|
|
18
|
+
# rm on safe targets: node_modules, dist, build, __pycache__, .cache, /tmp
|
|
19
|
+
#
|
|
20
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
21
|
+
#
|
|
22
|
+
# {
|
|
23
|
+
# "hooks": {
|
|
24
|
+
# "PreToolUse": [{
|
|
25
|
+
# "matcher": "Bash",
|
|
26
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/rm-safety-net.sh" }]
|
|
27
|
+
# }]
|
|
28
|
+
# }
|
|
29
|
+
# }
|
|
30
|
+
#
|
|
31
|
+
# Note: This hook checks rm, find -delete, and shred. Do NOT add an "if" field
|
|
32
|
+
# (v2.1.85) because "if" only supports one pattern and would miss the others.
|
|
33
|
+
|
|
34
|
+
INPUT=$(cat)
|
|
35
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
[ -z "$COMMAND" ] && exit 0
|
|
38
|
+
|
|
39
|
+
# --- rm command analysis ---
|
|
40
|
+
if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?rm\s'; then
|
|
41
|
+
# Safe targets that can be deleted freely
|
|
42
|
+
SAFE_TARGETS="node_modules|dist|build|__pycache__|\.cache|\.pytest_cache|coverage|\.nyc_output|\.next|\.nuxt|tmp|temp"
|
|
43
|
+
|
|
44
|
+
# Extract the target (last argument after flags)
|
|
45
|
+
TARGET=$(echo "$COMMAND" | grep -oP 'rm\s+[^;|&]*' | awk '{print $NF}')
|
|
46
|
+
|
|
47
|
+
# Block path traversal early
|
|
48
|
+
if echo "$TARGET" | grep -qF '..'; then
|
|
49
|
+
echo "BLOCKED: path traversal detected in rm target" >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Allow safe targets
|
|
54
|
+
if echo "$TARGET" | grep -qE "^(\./)?(${SAFE_TARGETS})(/|$)"; then
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Allow /tmp paths
|
|
59
|
+
if echo "$TARGET" | grep -qE "^/tmp/"; then
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Block rm on critical paths
|
|
64
|
+
CRITICAL="^/\$|^/home|^/etc|^/usr|^/var|^/opt|^/root|^~|^\.\.|^\.git$|^\.env"
|
|
65
|
+
if echo "$TARGET" | grep -qE "$CRITICAL"; then
|
|
66
|
+
echo "BLOCKED: rm targeting critical path: $TARGET" >&2
|
|
67
|
+
exit 2
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Block rm -rf on any non-safe path (extra safety)
|
|
71
|
+
if echo "$COMMAND" | grep -qE 'rm\s+.*-[rRf]*[rR][rRf]*'; then
|
|
72
|
+
# rm -rf on non-safe, non-tmp target — block unless it's a known safe directory
|
|
73
|
+
if ! echo "$TARGET" | grep -qE "^(\./)?(${SAFE_TARGETS})(/|$)|^/tmp/"; then
|
|
74
|
+
echo "BLOCKED: rm -rf on non-safe target: $TARGET" >&2
|
|
75
|
+
exit 2
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# --- find -delete ---
|
|
81
|
+
if echo "$COMMAND" | grep -qE 'find\s.*-delete'; then
|
|
82
|
+
# Allow find in safe directories only
|
|
83
|
+
FIND_PATH=$(echo "$COMMAND" | grep -oP 'find\s+\K[^\s]+')
|
|
84
|
+
if echo "$FIND_PATH" | grep -qE '^\.|^node_modules|^dist|^build|^/tmp'; then
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
echo "BLOCKED: find -delete outside safe directory: $FIND_PATH" >&2
|
|
88
|
+
exit 2
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# --- shred ---
|
|
92
|
+
if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?shred\s'; then
|
|
93
|
+
echo "BLOCKED: shred command (secure file deletion)" >&2
|
|
94
|
+
exit 2
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# rust-clippy-after-edit.sh — Run cargo clippy after editing Rust files
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Common Rust anti-patterns and potential bugs.
|
|
5
|
+
# Clippy catches: needless borrows, inefficient patterns,
|
|
6
|
+
# suspicious operations.
|
|
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
|
+
*.rs) ;;
|
|
17
|
+
*) exit 0 ;;
|
|
18
|
+
esac
|
|
19
|
+
|
|
20
|
+
[ ! -f "$FILE" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Find Cargo.toml
|
|
23
|
+
DIR=$(dirname "$FILE")
|
|
24
|
+
while [ "$DIR" != "/" ]; do
|
|
25
|
+
[ -f "$DIR/Cargo.toml" ] && break
|
|
26
|
+
DIR=$(dirname "$DIR")
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
if [ -f "$DIR/Cargo.toml" ] && command -v cargo >/dev/null 2>&1; then
|
|
30
|
+
WARNINGS=$(cd "$DIR" && cargo clippy --quiet 2>&1 | grep "^warning" | head -3)
|
|
31
|
+
if [ -n "$WARNINGS" ]; then
|
|
32
|
+
echo "Clippy warnings:" >&2
|
|
33
|
+
echo "$WARNINGS" | sed 's/^/ /' >&2
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
exit 0
|
|
@@ -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,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-token-counter.sh — Track tool usage count per session
|
|
3
|
+
#
|
|
4
|
+
# Solves: No visibility into how many tool calls a session makes.
|
|
5
|
+
# Useful for detecting runaway loops and estimating costs.
|
|
6
|
+
# Warns at configurable thresholds (default: 100, 200, 500).
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook that increments a counter file.
|
|
9
|
+
# At threshold crossings, outputs a warning to stderr.
|
|
10
|
+
# Does NOT block — just tracks and warns.
|
|
11
|
+
#
|
|
12
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
13
|
+
#
|
|
14
|
+
# {
|
|
15
|
+
# "hooks": {
|
|
16
|
+
# "PostToolUse": [{
|
|
17
|
+
# "matcher": "",
|
|
18
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-token-counter.sh" }]
|
|
19
|
+
# }]
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
#
|
|
23
|
+
# Environment variables:
|
|
24
|
+
# CC_TOOL_WARN_100 — threshold 1 (default: 100)
|
|
25
|
+
# CC_TOOL_WARN_200 — threshold 2 (default: 200)
|
|
26
|
+
# CC_TOOL_WARN_500 — threshold 3 (default: 500)
|
|
27
|
+
|
|
28
|
+
INPUT=$(cat)
|
|
29
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
30
|
+
|
|
31
|
+
[ -z "$TOOL" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Use a session-specific counter file
|
|
34
|
+
COUNTER_FILE="${CC_TOOL_COUNTER:-/tmp/cc-session-tool-count-$$}"
|
|
35
|
+
|
|
36
|
+
# Initialize if not exists
|
|
37
|
+
if [ ! -f "$COUNTER_FILE" ]; then
|
|
38
|
+
echo "0" > "$COUNTER_FILE"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Increment
|
|
42
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
43
|
+
COUNT=$((COUNT + 1))
|
|
44
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
45
|
+
|
|
46
|
+
# Check thresholds
|
|
47
|
+
WARN_1=${CC_TOOL_WARN_100:-100}
|
|
48
|
+
WARN_2=${CC_TOOL_WARN_200:-200}
|
|
49
|
+
WARN_3=${CC_TOOL_WARN_500:-500}
|
|
50
|
+
|
|
51
|
+
if [ "$COUNT" -eq "$WARN_1" ]; then
|
|
52
|
+
echo "INFO: Session has made $COUNT tool calls. Consider whether you're in a loop." >&2
|
|
53
|
+
elif [ "$COUNT" -eq "$WARN_2" ]; then
|
|
54
|
+
echo "WARNING: Session has made $COUNT tool calls. High usage may indicate a runaway loop." >&2
|
|
55
|
+
elif [ "$COUNT" -eq "$WARN_3" ]; then
|
|
56
|
+
echo "CRITICAL: Session has made $COUNT tool calls. Very high usage — review session behavior." >&2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
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": [{
|
|
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
|