Session Timeout Handler
Build resilient workflows that gracefully handle Claude Code's 2-minute timeout constraints.
Overview
Claude Code has a 120-second (2-minute) default tool timeout. This skill provides proven patterns for building workflows that work within this constraint while accomplishing complex, long-running tasks through intelligent chunking, checkpoints, and resumability.
When to Use
Use this skill when:
- Processing 50+ items in a loop
- Running commands that take >60 seconds
- Downloading multiple large files
- Performing bulk API calls
- Processing large datasets
- Running database migrations
- Building large projects
- Cloning multiple repositories
- Any operation risking timeout
Core Strategies
Strategy 1: Batch Processing
#!/bin/bash
# Process items in small batches
ITEMS_FILE="items.txt"
BATCH_SIZE=10
OFFSET=${1:-0}
# Process one batch
tail -n +$((OFFSET + 1)) "$ITEMS_FILE" | head -$BATCH_SIZE | while read item; do
process_item "$item"
echo "✓ Processed: $item"
done
# Calculate next offset
NEXT_OFFSET=$((OFFSET + BATCH_SIZE))
TOTAL=$(wc -l < "$ITEMS_FILE")
if [ $NEXT_OFFSET -lt $TOTAL ]; then
echo ""
echo "Progress: $NEXT_OFFSET/$TOTAL"
echo "Run: ./script.sh $NEXT_OFFSET"
else
echo "✓ All items processed!"
fi
Strategy 2: Checkpoint Resume
#!/bin/bash
# Checkpoint-based workflow
CHECKPOINT_FILE=".progress"
ITEMS=("item1" "item2" "item3" ... "item100")
# Load last checkpoint
LAST_COMPLETED=$(cat "$CHECKPOINT_FILE" 2>/dev/null || echo "-1")
START_INDEX=$((LAST_COMPLETED + 1))
# Process next batch (10 items)
END_INDEX=$((START_INDEX + 10))
[ $END_INDEX -gt ${#ITEMS[@]} ] && END_INDEX=${#ITEMS[@]}
for i in $(seq $START_INDEX $END_INDEX); do
process "${ITEMS[$i]}"
# Save checkpoint after each item
echo "$i" > "$CHECKPOINT_FILE"
echo "✓ Completed $i/${#ITEMS[@]}"
done
# Check if finished
if [ $END_INDEX -eq ${#ITEMS[@]} ]; then
echo "🎉 All items complete!"
rm "$CHECKPOINT_FILE"
else
echo "📋 Run again to continue from item $((END_INDEX + 1))"
fi
Strategy 3: State Machine
#!/bin/bash
# Multi-phase state machine workflow
STATE_FILE=".workflow_state"
STATE=$(cat "$STATE_FILE" 2>/dev/null || echo "INIT")
case $STATE in
INIT)
echo "Phase 1: Initialization"
setup_environment
echo "DOWNLOAD" > "$STATE_FILE"
echo "✓ Phase 1 complete. Run again for Phase 2."
;;
DOWNLOAD)
echo "Phase 2: Download (batch 1/5)"
download_batch_1
echo "PROCESS_1" > "$STATE_FILE"
echo "✓ Phase 2 complete. Run again for Phase 3."
;;
PROCESS_1)
echo "Phase 3: Process (batch 1/3)"
process_batch_1
echo "PROCESS_2" > "$STATE_FILE"
echo "✓ Phase 3 complete. Run again for Phase 4."
;;
PROCESS_2)
echo "Phase 4: Process (batch 2/3)"
process_batch_2
echo "PROCESS_3" > "$STATE_FILE"
echo "✓ Phase 4 complete. Run again for Phase 5."
;;
PROCESS_3)
echo "Phase 5: Process (batch 3/3)"
process_batch_3
echo "FINALIZE" > "$STATE_FILE"
echo "✓ Phase 5 complete. Run again to finalize."
;;
FINALIZE)
echo "Phase 6: Finalization"
cleanup_and_report
echo "COMPLETE" > "$STATE_FILE"
echo "🎉 Workflow complete!"
;;
COMPLETE)
echo "Workflow already completed."
cat results.txt
;;
esac
Strategy 4: Progress Tracking
#!/bin/bash
# Detailed progress tracking with JSON
PROGRESS_FILE="progress.json"
# Initialize progress
init_progress() {
cat > "$PROGRESS_FILE" << EOF
{
"total": $1,
"completed": 0,
"failed": 0,
"last_updated": "$(date -Iseconds)",
"completed_items": []
}
EOF
}
# Update progress
update_progress() {
local item="$1"
local status="$2" # "success" or "failed"
# Read current progress
local completed=$(jq -r '.completed' "$PROGRESS_FILE")
local failed=$(jq -r '.failed' "$PROGRESS_FILE")
if [ "$status" = "success" ]; then
completed=$((completed + 1))
else
failed=$((failed + 1))
fi
# Update JSON
jq --arg item "$item" \
--arg status "$status" \
--arg timestamp "$(date -Iseconds)" \
--argjson completed "$completed" \
--argjson failed "$failed" \
'.completed = $completed |
.failed = $failed |
.last_updated = $timestamp |
.completed_items += [{item: $item, status: $status, timestamp: $timestamp}]' \
"$PROGRESS_FILE" > "$PROGRESS_FILE.tmp"
mv "$PROGRESS_FILE.tmp" "$PROGRESS_FILE"
}
# Show progress
show_progress() {
jq -r '"Progress: \(.completed)/\(.total) completed, \(.failed) failed"' "$PROGRESS_FILE"
}
# Example usage
init_progress 100
for item in $(seq 1 10); do # Process 10 at a time
if process_item "$item"; then
update_progress "item-$item" "success"
else
update_progress "item-$item" "failed"
fi
done
show_progress
Timeout-Safe Patterns
Pattern: Parallel with Rate Limiting
#!/bin/bash
# Process items in parallel with limits
MAX_PARALLEL=3
ACTIVE_JOBS=0
process_with_limit() {
local item="$1"
# Wait if at max parallel
while [ $ACTIVE_JOBS -ge $MAX_PARALLEL ]; do
wait -n # Wait for any job to finish
ACTIVE_JOBS=$((ACTIVE_JOBS - 1))
done
# Start background job
(
process_item "$item"
echo "✓ $item"
) &
ACTIVE_JOBS=$((ACTIVE_JOBS + 1))
# Rate limiting
sleep 0.5
}
# Process items
for item in $(seq 1 15); do
process_with_limit "item-$item"
done
# Wait for remaining
wait
echo "✓ All processing complete"
Pattern: Timeout with Fallback
#!/bin/bash
# Try with timeout, fallback to cached/partial result
OPERATION_TIMEOUT=90 # 90 seconds, leave buffer
if timeout ${OPERATION_TIMEOUT}s expensive_operation > result.txt 2>&1; then
echo "✓ Operation completed"
cat result.txt
else
EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
echo "⚠️ Operation timed out after ${OPERATION_TIMEOUT}s"
# Use cached result if available
if [ -f "cached_result.txt" ]; then
echo "Using cached result from $(stat -c %y cached_result.txt)"
cat cached_result.txt
else
echo "Partial result:"
cat result.txt
echo ""
echo "Run again to continue processing"
fi
else
echo "✗ Operation failed with code $EXIT_CODE"
fi
fi
Pattern: Incremental Build
#!/bin/bash
# Incremental build with caching
BUILD_CACHE=".build_cache"
SOURCE_DIR="src"
# Track what needs rebuilding
mkdir -p "$BUILD_CACHE"
# Find changed files
find "$SOURCE_DIR" -type f -newer "$BUILD_CACHE/last_build" 2>/dev/null > changed_files.txt
if [ ! -s changed_files.txt ]; then
echo "✓ No changes detected, using cached build"
exit 0
fi
# Build only changed files (in batches)
head -10 changed_files.txt | while read file; do
echo "Building: $file"
build_file "$file"
done
# Update timestamp
touch "$BUILD_CACHE/last_build"
REMAINING=$(tail -n +11 changed_files.txt | wc -l)
if [ $REMAINING -gt 0 ]; then
echo ""
echo "⚠️ $REMAINING files remaining. Run again to continue."
else
echo "✓ Build complete!"
fi
Real-World Examples
Example 1: Bulk Repository Cloning
#!/bin/bash
# Clone 50 repos without timing out
REPOS_FILE="repos.txt"
CHECKPOINT="cloned.txt"
BATCH_SIZE=5
# Skip already cloned
while read repo_url; do
# Check checkpoint
if grep -qF "$repo_url" "$CHECKPOINT" 2>/dev/null; then
echo "✓ Skip: $repo_url (already cloned)"
continue
fi
REPO_NAME=$(basename "$repo_url" .git)
# Clone with timeout
if timeout 60s git clone --depth 1 "$repo_url" "$REPO_NAME" 2>/dev/null; then
echo "$repo_ur