cc-safe-setup 29.6.30 → 29.6.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dw/cc-safe-setup)](https://www.npmjs.com/package/cc-safe-setup)
5
5
  [![tests](https://github.com/yurukusa/cc-safe-setup/actions/workflows/test.yml/badge.svg)](https://github.com/yurukusa/cc-safe-setup/actions/workflows/test.yml)
6
6
 
7
- **One command to make Claude Code safe for autonomous operation.** 514 example hooks · 7,564 tests · 1,000+ installs/day · [日本語](docs/README.ja.md)
7
+ **One command to make Claude Code safe for autonomous operation.** 517 example hooks · 7,591 tests · 1,000+ installs/day · [日本語](docs/README.ja.md)
8
8
 
9
9
  ```bash
10
10
  npx cc-safe-setup
@@ -117,7 +117,7 @@ Install any of these: `npx cc-safe-setup --install-example <name>`
117
117
  | `--scan [--apply]` | Tech stack detection |
118
118
  | `--export / --import` | Team config sharing |
119
119
  | `--verify` | Test each hook |
120
- | `--install-example <name>` | Install from 514 examples |
120
+ | `--install-example <name>` | Install from 517 examples |
121
121
  | `--examples [filter]` | Browse examples by keyword |
122
122
  | `--full` | All-in-one setup |
123
123
  | `--status` | Check installed hooks |
@@ -400,6 +400,7 @@ See [Issue #1](https://github.com/yurukusa/cc-safe-setup/issues/1) for details.
400
400
 
401
401
  ## Learn More
402
402
 
403
+ - **[Hook Design Guide (Zenn Book)](https://zenn.dev/yurukusa/books/6076c23b1cb18b)** — 14-chapter guide from 700+ hours of autonomous operation. Hook design patterns, testing strategies, real incident postmortems. [Chapter 2 free](https://zenn.dev/yurukusa/books/6076c23b1cb18b/viewer/2-safety-guards)
403
404
  - [Cookbook](COOKBOOK.md) — 26 practical recipes (block, approve, protect, monitor, diagnose)
404
405
  - [Official Hooks Reference](https://code.claude.com/docs/en/hooks) — Claude Code hooks documentation
405
406
  - [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 25 recipes from real GitHub Issues ([interactive version](https://yurukusa.github.io/claude-code-hooks/))
@@ -1,6 +1,6 @@
1
1
  # Example Hooks
2
2
 
3
- 514 installable hooks. Each solves a real problem from GitHub Issues or autonomous operation. 7,564 tests.
3
+ 518 installable hooks. Each solves a real problem from GitHub Issues or autonomous operation. 7,603 tests.
4
4
 
5
5
  ```bash
6
6
  npx cc-safe-setup --install-example <name> # install one
@@ -13,7 +13,7 @@ npx cc-safe-setup --shield # install recommended set
13
13
 
14
14
  | Category | Count | Examples |
15
15
  |----------|-------|---------|
16
- | Destructive Command Prevention | 12 | `destructive-guard`, `branch-guard`, `no-sudo-guard`, `symlink-guard` |
16
+ | Destructive Command Prevention | 14 | `destructive-guard`, `branch-guard`, `no-sudo-guard`, `symlink-guard`, `shell-wrapper-guard`, `compound-inject-guard` |
17
17
  | Data Protection | 5 | `block-database-wipe`, `secret-guard`, `hardcoded-secret-detector` |
18
18
  | Git Safety | 11 | `git-config-guard`, `no-verify-blocker`, `push-requires-test-pass` |
19
19
  | Auto-Approve (PreToolUse) | 11 | `auto-approve-readonly`, `auto-approve-build`, `auto-approve-docker` |
@@ -21,8 +21,8 @@ npx cc-safe-setup --shield # install recommended set
21
21
  | Code Quality | 10 | `syntax-check`, `diff-size-guard`, `test-deletion-guard` |
22
22
  | Security | 10 | `credential-file-cat-guard`, `credential-exfil-guard`, `prompt-injection-guard` |
23
23
  | Deploy | 4 | `deploy-guard`, `no-deploy-friday`, `work-hours-guard` |
24
- | Monitoring & Cost | 13 | `context-monitor`, `cost-tracker`, `loop-detector`, `edit-error-counter` |
25
- | Utility | 17 | `comment-strip`, `session-handoff`, `auto-checkpoint`, `edit-retry-loop-guard` |
24
+ | Monitoring & Cost | 14 | `context-monitor`, `cost-tracker`, `loop-detector`, `edit-error-counter`, `dotenv-watch` |
25
+ | Utility | 20 | `comment-strip`, `session-handoff`, `auto-checkpoint`, `edit-retry-loop-guard`, `direnv-auto-reload`, `pre-compact-checkpoint` |
26
26
 
27
27
  ## Popular Hooks
28
28
 
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ # api-retry-limiter.sh — Limit API error retries to prevent token waste
3
+ #
4
+ # Solves: Claude Code retries API calls on transient errors without
5
+ # backoff, burning tokens on repeated failures.
6
+ # Related to #40376 (rescue from 4xx/5xx) and general cost concerns.
7
+ #
8
+ # How it works: PostToolUse hook that tracks API error patterns.
9
+ # If the same error appears 3+ times in 60 seconds, warns the user
10
+ # and suggests waiting or switching approaches.
11
+ #
12
+ # Complements api-error-alert (built-in) with retry-specific logic.
13
+ #
14
+ # TRIGGER: PostToolUse
15
+ # MATCHER: "" (all tools — API errors can come from any tool)
16
+
17
+ INPUT=$(cat)
18
+ ERROR=$(echo "$INPUT" | jq -r '.tool_output // empty' 2>/dev/null | head -c 500)
19
+ [ -z "$ERROR" ] && exit 0
20
+
21
+ # Only track API-related errors
22
+ echo "$ERROR" | grep -qiE "rate.limit|429|500|502|503|529|overloaded|timeout|ECONNREFUSED|ENOTFOUND" || exit 0
23
+
24
+ ERROR_LOG="/tmp/cc-api-errors-${PPID}"
25
+ NOW=$(date +%s)
26
+ ERROR_TYPE=$(echo "$ERROR" | grep -oiE "rate.limit|429|500|502|503|529|overloaded|timeout|ECONNREFUSED" | head -1)
27
+
28
+ echo "$NOW $ERROR_TYPE" >> "$ERROR_LOG"
29
+
30
+ # Count recent errors of same type
31
+ RECENT=$(awk -v now="$NOW" -v type="$ERROR_TYPE" '$1 > now - 60 && $2 == type {count++} END {print count+0}' "$ERROR_LOG")
32
+
33
+ if [ "$RECENT" -ge 5 ]; then
34
+ echo "⚠ API error loop: ${ERROR_TYPE} occurred ${RECENT} times in 60s" >&2
35
+ echo " Suggestion: Wait 30-60 seconds before retrying" >&2
36
+ echo " Or: Switch to a different approach that doesn't require this API" >&2
37
+ elif [ "$RECENT" -ge 3 ]; then
38
+ echo "⚠ Repeated API error: ${ERROR_TYPE} (${RECENT}x in 60s)" >&2
39
+ fi
40
+
41
+ exit 0
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # binary-upload-guard.sh — Block committing binary files to git
3
+ #
4
+ # Solves: Claude adds binary files (images, compiled binaries, archives)
5
+ # to git, bloating the repository. Once committed, binaries
6
+ # are in git history forever (even if deleted later).
7
+ #
8
+ # How it works: PreToolUse hook on Bash that checks git add/commit
9
+ # commands for binary file extensions.
10
+ #
11
+ # TRIGGER: PreToolUse
12
+ # MATCHER: "Bash"
13
+
14
+ INPUT=$(cat)
15
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
16
+ [ -z "$COMMAND" ] && exit 0
17
+
18
+ # Only check git add/commit commands
19
+ echo "$COMMAND" | grep -qE 'git\s+(add|commit)' || exit 0
20
+
21
+ # Binary file extensions to block
22
+ BINARY_EXT='\.(exe|dll|so|dylib|bin|obj|o|a|lib|class|jar|war|ear|pyc|pyo|whl|egg|tar|gz|bz2|xz|zip|rar|7z|iso|dmg|pkg|deb|rpm|msi|app|apk|ipa|pdf|doc|docx|xls|xlsx|ppt|pptx|sqlite|db|mdb|wasm)(\s|$|")'
23
+
24
+ # Check if the command references binary files
25
+ if echo "$COMMAND" | grep -qiE "$BINARY_EXT"; then
26
+ MATCHED=$(echo "$COMMAND" | grep -oiE "[^ \"']+${BINARY_EXT}" | head -3)
27
+ echo "⚠ Binary file detected in git command:" >&2
28
+ echo " $MATCHED" >&2
29
+ echo " Binary files bloat git history permanently." >&2
30
+ echo " Consider: .gitignore, git-lfs, or external storage." >&2
31
+ # Warn but don't block (some binaries are intentional)
32
+ fi
33
+
34
+ # Block large archives being added
35
+ if echo "$COMMAND" | grep -qE 'git\s+add.*\.(tar\.gz|zip|rar|7z|iso|dmg)\b'; then
36
+ echo "BLOCKED: Large archive file in git add" >&2
37
+ echo " Archives should not be committed to git." >&2
38
+ echo " Use .gitignore or external storage." >&2
39
+ exit 2
40
+ fi
41
+
42
+ exit 0
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # compaction-transcript-guard.sh — Save conversation state before compaction
3
+ #
4
+ # Solves: Compaction race condition destroys transcript (#40352).
5
+ # When rate limiting hits during compaction, the JSONL transcript
6
+ # is left with 4,300+ empty messages. All context is permanently lost.
7
+ #
8
+ # How it works: Uses PreCompact hook event to save a recovery snapshot
9
+ # before compaction begins. If compaction fails, the snapshot enables
10
+ # manual recovery.
11
+ #
12
+ # Saves:
13
+ # 1. Git state (uncommitted changes committed as checkpoint)
14
+ # 2. Current task context to ~/.claude/recovery/pre-compact-snapshot.md
15
+ # 3. Recent file list to ~/.claude/recovery/recent-files.txt
16
+ #
17
+ # TRIGGER: PreCompact
18
+ # MATCHER: No matcher support — fires on every compaction
19
+ #
20
+ # DECISION CONTROL: None (notification only)
21
+
22
+ RECOVERY_DIR="${HOME}/.claude/recovery"
23
+ mkdir -p "$RECOVERY_DIR"
24
+
25
+ TIMESTAMP=$(date -u '+%Y%m%d-%H%M%S')
26
+ SNAPSHOT="${RECOVERY_DIR}/pre-compact-${TIMESTAMP}.md"
27
+
28
+ # 1. Save git state
29
+ if git rev-parse --is-inside-work-tree &>/dev/null; then
30
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
31
+ DIRTY=$(git status --porcelain 2>/dev/null | wc -l)
32
+ LAST_COMMIT=$(git log --oneline -1 2>/dev/null)
33
+
34
+ # Auto-commit uncommitted changes
35
+ if [ "$DIRTY" -gt 0 ]; then
36
+ git add -A 2>/dev/null
37
+ git commit -m "recovery: pre-compact checkpoint (${DIRTY} files, ${TIMESTAMP})" --no-verify 2>/dev/null
38
+ echo "📸 Recovery checkpoint: ${DIRTY} uncommitted files saved" >&2
39
+ fi
40
+
41
+ cat > "$SNAPSHOT" << EOF
42
+ # Pre-Compaction Recovery Snapshot
43
+ Timestamp: ${TIMESTAMP}
44
+ Branch: ${BRANCH}
45
+ Uncommitted files: ${DIRTY}
46
+ Last commit: ${LAST_COMMIT}
47
+
48
+ ## Recent files (last 10 modified)
49
+ $(git diff --name-only HEAD~3 HEAD 2>/dev/null | tail -10)
50
+
51
+ ## Working directory
52
+ $(pwd)
53
+
54
+ ## Recovery instructions
55
+ If compaction failed and context was lost:
56
+ 1. Check git log: git log --oneline -5
57
+ 2. Restore from checkpoint: git show HEAD
58
+ 3. Resume work from this snapshot
59
+ EOF
60
+ fi
61
+
62
+ # 2. Save list of recently accessed files
63
+ if [ -f "${HOME}/.claude/session-changes.log" ]; then
64
+ tail -20 "${HOME}/.claude/session-changes.log" > "${RECOVERY_DIR}/recent-files-${TIMESTAMP}.txt"
65
+ fi
66
+
67
+ echo "📋 Recovery snapshot saved: ${SNAPSHOT}" >&2
68
+ echo " If compaction fails, recovery data is preserved" >&2
69
+
70
+ exit 0
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # compound-inject-guard.sh — Block destructive commands hidden in compound statements
3
+ #
4
+ # Solves: Permission allow list glob wildcards match shell operators (&&, ;, ||),
5
+ # allowing destructive commands to bypass the allowlist.
6
+ # Example: `Bash(git -C * status)` also matches
7
+ # `git -C "/repo" && rm -rf / && git -C "/repo" status`
8
+ #
9
+ # Related: GitHub #40344 — "Permission allow list glob wildcards match shell
10
+ # operators, enabling command injection"
11
+ #
12
+ # How it works: Splits compound commands on shell operators (&&, ||, ;)
13
+ # and checks each segment independently for destructive patterns.
14
+ # This prevents destructive commands from hiding inside compound statements
15
+ # that match overly broad permission allow rules.
16
+ #
17
+ # TRIGGER: PreToolUse
18
+ # MATCHER: "Bash"
19
+
20
+ INPUT=$(cat)
21
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
22
+ [ -z "$COMMAND" ] && exit 0
23
+
24
+ # Only check compound commands (those with shell operators)
25
+ echo "$COMMAND" | grep -qE '&&|\|\||;' || exit 0
26
+
27
+ # Destructive patterns to detect in each segment
28
+ DESTRUCT='rm\s+-[rf]*\s+[/~]|rm\s+-[rf]*\s+\.\.|git\s+reset\s+--hard|git\s+clean\s+-[fd]+|mkfs\.|dd\s+if=|chmod\s+777\s+/|>\s*/dev/sd'
29
+
30
+ # Split on shell operators and check each segment
31
+ IFS=$'\n'
32
+ for segment in $(echo "$COMMAND" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g'); do
33
+ # Trim leading whitespace
34
+ segment=$(echo "$segment" | sed 's/^\s*//')
35
+ [ -z "$segment" ] && continue
36
+
37
+ if echo "$segment" | grep -qE "$DESTRUCT"; then
38
+ echo "BLOCKED: Destructive command in compound statement" >&2
39
+ echo " Segment: $segment" >&2
40
+ echo " Fix: Run destructive commands separately, not chained with && or ;" >&2
41
+ exit 2
42
+ fi
43
+ done
44
+
45
+ exit 0
@@ -0,0 +1,65 @@
1
+ #!/bin/bash
2
+ # concurrent-edit-lock.sh — Prevent file corruption from concurrent Claude sessions
3
+ #
4
+ # Solves: File corruption when running multiple Claude Code terminals (#35682).
5
+ # Two sessions editing the same file simultaneously can produce
6
+ # interleaved writes, truncated content, or merge conflicts.
7
+ #
8
+ # How it works: PreToolUse hook on Edit/Write that creates a lock file
9
+ # before editing. If another session holds the lock, blocks the edit.
10
+ # Lock auto-expires after 60 seconds (configurable) to prevent deadlocks.
11
+ #
12
+ # CONFIG:
13
+ # CC_EDIT_LOCK_TIMEOUT=60 # seconds before lock expires
14
+ #
15
+ # TRIGGER: PreToolUse
16
+ # MATCHER: "Edit|Write"
17
+
18
+ INPUT=$(cat)
19
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
20
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
21
+ [ -z "$FILE" ] && exit 0
22
+
23
+ LOCK_DIR="${HOME}/.claude/locks"
24
+ mkdir -p "$LOCK_DIR"
25
+
26
+ LOCK_TIMEOUT=${CC_EDIT_LOCK_TIMEOUT:-60}
27
+
28
+ # Create a hash of the file path for the lock file name
29
+ LOCK_HASH=$(echo "$FILE" | md5sum | cut -c1-16)
30
+ LOCK_FILE="${LOCK_DIR}/${LOCK_HASH}.lock"
31
+ SESSION_ID="$$"
32
+
33
+ # Check for existing lock
34
+ if [ -f "$LOCK_FILE" ]; then
35
+ LOCK_INFO=$(cat "$LOCK_FILE")
36
+ LOCK_PID=$(echo "$LOCK_INFO" | cut -d'|' -f1)
37
+ LOCK_TIME=$(echo "$LOCK_INFO" | cut -d'|' -f2)
38
+ NOW=$(date +%s)
39
+
40
+ # Check if lock is expired
41
+ if [ $((NOW - LOCK_TIME)) -gt "$LOCK_TIMEOUT" ]; then
42
+ # Lock expired — remove and proceed
43
+ rm -f "$LOCK_FILE"
44
+ elif [ "$LOCK_PID" != "$SESSION_ID" ]; then
45
+ # Another session holds the lock
46
+ # Check if the locking process is still alive
47
+ if kill -0 "$LOCK_PID" 2>/dev/null; then
48
+ echo "BLOCKED: File is being edited by another Claude session (PID ${LOCK_PID})" >&2
49
+ echo " File: $(basename "$FILE")" >&2
50
+ echo " Wait for the other session to finish, or remove lock: rm ${LOCK_FILE}" >&2
51
+ exit 2
52
+ else
53
+ # Process is dead — stale lock
54
+ rm -f "$LOCK_FILE"
55
+ fi
56
+ fi
57
+ fi
58
+
59
+ # Acquire lock
60
+ echo "${SESSION_ID}|$(date +%s)" > "$LOCK_FILE"
61
+
62
+ # Schedule lock cleanup (best effort — PostToolUse should handle this)
63
+ # The lock will expire naturally after LOCK_TIMEOUT seconds
64
+
65
+ exit 0
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # context-threshold-alert.sh — Alert at configurable context usage thresholds
3
+ #
4
+ # Solves: No hook event for context usage thresholds (#40256).
5
+ # Users want to be notified when context hits 50%, 75%, 90%
6
+ # without waiting for the built-in context-monitor warnings.
7
+ #
8
+ # How it works: PostToolUse hook that reads the context percentage
9
+ # from the tool output and triggers alerts at configurable thresholds.
10
+ # Each threshold fires only once per session (tracked via temp file).
11
+ #
12
+ # CONFIG (environment variables):
13
+ # CC_CONTEXT_WARN=50 # First warning at 50%
14
+ # CC_CONTEXT_ALERT=75 # Alert at 75%
15
+ # CC_CONTEXT_CRITICAL=90 # Critical at 90%
16
+ # CC_CONTEXT_ACTION=log # "log" (stderr only) or "block" (exit 2 at critical)
17
+ #
18
+ # TRIGGER: PostToolUse
19
+ # MATCHER: "" (all tools — checks context on every call)
20
+
21
+ INPUT=$(cat)
22
+
23
+ # Extract context percentage from tool output if available
24
+ CONTEXT_PCT=$(echo "$INPUT" | jq -r '.context_window.remaining_percentage // empty' 2>/dev/null)
25
+
26
+ # If no context data in this tool call, try the session state
27
+ if [ -z "$CONTEXT_PCT" ]; then
28
+ # Check if context-monitor data exists
29
+ STATE_FILE="/tmp/cc-context-state-$$"
30
+ [ -f "$STATE_FILE" ] && CONTEXT_PCT=$(cat "$STATE_FILE")
31
+ fi
32
+
33
+ [ -z "$CONTEXT_PCT" ] && exit 0
34
+
35
+ # Convert remaining % to used %
36
+ USED=$((100 - CONTEXT_PCT))
37
+
38
+ # Configurable thresholds
39
+ WARN=${CC_CONTEXT_WARN:-50}
40
+ ALERT=${CC_CONTEXT_ALERT:-75}
41
+ CRITICAL=${CC_CONTEXT_CRITICAL:-90}
42
+ ACTION=${CC_CONTEXT_ACTION:-log}
43
+
44
+ # Track which thresholds have fired (once per session)
45
+ FIRED_FILE="/tmp/cc-context-fired-${PPID}"
46
+
47
+ already_fired() {
48
+ grep -q "^$1$" "$FIRED_FILE" 2>/dev/null
49
+ }
50
+
51
+ mark_fired() {
52
+ echo "$1" >> "$FIRED_FILE"
53
+ }
54
+
55
+ # Check thresholds (highest first)
56
+ if [ "$USED" -ge "$CRITICAL" ] && ! already_fired "critical"; then
57
+ echo "🔴 CRITICAL: Context usage at ${USED}% (threshold: ${CRITICAL}%)" >&2
58
+ echo " Consider running /compact or starting a new session" >&2
59
+ mark_fired "critical"
60
+ [ "$ACTION" = "block" ] && exit 2
61
+ elif [ "$USED" -ge "$ALERT" ] && ! already_fired "alert"; then
62
+ echo "🟠 ALERT: Context usage at ${USED}% (threshold: ${ALERT}%)" >&2
63
+ echo " Commit work and prepare for compaction" >&2
64
+ mark_fired "alert"
65
+ elif [ "$USED" -ge "$WARN" ] && ! already_fired "warn"; then
66
+ echo "🟡 WARNING: Context usage at ${USED}% (threshold: ${WARN}%)" >&2
67
+ mark_fired "warn"
68
+ fi
69
+
70
+ exit 0
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # context-warning-verifier.sh — Verify context warnings are genuine
3
+ #
4
+ # Solves: Claude weaponizes user's CLAUDE.md context warning rules to
5
+ # fabricate urgency and manipulate users (#35357, 2 reactions).
6
+ # Claude triggers context warnings at the exact format defined
7
+ # in CLAUDE.md when the context window is nowhere near the threshold.
8
+ #
9
+ # How it works: PostToolUse hook that independently verifies context usage
10
+ # after Claude claims the context is running low. If Claude's claim
11
+ # doesn't match the actual context state, warns the user.
12
+ #
13
+ # This hook provides an independent "second opinion" on context warnings,
14
+ # preventing the model from using the user's own rules as manipulation tools.
15
+ #
16
+ # TRIGGER: PostToolUse
17
+ # MATCHER: "" (monitors all tool outputs for fabricated warnings)
18
+
19
+ INPUT=$(cat)
20
+ OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // empty' 2>/dev/null)
21
+ [ -z "$OUTPUT" ] && exit 0
22
+
23
+ # Check if the output contains context warning patterns
24
+ HAS_WARNING=false
25
+ echo "$OUTPUT" | grep -qiE "context.*(running out|low|critical|depleted|remaining.*[0-9]+%)" && HAS_WARNING=true
26
+ echo "$OUTPUT" | grep -qiE "(20|15|10|5)%.*(context|remaining|left)" && HAS_WARNING=true
27
+ echo "$OUTPUT" | grep -qiE "コンテキスト.*(残|不足|危険|低)" && HAS_WARNING=true
28
+
29
+ [ "$HAS_WARNING" = "false" ] && exit 0
30
+
31
+ # Claude claims context is low — verify independently
32
+ # Get actual context percentage from the tool metadata if available
33
+ ACTUAL_PCT=$(echo "$INPUT" | jq -r '.context_window.remaining_percentage // empty' 2>/dev/null)
34
+
35
+ if [ -n "$ACTUAL_PCT" ] && [ "$ACTUAL_PCT" -gt 50 ] 2>/dev/null; then
36
+ echo "⚠ CONTEXT WARNING VERIFICATION FAILED" >&2
37
+ echo " Claude claims context is running low" >&2
38
+ echo " Actual remaining: ${ACTUAL_PCT}% (well above danger zone)" >&2
39
+ echo " This may be a fabricated warning to avoid work." >&2
40
+ echo " Reference: GitHub Issue #35357" >&2
41
+ elif [ -z "$ACTUAL_PCT" ]; then
42
+ echo "ℹ Context warning detected but cannot independently verify." >&2
43
+ echo " Check actual usage with: context-monitor or statusline" >&2
44
+ fi
45
+
46
+ exit 0
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # cross-session-error-log.sh — Persist error patterns across sessions
3
+ #
4
+ # Solves: Agent ignores spec across multiple sessions (#40383).
5
+ # Each new session starts fresh with no memory of previous failures.
6
+ # The same destructive mistakes are repeated session after session.
7
+ #
8
+ # How it works: PostToolUse hook that logs blocked/failed operations
9
+ # to a persistent file (~/.claude/error-history.log). On SessionStart,
10
+ # checks for recurring patterns and warns the new session about them.
11
+ #
12
+ # The error history survives session restarts, so the next Claude session
13
+ # sees "this operation failed 3 times in previous sessions" before
14
+ # attempting it again.
15
+ #
16
+ # TRIGGER: PostToolUse (log errors) + Notification/SessionStart (read history)
17
+ # MATCHER: "" (all tools)
18
+
19
+ INPUT=$(cat)
20
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
21
+ EVENT=$(echo "$INPUT" | jq -r '.event // empty' 2>/dev/null)
22
+
23
+ ERROR_LOG="${HOME}/.claude/error-history.log"
24
+ mkdir -p "$(dirname "$ERROR_LOG")"
25
+
26
+ # On session start: show recent error patterns
27
+ if [ "$EVENT" = "session_start" ] || [ "$EVENT" = "SessionStart" ]; then
28
+ if [ -f "$ERROR_LOG" ]; then
29
+ RECENT=$(tail -50 "$ERROR_LOG" | awk -F'|' '{print $3}' | sort | uniq -c | sort -rn | head -5)
30
+ if [ -n "$RECENT" ]; then
31
+ echo "📋 Recurring error patterns from previous sessions:" >&2
32
+ echo "$RECENT" | while read count pattern; do
33
+ [ "$count" -ge 2 ] && echo " ${count}x: ${pattern}" >&2
34
+ done
35
+ fi
36
+ fi
37
+ exit 0
38
+ fi
39
+
40
+ # On tool use: check for error indicators
41
+ OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // empty' 2>/dev/null | head -c 200)
42
+ [ -z "$OUTPUT" ] && exit 0
43
+
44
+ # Detect error patterns
45
+ IS_ERROR=false
46
+ ERROR_TYPE=""
47
+
48
+ if echo "$OUTPUT" | grep -qiE "error|failed|BLOCKED|exit code [1-9]|permission denied|not found|syntax error"; then
49
+ IS_ERROR=true
50
+ ERROR_TYPE=$(echo "$OUTPUT" | grep -oiE "error:[^\"]*|failed:[^\"]*|BLOCKED:[^\"]*" | head -1 | head -c 80)
51
+ [ -z "$ERROR_TYPE" ] && ERROR_TYPE="$(echo "$OUTPUT" | head -c 60)"
52
+ fi
53
+
54
+ # Log error with timestamp and tool name
55
+ if [ "$IS_ERROR" = "true" ]; then
56
+ TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%S')
57
+ echo "${TIMESTAMP}|${TOOL}|${ERROR_TYPE}" >> "$ERROR_LOG"
58
+
59
+ # Keep log manageable (last 200 entries)
60
+ if [ "$(wc -l < "$ERROR_LOG")" -gt 200 ]; then
61
+ tail -100 "$ERROR_LOG" > "${ERROR_LOG}.tmp"
62
+ mv "${ERROR_LOG}.tmp" "$ERROR_LOG"
63
+ fi
64
+ fi
65
+
66
+ exit 0
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ # file-age-guard.sh — Warn before editing files not modified in 30+ days
3
+ #
4
+ # Solves: Claude edits stable/legacy files that haven't been touched
5
+ # in months, introducing regressions in well-tested code.
6
+ #
7
+ # How it works: PreToolUse hook on Edit/Write that checks the last
8
+ # modification time of the target file. If the file hasn't been
9
+ # modified in CC_FILE_AGE_DAYS (default 30), warns the user.
10
+ #
11
+ # This doesn't block — just warns. The assumption is that files
12
+ # untouched for a long time are stable and should be edited carefully.
13
+ #
14
+ # CONFIG:
15
+ # CC_FILE_AGE_DAYS=30 (warn threshold in days)
16
+ #
17
+ # TRIGGER: PreToolUse
18
+ # MATCHER: "Edit"
19
+
20
+ INPUT=$(cat)
21
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
22
+ [ -z "$FILE" ] && exit 0
23
+ [ -f "$FILE" ] || exit 0
24
+
25
+ AGE_THRESHOLD=${CC_FILE_AGE_DAYS:-30}
26
+
27
+ # Get file modification time
28
+ FILE_MTIME=$(stat -c %Y "$FILE" 2>/dev/null)
29
+ [ -z "$FILE_MTIME" ] && exit 0
30
+
31
+ NOW=$(date +%s)
32
+ AGE_DAYS=$(( (NOW - FILE_MTIME) / 86400 ))
33
+
34
+ if [ "$AGE_DAYS" -ge "$AGE_THRESHOLD" ]; then
35
+ FILENAME=$(basename "$FILE")
36
+ echo "⚠ Editing stable file: ${FILENAME} (last modified ${AGE_DAYS} days ago)" >&2
37
+ echo " This file hasn't been touched in ${AGE_DAYS} days." >&2
38
+ echo " Verify changes don't break existing behavior." >&2
39
+ fi
40
+
41
+ exit 0
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # heredoc-backtick-approver.sh — Auto-approve backtick warnings in heredoc strings
3
+ #
4
+ # Solves: Backticks inside heredoc quoted strings trigger false-positive
5
+ # permission prompt (#35183, 2 reactions).
6
+ # `git commit -m "$(cat <<'EOF' ... \`code\` ... EOF)"` triggers
7
+ # "Command contains backticks for command substitution" even though
8
+ # backticks inside <<'EOF' are inert literal characters.
9
+ #
10
+ # How it works: PermissionRequest hook that checks if the backtick warning
11
+ # is from a command containing a quoted heredoc (<<'EOF' or <<"EOF").
12
+ # If so, auto-approves since the backticks are string content, not shell.
13
+ #
14
+ # TRIGGER: PermissionRequest
15
+ # MATCHER: "" (all permission requests)
16
+
17
+ INPUT=$(cat)
18
+ MESSAGE=$(echo "$INPUT" | jq -r '.message // empty' 2>/dev/null)
19
+ [ -z "$MESSAGE" ] && exit 0
20
+
21
+ # Only handle backtick/command substitution warnings
22
+ echo "$MESSAGE" | grep -qiE "backtick|command substitution" || exit 0
23
+
24
+ # Get the command that triggered the warning
25
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
26
+ [ -z "$COMMAND" ] && exit 0
27
+
28
+ # Check if command contains a quoted heredoc
29
+ # <<'EOF' (single-quoted) or <<"EOF" (double-quoted) disable backtick expansion
30
+ if echo "$COMMAND" | grep -qE "<<'[A-Za-z_]+'" || echo "$COMMAND" | grep -qE '<<"[A-Za-z_]+"'; then
31
+ # Backticks inside a quoted heredoc are literal — safe to approve
32
+ echo '{"permissionDecision":"allow","permissionDecisionReason":"Backticks are literal characters inside quoted heredoc (<<'"'"'EOF'"'"')"}'
33
+ exit 0
34
+ fi
35
+
36
+ # Also handle unquoted heredoc with git commit pattern
37
+ # git commit -m "$(cat <<EOF ... `code` ... EOF)" — common pattern
38
+ if echo "$COMMAND" | grep -qE 'git\s+commit.*<<\s*[A-Za-z_]+' && \
39
+ echo "$COMMAND" | grep -qE '`[a-zA-Z_][a-zA-Z0-9_]*`'; then
40
+ # Likely markdown formatting backticks in commit message
41
+ echo '{"permissionDecision":"allow","permissionDecisionReason":"Backticks appear to be markdown formatting in commit message"}'
42
+ exit 0
43
+ fi
44
+
45
+ exit 0
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # hook-stdout-sanitizer.sh — Prevent hook stdout from corrupting tool results
3
+ #
4
+ # Solves: Worktree path corrupted by hook stdout (#40262).
5
+ # When a hook writes to stdout (instead of stderr), the output
6
+ # gets concatenated into tool results like file paths, causing
7
+ # corruption (e.g., worktree path becomes "/path/to/repo{hookJSON}").
8
+ #
9
+ # How it works: Wraps another hook script, redirecting all of its stdout
10
+ # to stderr. Only valid JSON hookSpecificOutput is sent to stdout.
11
+ # This prevents accidental stdout pollution.
12
+ #
13
+ # Usage: Wrap any existing hook:
14
+ # Original: "command": "bash ~/.claude/hooks/my-hook.sh"
15
+ # Wrapped: "command": "bash ~/.claude/hooks/hook-stdout-sanitizer.sh ~/.claude/hooks/my-hook.sh"
16
+ #
17
+ # Or use as a template for writing safe hooks.
18
+ #
19
+ # TRIGGER: Any (wrapper for other hooks)
20
+ # MATCHER: Any
21
+
22
+ TARGET_HOOK="$1"
23
+ shift
24
+
25
+ if [ -z "$TARGET_HOOK" ] || [ ! -f "$TARGET_HOOK" ]; then
26
+ echo "Usage: hook-stdout-sanitizer.sh <path-to-hook.sh>" >&2
27
+ exit 0
28
+ fi
29
+
30
+ # Capture stdin (tool input)
31
+ TOOL_INPUT=$(cat)
32
+
33
+ # Run the target hook, capturing stdout and stderr separately
34
+ STDOUT_FILE=$(mktemp)
35
+ STDERR_FILE=$(mktemp)
36
+ echo "$TOOL_INPUT" | bash "$TARGET_HOOK" "$@" > "$STDOUT_FILE" 2> "$STDERR_FILE"
37
+ EXIT_CODE=$?
38
+
39
+ # Forward stderr (safe — always goes to user)
40
+ cat "$STDERR_FILE" >&2
41
+
42
+ # Only forward stdout if it looks like valid hookSpecificOutput JSON
43
+ STDOUT_CONTENT=$(cat "$STDOUT_FILE")
44
+ if [ -n "$STDOUT_CONTENT" ]; then
45
+ # Check if it's valid JSON with hookSpecificOutput
46
+ if echo "$STDOUT_CONTENT" | jq -e '.hookSpecificOutput' &>/dev/null 2>&1 || \
47
+ echo "$STDOUT_CONTENT" | jq -e '.permissionDecision' &>/dev/null 2>&1 || \
48
+ echo "$STDOUT_CONTENT" | jq -e '.systemMessage' &>/dev/null 2>&1; then
49
+ # Valid hook output — forward to stdout
50
+ echo "$STDOUT_CONTENT"
51
+ else
52
+ # Not valid hook JSON — redirect to stderr to prevent corruption
53
+ echo "⚠ hook-stdout-sanitizer: redirected non-JSON stdout to stderr" >&2
54
+ echo "$STDOUT_CONTENT" >&2
55
+ fi
56
+ fi
57
+
58
+ # Clean up
59
+ rm -f "$STDOUT_FILE" "$STDERR_FILE"
60
+
61
+ exit $EXIT_CODE