cc-safe-setup 29.6.37 → 29.6.39

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.
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # pre-compact-transcript-backup.sh — Backup transcript before compaction
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Creates a full copy of the session transcript JSONL before
7
+ # compaction begins. If compaction fails (rate limit, API error),
8
+ # the original transcript is preserved and can be restored.
9
+ #
10
+ # TRIGGER: PreCompact
11
+ # MATCHER: (none — PreCompact has no matcher)
12
+ #
13
+ # WHY THIS MATTERS:
14
+ # Compaction wipes message content from the JSONL transcript
15
+ # BEFORE the compaction API call succeeds. If the API call
16
+ # fails (e.g., rate limit), all original content is permanently
17
+ # lost — the transcript is left with thousands of empty messages
18
+ # and no compaction summary. This hook ensures a recoverable
19
+ # backup exists.
20
+ #
21
+ # WHAT IT DOES:
22
+ # 1. Reads transcript_path from stdin JSON
23
+ # 2. Copies the full JSONL file to a backup location
24
+ # 3. Keeps last 3 backups per session to save disk space
25
+ #
26
+ # CONFIGURATION:
27
+ # CC_COMPACT_BACKUP_DIR — backup directory
28
+ # (default: ~/.claude/compact-backups)
29
+ # CC_COMPACT_BACKUP_KEEP — number of backups to keep (default: 3)
30
+ #
31
+ # RECOVERY:
32
+ # cp ~/.claude/compact-backups/<session-id>/latest.jsonl \
33
+ # ~/.claude/projects/<project>/sessions/<session>.jsonl
34
+ #
35
+ # RELATED ISSUES:
36
+ # https://github.com/anthropics/claude-code/issues/40352
37
+ # ================================================================
38
+
39
+ set -u
40
+
41
+ INPUT=$(cat)
42
+
43
+ BACKUP_DIR="${CC_COMPACT_BACKUP_DIR:-${HOME}/.claude/compact-backups}"
44
+ KEEP="${CC_COMPACT_BACKUP_KEEP:-3}"
45
+
46
+ # Get transcript path from hook input
47
+ TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
48
+
49
+ if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then
50
+ exit 0
51
+ fi
52
+
53
+ # Create backup
54
+ BASENAME=$(basename "$TRANSCRIPT" .jsonl)
55
+ DEST_DIR="${BACKUP_DIR}/${BASENAME}"
56
+ mkdir -p "$DEST_DIR"
57
+
58
+ TIMESTAMP=$(date -u +"%Y%m%d-%H%M%S")
59
+ BACKUP_FILE="${DEST_DIR}/${TIMESTAMP}.jsonl"
60
+
61
+ cp "$TRANSCRIPT" "$BACKUP_FILE" 2>/dev/null
62
+
63
+ if [ -f "$BACKUP_FILE" ]; then
64
+ SIZE=$(du -sh "$BACKUP_FILE" 2>/dev/null | cut -f1)
65
+ printf 'Pre-compact backup: %s (%s)\n' "$BACKUP_FILE" "$SIZE" >&2
66
+
67
+ # Prune old backups
68
+ ls -1t "$DEST_DIR"/*.jsonl 2>/dev/null | tail -n +$((KEEP + 1)) | xargs rm -f 2>/dev/null
69
+ fi
70
+
71
+ exit 0
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # prompt-usage-logger.sh — Log every prompt with timestamps
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Track when and what you send to Claude, so you can correlate
7
+ # prompts with token usage on the billing dashboard.
8
+ # Helps diagnose unexpectedly fast token consumption.
9
+ #
10
+ # TRIGGER: UserPromptSubmit
11
+ # MATCHER: ""
12
+ #
13
+ # HOW IT WORKS:
14
+ # Reads the prompt from stdin JSON, truncates to first 100 chars,
15
+ # and appends a timestamped line to a log file.
16
+ # After a session, compare timestamps with your usage dashboard
17
+ # to identify which interactions consumed the most tokens.
18
+ #
19
+ # CONFIGURATION:
20
+ # CC_PROMPT_LOG=/tmp/claude-usage-log.txt (default log path)
21
+ #
22
+ # OUTPUT:
23
+ # Passes through original input on stdout (required for
24
+ # UserPromptSubmit hooks).
25
+ #
26
+ # EXAMPLE LOG:
27
+ # 12:34:56 prompt=Read the file src/main.ts and explain the error handling
28
+ # 12:35:23 prompt=Fix the bug in the validateInput function
29
+ #
30
+ # SEE ALSO:
31
+ # cost-tracker.sh (PostToolUse-based cost estimation)
32
+ # daily-usage-tracker.sh (daily aggregation)
33
+ #
34
+ # RELATED ISSUES:
35
+ # https://github.com/anthropics/claude-code/issues/41249
36
+ # https://github.com/anthropics/claude-code/issues/38335
37
+ # https://github.com/anthropics/claude-code/issues/16157
38
+ # ================================================================
39
+
40
+ set -euo pipefail
41
+
42
+ INPUT=$(cat)
43
+
44
+ LOG_FILE="${CC_PROMPT_LOG:-/tmp/claude-usage-log.txt}"
45
+
46
+ # Extract first 100 chars of the prompt
47
+ PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt | .[0:100]' 2>/dev/null || echo "(parse error)")
48
+
49
+ # Append timestamped entry
50
+ echo "$(date -u +%H:%M:%S) prompt=$PROMPT" >> "$LOG_FILE"
51
+
52
+ # Pass through original input (required for UserPromptSubmit)
53
+ printf '%s\n' "$INPUT"
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # read-only-mode.sh — Block all file modifications and destructive commands
3
+ #
4
+ # Solves: Claude Code making unauthorized changes during test-only or
5
+ # audit-only tasks, even when CLAUDE.md says "no changes" (#41063)
6
+ #
7
+ # Why a hook instead of CLAUDE.md: CLAUDE.md instructions are advisory —
8
+ # the model can ignore them under pressure (e.g., when it finds a bug
9
+ # and instinctively wants to fix it). Hooks are enforced at the process
10
+ # level and cannot be bypassed.
11
+ #
12
+ # Toggle: Set CLAUDE_READONLY=1 to enable, unset to disable
13
+ # export CLAUDE_READONLY=1 # enable read-only mode
14
+ # unset CLAUDE_READONLY # disable
15
+ #
16
+ # Usage: Add to settings.json as a PreToolUse hook
17
+ #
18
+ # {
19
+ # "hooks": {
20
+ # "PreToolUse": [
21
+ # {
22
+ # "matcher": "Write|Edit|NotebookEdit",
23
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/read-only-mode.sh" }]
24
+ # },
25
+ # {
26
+ # "matcher": "Bash",
27
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/read-only-mode.sh" }]
28
+ # }
29
+ # ]
30
+ # }
31
+ # }
32
+ #
33
+ # TRIGGER: PreToolUse MATCHER: "Write|Edit|NotebookEdit|Bash"
34
+
35
+ # Only active when CLAUDE_READONLY=1
36
+ [[ "${CLAUDE_READONLY:-}" != "1" ]] && exit 0
37
+
38
+ INPUT=$(cat)
39
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
40
+
41
+ # Block all file write tools
42
+ case "$TOOL" in
43
+ Write|Edit|NotebookEdit)
44
+ echo "BLOCKED: Read-only mode is active. Document this in your report instead of modifying files." >&2
45
+ exit 2
46
+ ;;
47
+ esac
48
+
49
+ # For Bash, block destructive commands but allow reads
50
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
51
+ [[ -z "$CMD" ]] && exit 0
52
+
53
+ # Allow read-only commands
54
+ if echo "$CMD" | grep -qE '^\s*(ls|cat|head|tail|less|more|wc|find|grep|rg|ag|git\s+(log|show|diff|status|branch)|pwd|echo|printf|date|whoami|env|which|type|file|stat|du|df|uname|hostname|id|test|true|false|\[)'; then
55
+ exit 0
56
+ fi
57
+
58
+ # Block database mutations
59
+ if echo "$CMD" | grep -qiE '\b(ALTER|DROP|TRUNCATE|INSERT|UPDATE|DELETE|CREATE|GRANT|REVOKE)\b'; then
60
+ echo "BLOCKED: Read-only mode — database mutations are not allowed. Document the needed change in your report." >&2
61
+ exit 2
62
+ fi
63
+
64
+ # Block Docker/service mutations
65
+ if echo "$CMD" | grep -qiE 'docker\s+(restart|stop|rm|build|compose\s+up)|systemctl\s+(start|stop|restart|enable|disable)|service\s+\S+\s+(start|stop|restart)'; then
66
+ echo "BLOCKED: Read-only mode — service mutations are not allowed. Document the needed change in your report." >&2
67
+ exit 2
68
+ fi
69
+
70
+ # Block file writes via shell
71
+ if echo "$CMD" | grep -qE '(>\s|>>|tee\s|mv\s|cp\s|rm\s|mkdir\s|rmdir\s|chmod\s|chown\s|ln\s|touch\s|sed\s+-i|install\s|pip\s+install|npm\s+(install|publish|unpublish)|yarn\s+add|apt\s+install|brew\s+install)'; then
72
+ echo "BLOCKED: Read-only mode — file/package modifications are not allowed. Document what needs to change in your report." >&2
73
+ exit 2
74
+ fi
75
+
76
+ # Allow everything else (mostly read commands)
77
+ exit 0
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # replace-all-guard.sh — Warn when Edit uses replace_all: true
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # The Edit tool's replace_all parameter replaces ALL occurrences
7
+ # of old_string in a file. This is extremely dangerous for data
8
+ # files where the same string appears in different contexts
9
+ # (e.g., column names, values, SQL). A single replace_all can
10
+ # corrupt dozens of lines in ways that are hard to detect.
11
+ #
12
+ # TRIGGER: PreToolUse
13
+ # MATCHER: "Edit"
14
+ #
15
+ # WHY THIS MATTERS:
16
+ # Claude frequently uses replace_all as a shortcut instead of
17
+ # making targeted single-line edits. In code files this is
18
+ # usually safe, but in data/config/SQL files it causes silent
19
+ # corruption — correct values are overwritten along with the
20
+ # intended target.
21
+ #
22
+ # WHAT IT DOES:
23
+ # Checks if the Edit tool input has replace_all: true. If so,
24
+ # warns via stderr. In strict mode (CC_BLOCK_REPLACE_ALL=1),
25
+ # blocks the operation entirely.
26
+ #
27
+ # CONFIGURATION:
28
+ # CC_BLOCK_REPLACE_ALL=1 — block replace_all operations (default: warn only)
29
+ #
30
+ # RELATED ISSUES:
31
+ # https://github.com/anthropics/claude-code/issues/41681
32
+ # ================================================================
33
+
34
+ set -u
35
+
36
+ INPUT=$(cat)
37
+
38
+ REPLACE_ALL=$(printf '%s' "$INPUT" | jq -r '.tool_input.replace_all // false' 2>/dev/null)
39
+
40
+ if [ "$REPLACE_ALL" = "true" ]; then
41
+ FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // "unknown"' 2>/dev/null)
42
+ OLD=$(printf '%s' "$INPUT" | jq -r '.tool_input.old_string // "" | .[0:60]' 2>/dev/null)
43
+
44
+ if [ "${CC_BLOCK_REPLACE_ALL:-0}" = "1" ]; then
45
+ printf 'BLOCKED: replace_all=true on %s\n' "$FILE" >&2
46
+ printf 'Pattern: "%s"\n' "$OLD" >&2
47
+ printf 'replace_all replaces ALL occurrences. Use targeted single edits instead.\n' >&2
48
+ exit 2
49
+ fi
50
+
51
+ printf '\n⚠ replace_all=true detected on %s\n' "$FILE" >&2
52
+ printf ' Pattern: "%s"\n' "$OLD" >&2
53
+ printf ' This replaces ALL occurrences. Verify this is intentional.\n\n' >&2
54
+ fi
55
+
56
+ exit 0
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # ripgrep-permission-fix.sh — Auto-fix ripgrep execute permission on start
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # After Claude Code upgrades, the vendored ripgrep binary
7
+ # sometimes loses its execute permission (installed as 644
8
+ # instead of 755). This silently breaks custom commands,
9
+ # skills discovery, and file search. This hook auto-fixes
10
+ # the permission on every session start.
11
+ #
12
+ # TRIGGER: SessionStart
13
+ # MATCHER: (none)
14
+ #
15
+ # WHY THIS MATTERS:
16
+ # Claude Code uses ripgrep internally to scan .claude/commands/
17
+ # and .claude/skills/ for .md files. Without execute permission,
18
+ # it silently returns empty results, making all custom commands
19
+ # and skills invisible. Multiple users hit this on v2.1.88/89.
20
+ #
21
+ # WHAT IT DOES:
22
+ # Finds the vendored ripgrep binary and adds +x if missing.
23
+ # No-op if ripgrep already has execute permission.
24
+ #
25
+ # RELATED ISSUES:
26
+ # https://github.com/anthropics/claude-code/issues/41933
27
+ # https://github.com/anthropics/claude-code/issues/41882
28
+ # https://github.com/anthropics/claude-code/issues/41243
29
+ # ================================================================
30
+
31
+ # Find the Claude Code installation directory
32
+ CLAUDE_BIN=$(command -v claude 2>/dev/null)
33
+ [ -z "$CLAUDE_BIN" ] && exit 0
34
+
35
+ # Resolve symlinks to find the actual installation
36
+ CLAUDE_REAL=$(readlink -f "$CLAUDE_BIN" 2>/dev/null || realpath "$CLAUDE_BIN" 2>/dev/null)
37
+ [ -z "$CLAUDE_REAL" ] && exit 0
38
+
39
+ CLAUDE_DIR=$(dirname "$CLAUDE_REAL")
40
+
41
+ # Search for vendored ripgrep
42
+ for rg_path in \
43
+ "${CLAUDE_DIR}/../vendor/ripgrep/"*/rg \
44
+ "${CLAUDE_DIR}/../lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/"*/rg \
45
+ "$(npm root -g 2>/dev/null)/@anthropic-ai/claude-code/vendor/ripgrep/"*/rg; do
46
+
47
+ # Expand glob
48
+ for rg in $rg_path; do
49
+ [ -f "$rg" ] || continue
50
+
51
+ if [ ! -x "$rg" ]; then
52
+ chmod +x "$rg" 2>/dev/null
53
+ printf 'Fixed ripgrep permission: %s\n' "$rg" >&2
54
+ fi
55
+ done
56
+ done
57
+
58
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # session-backup-on-start.sh — Backup session JSONL files on start
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Creates a timestamped backup of all session JSONL files when
7
+ # a new session starts. Protects against silent deletion of
8
+ # session data by the desktop app or unexpected corruption.
9
+ #
10
+ # TRIGGER: SessionStart
11
+ # MATCHER: (none — SessionStart has no matcher)
12
+ #
13
+ # WHY THIS MATTERS:
14
+ # The Claude Code desktop app has been observed silently deleting
15
+ # session JSONL files while leaving subagent directories intact.
16
+ # Without backups, entire conversation histories are lost with
17
+ # no way to recover them.
18
+ #
19
+ # WHAT IT DOES:
20
+ # 1. Finds the project session directory
21
+ # 2. Copies all .jsonl files to a timestamped backup directory
22
+ # 3. Keeps only the last 5 backups to avoid disk bloat
23
+ #
24
+ # CONFIGURATION:
25
+ # CC_SESSION_BACKUP_DIR — backup location (default: ~/.claude/session-backups)
26
+ # CC_SESSION_BACKUP_KEEP — number of backups to keep (default: 5)
27
+ #
28
+ # RELATED ISSUES:
29
+ # https://github.com/anthropics/claude-code/issues/41874
30
+ # ================================================================
31
+
32
+ set -u
33
+
34
+ BACKUP_DIR="${CC_SESSION_BACKUP_DIR:-${HOME}/.claude/session-backups}"
35
+ KEEP="${CC_SESSION_BACKUP_KEEP:-5}"
36
+
37
+ # Find the current project's session directory
38
+ CWD=$(pwd)
39
+ PROJECT_NAME=$(printf '%s' "$CWD" | sed 's|/|-|g; s|^-||')
40
+ SESSION_DIR="${HOME}/.claude/projects/${PROJECT_NAME}"
41
+
42
+ if [ ! -d "$SESSION_DIR" ]; then
43
+ exit 0
44
+ fi
45
+
46
+ # Check if there are JSONL files to back up
47
+ JSONL_COUNT=$(find "$SESSION_DIR" -maxdepth 1 -name "*.jsonl" -type f 2>/dev/null | wc -l)
48
+ if [ "$JSONL_COUNT" -eq 0 ]; then
49
+ exit 0
50
+ fi
51
+
52
+ # Create timestamped backup
53
+ TIMESTAMP=$(date -u +"%Y%m%d-%H%M%S")
54
+ DEST="${BACKUP_DIR}/${PROJECT_NAME}/${TIMESTAMP}"
55
+ mkdir -p "$DEST"
56
+
57
+ # Copy JSONL files (not subdirectories — those are subagent sessions)
58
+ cp "$SESSION_DIR"/*.jsonl "$DEST/" 2>/dev/null
59
+
60
+ BACKED_UP=$(find "$DEST" -name "*.jsonl" -type f 2>/dev/null | wc -l)
61
+
62
+ # Prune old backups (keep last N)
63
+ PARENT="${BACKUP_DIR}/${PROJECT_NAME}"
64
+ if [ -d "$PARENT" ]; then
65
+ ls -1dt "$PARENT"/*/ 2>/dev/null | tail -n +$((KEEP + 1)) | xargs rm -rf 2>/dev/null
66
+ fi
67
+
68
+ if [ "$BACKED_UP" -gt 0 ]; then
69
+ printf 'Session backup: %d JSONL files saved to %s\n' "$BACKED_UP" "$DEST" >&2
70
+ fi
71
+
72
+ exit 0
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # session-index-repair.sh — Rebuild sessions-index.json on exit
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # Fixes stale/missing sessions-index.json files that cause
7
+ # `claude --resume` to show old or missing sessions. Runs on
8
+ # session Stop to rebuild the index from actual JSONL files.
9
+ #
10
+ # TRIGGER: Stop
11
+ # MATCHER: (none — Stop has no matcher)
12
+ #
13
+ # WHY THIS MATTERS:
14
+ # Claude Code writes session data to JSONL files but sometimes
15
+ # fails to update sessions-index.json. Without the index,
16
+ # `claude --resume` can't find recent sessions. This hook
17
+ # scans for JSONL files and rebuilds the index.
18
+ #
19
+ # OUTPUT:
20
+ # Updated sessions-index.json in the current project directory.
21
+ # Status message to stderr.
22
+ #
23
+ # RELATED ISSUES:
24
+ # https://github.com/anthropics/claude-code/issues/25032
25
+ # ================================================================
26
+
27
+ set -u
28
+
29
+ # Find the project sessions directory
30
+ PROJECT_DIR="${HOME}/.claude/projects"
31
+
32
+ if [ ! -d "$PROJECT_DIR" ]; then
33
+ exit 0
34
+ fi
35
+
36
+ # Get current working directory's project hash
37
+ # Claude Code uses the cwd path (with slashes replaced by dashes) as the project dir name
38
+ CWD=$(pwd)
39
+ # Convert path to Claude's project directory naming scheme
40
+ PROJECT_NAME=$(printf '%s' "$CWD" | sed 's|/|-|g; s|^-||')
41
+ SESSION_DIR="${PROJECT_DIR}/${PROJECT_NAME}"
42
+
43
+ if [ ! -d "$SESSION_DIR" ]; then
44
+ exit 0
45
+ fi
46
+
47
+ INDEX_FILE="${SESSION_DIR}/sessions-index.json"
48
+
49
+ # Build index from JSONL files
50
+ ENTRIES="["
51
+ FIRST=true
52
+
53
+ for jsonl in "${SESSION_DIR}"/*.jsonl; do
54
+ [ -f "$jsonl" ] || continue
55
+
56
+ # Extract session info from the JSONL filename and content
57
+ BASENAME=$(basename "$jsonl")
58
+ MTIME=$(stat -c '%Y' "$jsonl" 2>/dev/null || stat -f '%m' "$jsonl" 2>/dev/null)
59
+
60
+ # Try to get the session title from custom-title entries
61
+ TITLE=$(grep -o '"type":"custom-title","title":"[^"]*"' "$jsonl" 2>/dev/null | tail -1 | sed 's/.*"title":"//; s/"//')
62
+ if [ -z "$TITLE" ]; then
63
+ # Fallback: use first user message as title
64
+ TITLE=$(head -20 "$jsonl" | grep -o '"role":"user"' -m1 >/dev/null && head -20 "$jsonl" | jq -r 'select(.message.role == "user") | .message.content | .[0:60]' 2>/dev/null | head -1)
65
+ fi
66
+ [ -z "$TITLE" ] && TITLE="(untitled)"
67
+
68
+ if [ "$FIRST" = true ]; then
69
+ FIRST=false
70
+ else
71
+ ENTRIES="${ENTRIES},"
72
+ fi
73
+
74
+ ENTRIES="${ENTRIES}{\"file\":\"${BASENAME}\",\"title\":\"${TITLE}\",\"mtime\":${MTIME:-0}}"
75
+ done
76
+
77
+ ENTRIES="${ENTRIES}]"
78
+
79
+ # Write the index
80
+ printf '%s' "$ENTRIES" | jq '.' > "$INDEX_FILE" 2>/dev/null
81
+
82
+ if [ -f "$INDEX_FILE" ]; then
83
+ COUNT=$(printf '%s' "$ENTRIES" | jq 'length' 2>/dev/null)
84
+ printf 'sessions-index.json rebuilt: %s sessions indexed\n' "${COUNT:-0}" >&2
85
+ fi
86
+
87
+ exit 0
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # ================================================================
3
+ # subagent-error-detector.sh — Detect failed subagent results
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # When a subagent returns, checks whether the result contains
7
+ # API error indicators (529 Overloaded, 500 Internal, timeout).
8
+ # Warns via stderr so the main agent doesn't silently accept
9
+ # error results as valid work.
10
+ #
11
+ # TRIGGER: PostToolUse
12
+ # MATCHER: "Agent"
13
+ #
14
+ # WHY THIS MATTERS:
15
+ # API 529 "Overloaded" errors silently kill parallel subagents.
16
+ # The agent reports "completion" but the result is just an error
17
+ # string. Without detection, the main agent accepts the error
18
+ # as a valid response and continues — losing all subagent work.
19
+ #
20
+ # WHAT IT CHECKS:
21
+ # - 529 Overloaded errors
22
+ # - 500/502/503 API errors
23
+ # - Timeout indicators
24
+ # - Empty or suspiciously short results
25
+ #
26
+ # OUTPUT:
27
+ # Warning to stderr when subagent result looks like an error.
28
+ # Always exits 0 — advisory only.
29
+ #
30
+ # RELATED ISSUES:
31
+ # https://github.com/anthropics/claude-code/issues/41911
32
+ # ================================================================
33
+
34
+ set -u
35
+
36
+ INPUT=$(cat)
37
+
38
+ # Extract the tool result (subagent's returned output)
39
+ RESULT=$(printf '%s' "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null)
40
+
41
+ if [ -z "$RESULT" ]; then
42
+ exit 0
43
+ fi
44
+
45
+ WARNINGS=""
46
+
47
+ # Check for API error patterns
48
+ if printf '%s' "$RESULT" | grep -qiE '529.*overloaded|overloaded_error'; then
49
+ WARNINGS="${WARNINGS} - ⛔ 529 Overloaded error detected — subagent hit API rate limit\n"
50
+ fi
51
+
52
+ if printf '%s' "$RESULT" | grep -qiE '500 Internal|502 Bad Gateway|503 Service Unavailable'; then
53
+ WARNINGS="${WARNINGS} - ⛔ Server error detected in subagent result\n"
54
+ fi
55
+
56
+ if printf '%s' "$RESULT" | grep -qiE 'timeout|timed out|ETIMEDOUT|ECONNRESET'; then
57
+ WARNINGS="${WARNINGS} - ⚠ Timeout detected in subagent result\n"
58
+ fi
59
+
60
+ # Check for suspiciously short results (< 50 chars often means error)
61
+ RESULT_LEN=${#RESULT}
62
+ if [ "$RESULT_LEN" -lt 50 ]; then
63
+ WARNINGS="${WARNINGS} - ⚠ Subagent result is only ${RESULT_LEN} chars — may be an error, not real work\n"
64
+ fi
65
+
66
+ if [ -n "$WARNINGS" ]; then
67
+ printf '\n⚠ Subagent result quality check:\n' >&2
68
+ printf '%b' "$WARNINGS" >&2
69
+ printf 'The subagent may have failed. Verify the result before using it.\n' >&2
70
+ printf 'Consider re-running the subagent or doing the work directly.\n\n' >&2
71
+ fi
72
+
73
+ exit 0
@@ -1,49 +1,73 @@
1
1
  #!/bin/bash
2
- # subagent-scope-validator.sh — Validate subagent task scope before launch
3
- #
4
- # Solves: Main agent's subagent delegation produces poor scoping (#40339).
5
- # Subagents are launched with vague prompts, missing context,
6
- # and no result verification criteria.
7
- #
8
- # How it works: PreToolUse hook on "Agent" that checks the prompt
9
- # for minimum scope requirements:
10
- # 1. Prompt must be longer than 50 characters (not just "do X")
11
- # 2. Must contain file paths or specific identifiers
12
- # 3. Warns if no success criteria are mentioned
2
+ # ================================================================
3
+ # subagent-scope-validator.sh — Warn on vague subagent delegation
4
+ # ================================================================
5
+ # PURPOSE:
6
+ # When the main agent spawns a subagent, checks whether the
7
+ # delegation prompt includes sufficient context: file paths,
8
+ # specific questions, and adequate length. Warns via stderr
9
+ # when the delegation looks vague — the #1 cause of poor
10
+ # subagent results.
13
11
  #
14
12
  # TRIGGER: PreToolUse
15
13
  # MATCHER: "Agent"
14
+ #
15
+ # WHY THIS MATTERS:
16
+ # The main agent often delegates with vague prompts like
17
+ # "investigate the auth flow" instead of "read src/auth/login.ts
18
+ # lines 45-80 and trace how the JWT is validated." Vague prompts
19
+ # produce shallow, incorrect subagent results. This hook catches
20
+ # it before the subagent wastes a context window.
21
+ #
22
+ # WHAT IT CHECKS:
23
+ # 1. Prompt length (< 100 chars is almost always too vague)
24
+ # 2. Presence of file paths (subagents need specific files)
25
+ # 3. Presence of actionable verbs (read, check, verify, find, grep)
26
+ #
27
+ # OUTPUT:
28
+ # Warning to stderr when delegation looks vague.
29
+ # Always exits 0 — advisory only, never blocks.
30
+ #
31
+ # CONFIGURATION:
32
+ # CC_SUBAGENT_MIN_PROMPT_LEN — minimum prompt length (default: 100)
33
+ #
34
+ # RELATED ISSUES:
35
+ # https://github.com/anthropics/claude-code/issues/40339
36
+ # ================================================================
37
+
38
+ set -u
16
39
 
17
40
  INPUT=$(cat)
18
- TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
19
- [ "$TOOL" != "Agent" ] && exit 0
20
41
 
21
- PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty' 2>/dev/null)
22
- [ -z "$PROMPT" ] && exit 0
42
+ PROMPT=$(printf '%s' "$INPUT" | jq -r '.tool_input.prompt // empty' 2>/dev/null)
23
43
 
24
- PROMPT_LEN=${#PROMPT}
44
+ if [ -z "$PROMPT" ]; then
45
+ exit 0
46
+ fi
47
+
48
+ MIN_LEN="${CC_SUBAGENT_MIN_PROMPT_LEN:-100}"
25
49
  WARNINGS=""
26
50
 
27
- # Check 1: Minimum prompt length
28
- if [ "$PROMPT_LEN" -lt 50 ]; then
29
- WARNINGS="${WARNINGS}\n - Prompt is only ${PROMPT_LEN} chars. Subagents need detailed context (50+ chars recommended)"
51
+ # Check 1: Prompt length
52
+ PROMPT_LEN=${#PROMPT}
53
+ if [ "$PROMPT_LEN" -lt "$MIN_LEN" ]; then
54
+ WARNINGS="${WARNINGS} - Prompt is only ${PROMPT_LEN} chars (minimum recommended: ${MIN_LEN})\n"
30
55
  fi
31
56
 
32
- # Check 2: Contains specific identifiers (files, functions, paths)
33
- if ! echo "$PROMPT" | grep -qE '/[a-zA-Z]|\.ts|\.py|\.js|\.md|\.json|\.sh|function |class |def |const |let |var '; then
34
- WARNINGS="${WARNINGS}\n - No file paths or code identifiers found. Subagent may lack context"
57
+ # Check 2: File paths present?
58
+ if ! printf '%s' "$PROMPT" | grep -qE '(/[a-zA-Z0-9_.-]+){2,}|\.[a-z]{1,4}\b|src/|lib/|test/|docs/'; then
59
+ WARNINGS="${WARNINGS} - No file paths detected. Subagents need specific files to read.\n"
35
60
  fi
36
61
 
37
- # Check 3: Success criteria
38
- if ! echo "$PROMPT" | grep -qiE 'verify|confirm|test|check|ensure|must|should|expect|return|report'; then
39
- WARNINGS="${WARNINGS}\n - No success criteria detected. Consider adding verification steps"
62
+ # Check 3: Actionable verbs?
63
+ if ! printf '%s' "$PROMPT" | grep -qiE '\b(read|check|verify|find|grep|search|look at|examine|trace|compare|analyze)\b'; then
64
+ WARNINGS="${WARNINGS} - No actionable verbs found. Tell the subagent exactly what to do.\n"
40
65
  fi
41
66
 
42
- # Output warnings (don't block — just inform)
43
67
  if [ -n "$WARNINGS" ]; then
44
- echo "⚠ Subagent scope review:" >&2
45
- echo -e "$WARNINGS" >&2
46
- echo " Prompt preview: $(echo "$PROMPT" | head -c 100)..." >&2
68
+ printf '\n⚠ Subagent delegation quality check:\n' >&2
69
+ printf '%b' "$WARNINGS" >&2
70
+ printf 'Tip: Include specific file paths, line ranges, and what a complete answer looks like.\n\n' >&2
47
71
  fi
48
72
 
49
73
  exit 0