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.
- package/2026-03-29 +2 -0
- package/COOKBOOK.md +35 -0
- package/README.md +57 -6
- package/TROUBLESHOOTING.md +45 -0
- package/examples/cch-cache-guard.sh +25 -0
- package/examples/compact-alert-notification.sh +39 -0
- package/examples/conversation-history-guard.sh +73 -0
- package/examples/dotenv-watch.sh +5 -5
- package/examples/file-change-undo-tracker.sh +2 -2
- package/examples/image-file-validator.sh +40 -0
- package/examples/mcp-warmup-wait.sh +39 -0
- package/examples/permission-denial-enforcer.sh +92 -0
- package/examples/pre-compact-transcript-backup.sh +71 -0
- package/examples/prompt-usage-logger.sh +53 -0
- package/examples/read-only-mode.sh +77 -0
- package/examples/replace-all-guard.sh +56 -0
- package/examples/ripgrep-permission-fix.sh +58 -0
- package/examples/session-backup-on-start.sh +72 -0
- package/examples/session-index-repair.sh +87 -0
- package/examples/subagent-error-detector.sh +73 -0
- package/examples/subagent-scope-validator.sh +53 -29
- package/examples/task-integrity-guard.sh +82 -0
- package/examples/working-directory-fence.sh +91 -0
- package/index.mjs +28 -9
- package/package.json +7 -3
- package/scripts.json +9 -9
|
@@ -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
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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=$(
|
|
22
|
-
[ -z "$PROMPT" ] && exit 0
|
|
42
|
+
PROMPT=$(printf '%s' "$INPUT" | jq -r '.tool_input.prompt // empty' 2>/dev/null)
|
|
23
43
|
|
|
24
|
-
|
|
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:
|
|
28
|
-
|
|
29
|
-
|
|
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:
|
|
33
|
-
if !
|
|
34
|
-
WARNINGS="${WARNINGS}
|
|
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:
|
|
38
|
-
if !
|
|
39
|
-
WARNINGS="${WARNINGS}
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|