Created
February 20, 2026 06:20
-
-
Save ds17f/f4dd09c230122ac5e6875fce9cd1920a to your computer and use it in GitHub Desktop.
Schedule claude --resume sessions to run overnight in tmux panes. Sleep until a target time, then launch each session as an interactive Claude Code instance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # claude-resume — Schedule claude --resume sessions to run overnight in tmux panes | |
| usage() { | |
| cat <<'EOF' | |
| Usage: claude-resume <time> <sessions> [options] | |
| Arguments: | |
| <time> Target time to start (e.g., 2:00am, 14:00, now) | |
| <sessions> Comma-separated session names/search terms | |
| Options: | |
| --prompt "…" Override default prompt (default: "execute the plan") | |
| --model M Override model (e.g., sonnet) | |
| --help Show this help | |
| Sessions run as interactive claude instances in tmux panes. After they | |
| finish processing the prompt, they stay open for you to interact with. | |
| Requires tmux. | |
| Examples: | |
| claude-resume 2:00am fix-android-auto,fix-dupe-qr | |
| claude-resume now test-session | |
| claude-resume 2:00am s1,s2 --prompt "implement step 1" | |
| claude-resume 2:00am s1,s2 --model sonnet | |
| EOF | |
| exit 0 | |
| } | |
| die() { echo "error: $*" >&2; exit 1; } | |
| # --- Parse arguments --- | |
| [[ $# -lt 1 ]] && usage | |
| [[ "$1" == "--help" || "$1" == "-h" ]] && usage | |
| [[ $# -lt 2 ]] && die "missing <sessions> argument" | |
| TARGET_TIME="$1" | |
| SESSION_LIST="$2" | |
| shift 2 | |
| PROMPT="execute the plan" | |
| MODEL="" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --prompt) [[ $# -lt 2 ]] && die "--prompt requires a value"; PROMPT="$2"; shift 2 ;; | |
| --model) [[ $# -lt 2 ]] && die "--model requires a value"; MODEL="$2"; shift 2 ;; | |
| --help|-h) usage ;; | |
| *) die "unknown option: $1" ;; | |
| esac | |
| done | |
| # --- Parse sessions --- | |
| IFS=',' read -ra SESSIONS <<< "$SESSION_LIST" | |
| [[ ${#SESSIONS[@]} -eq 0 ]] && die "no sessions specified" | |
| # --- Require tmux --- | |
| [[ -z "${TMUX:-}" ]] && die "not in a tmux session. Run this from inside tmux." | |
| # --- Compute sleep duration --- | |
| RUN_ID=$(date +%Y%m%d-%H%M%S) | |
| LOG_DIR="$HOME/.claude/resume-logs/$RUN_ID" | |
| mkdir -p "$LOG_DIR" | |
| if [[ "$TARGET_TIME" == "now" ]]; then | |
| SLEEP_SECONDS=0 | |
| WAKE_DISPLAY="now" | |
| else | |
| TARGET_EPOCH=$(date -d "$TARGET_TIME" +%s 2>/dev/null) \ | |
| || die "cannot parse time: '$TARGET_TIME' (try 2:00am, 14:00, etc.)" | |
| NOW_EPOCH=$(date +%s) | |
| SLEEP_SECONDS=$(( TARGET_EPOCH - NOW_EPOCH )) | |
| if [[ $SLEEP_SECONDS -lt 0 ]]; then | |
| TARGET_EPOCH=$(date -d "tomorrow $TARGET_TIME" +%s 2>/dev/null) \ | |
| || die "cannot parse time: 'tomorrow $TARGET_TIME'" | |
| SLEEP_SECONDS=$(( TARGET_EPOCH - NOW_EPOCH )) | |
| fi | |
| WAKE_DISPLAY=$(date -d "@$TARGET_EPOCH" "+%-I:%M %p") | |
| fi | |
| format_duration() { | |
| local secs=$1 | |
| local hours=$(( secs / 3600 )) | |
| local mins=$(( (secs % 3600) / 60 )) | |
| if [[ $hours -gt 0 ]]; then | |
| echo "${hours}h ${mins}m" | |
| else | |
| echo "${mins}m" | |
| fi | |
| } | |
| # --- Print confirmation --- | |
| echo "" | |
| if [[ "$TARGET_TIME" == "now" ]]; then | |
| echo "claude-resume starting immediately" | |
| else | |
| echo "claude-resume scheduled for $WAKE_DISPLAY (in $(format_duration $SLEEP_SECONDS))" | |
| fi | |
| echo "" | |
| echo "Sessions (${#SESSIONS[@]}):" | |
| for i in "${!SESSIONS[@]}"; do | |
| echo " $((i+1)). ${SESSIONS[$i]}" | |
| done | |
| echo "" | |
| echo "Prompt: \"$PROMPT\"" | |
| [[ -n "$MODEL" ]] && echo "Model: $MODEL" | |
| echo "Logs: $LOG_DIR/" | |
| echo "" | |
| # --- Sleep --- | |
| if [[ $SLEEP_SECONDS -gt 0 ]]; then | |
| echo "Press Ctrl+C to cancel." | |
| echo "Sleeping..." | |
| sleep "$SLEEP_SECONDS" | |
| echo "" | |
| echo "Waking up at $(date "+%-I:%M %p")..." | |
| echo "" | |
| fi | |
| # --- Launch tmux panes --- | |
| WINDOW_NAME="claude-resume" | |
| tmux new-window -n "$WINDOW_NAME" | |
| BASE_PANE=$(tmux display-message -p -t "$WINDOW_NAME" '#{pane_id}') | |
| PANE_IDS=("$BASE_PANE") | |
| for i in "${!SESSIONS[@]}"; do | |
| [[ $i -eq 0 ]] && continue | |
| NEW_PANE=$(tmux split-window -t "$WINDOW_NAME" -P -F '#{pane_id}') | |
| PANE_IDS+=("$NEW_PANE") | |
| tmux select-layout -t "$WINDOW_NAME" tiled | |
| done | |
| echo "Launching ${#SESSIONS[@]} session(s)..." | |
| for i in "${!SESSIONS[@]}"; do | |
| session="${SESSIONS[$i]}" | |
| log_file="$LOG_DIR/${session}.log" | |
| pane="${PANE_IDS[$i]}" | |
| cmd="claude --resume '${session}' --dangerously-skip-permissions" | |
| [[ -n "$MODEL" ]] && cmd+=" --model $MODEL" | |
| cmd+=" '${PROMPT}'" | |
| # Wrap in script(1) for logging | |
| full_cmd="script -q -c \"$cmd\" '${log_file}'" | |
| tmux send-keys -t "$pane" "$full_cmd" Enter | |
| # Stagger API calls slightly | |
| if [[ $i -lt $(( ${#SESSIONS[@]} - 1 )) ]]; then | |
| sleep 2 | |
| fi | |
| done | |
| echo "" | |
| echo "All sessions launched in tmux window '$WINDOW_NAME'." | |
| echo "" | |
| echo " tmux select-window -t $WINDOW_NAME" | |
| echo "" | |
| echo "Logs: $LOG_DIR/" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment