-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbg-task-limiter.sh
More file actions
79 lines (64 loc) · 2.41 KB
/
bg-task-limiter.sh
File metadata and controls
79 lines (64 loc) · 2.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env bash
# bg-task-limiter.sh
# Claude Code PreToolUse hook that prevents background bash tasks from
# respawning infinitely by tracking spawn attempts per command pattern
# and killing runaway processes.
set -euo pipefail
# --- Configuration (override via environment variables) ---
MAX_RESPAWNS="${BG_TASK_MAX_RESPAWNS:-5}"
WINDOW_SECONDS="${BG_TASK_WINDOW_SECONDS:-300}"
TRACK_DIR="${BG_TASK_TRACK_DIR:-/tmp/claude-bg-tasks}"
LOG_FILE="${BG_TASK_LOG_FILE:-/tmp/claude-bg-task-limiter.log}"
mkdir -p "$TRACK_DIR"
# Read hook input (JSON on stdin)
INPUT=$(cat)
# Only act on Bash tool calls
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || exit 0
[ "$TOOL_NAME" = "Bash" ] || exit 0
# Only act on background tasks
IS_BG=$(echo "$INPUT" | jq -r '.tool_input.run_in_background // false' 2>/dev/null) || exit 0
[ "$IS_BG" = "true" ] || exit 0
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null) || exit 0
[ -n "$COMMAND" ] || exit 0
# Normalize the command: collapse numbers and temp paths so similar
# invocations hash to the same pattern.
normalize() {
sed -E 's/[0-9]{2,}/N/g; s|/tmp/[^ ]*|TMPPATH|g; s/[[:space:]]+/ /g'
}
if command -v md5 &>/dev/null; then
PATTERN=$(echo "$COMMAND" | normalize | md5 -q)
else
PATTERN=$(echo "$COMMAND" | normalize | md5sum | cut -d' ' -f1)
fi
TRACK_FILE="$TRACK_DIR/$PATTERN"
NOW=$(date +%s)
CUTOFF=$((NOW - WINDOW_SECONDS))
# Count recent spawn attempts within the sliding time window
RECENT_COUNT=0
KEPT=""
if [ -f "$TRACK_FILE" ]; then
while IFS= read -r ts; do
if [ "${ts:-0}" -ge "$CUTOFF" ] 2>/dev/null; then
KEPT="${KEPT}${ts}"$'\n'
RECENT_COUNT=$((RECENT_COUNT + 1))
fi
done < "$TRACK_FILE"
printf '%s' "$KEPT" > "$TRACK_FILE"
fi
# Block if over the limit
if [ "$RECENT_COUNT" -ge "$MAX_RESPAWNS" ]; then
# Kill existing processes matching this command
pkill -f "$COMMAND" 2>/dev/null || true
TIMESTAMP=$(date -Iseconds 2>/dev/null || date)
printf '[%s] BLOCKED (count=%d limit=%d window=%ds): %s\n' \
"$TIMESTAMP" "$RECENT_COUNT" "$MAX_RESPAWNS" "$WINDOW_SECONDS" \
"$COMMAND" >> "$LOG_FILE"
cat >&2 <<EOF
Background task BLOCKED: command pattern spawned $RECENT_COUNT times in ${WINDOW_SECONDS}s (limit: $MAX_RESPAWNS).
Matching processes killed. See $LOG_FILE for details.
EOF
exit 2
fi
# Record this spawn attempt
echo "$NOW" >> "$TRACK_FILE"
exit 0