← echo

Detached background tasks with tmux

Model downloads, fine-tunes, big rsyncs — anything that outlives an SSH session. park drops the command into a detached tmux session and tees its output to a log; parked tells you which ones are still going, finished, or died.

~/.local/bin/park — start a command detached, log it, auto-close on finish:

#!/usr/bin/env bash
# usage: park <session-name> <command...>
set -euo pipefail

if [ "$#" -lt 2 ]; then
    echo "usage: park <session-name> <command...>" >&2
    exit 1
fi

session="$1"; shift

# refuse to clobber a session that's already running
if tmux has-session -t "$session" 2>/dev/null; then
    echo "session '$session' already exists — attach with: tmux attach -t $session" >&2
    exit 1
fi

logdir="$HOME/.park"
mkdir -p "$logdir"
logfile="$logdir/${session}.log"

# -d = detached: tmux runs the command in the background and returns immediately
tmux new-session -d -s "$session" \
    bash -c '
        set -o pipefail
        logfile="$1"; shift
        {
            printf ">>> started: %s\n" "$(date)"
            printf ">>> cmd:"; printf " %q" "$@"; printf "\n\n"  # %q re-quotes each arg exactly
            "$@"
        } 2>&1 | tee "$logfile"
        code=${PIPESTATUS[0]}                                    # real exit code, not tee's
        {
            printf "\n>>> exit code: %s\n" "$code"
            printf ">>> finished: %s\n" "$(date)"
        } >> "$logfile"
    ' bash "$logfile" "$@"

echo "started '$session'  (log: $logfile)"
echo "check later:  tmux ls   |   tail -f $logfile   |   cat $logfile"

~/.local/bin/parked — summarize every task at a glance, or clear finished ones:

#!/usr/bin/env bash
# usage:
#   parked [-l|--logs]                  list tasks (-l also prints each log's last line)
#   parked -k|--cancel <name>           stop a running task (kills its tmux session)
#   parked -c|--clear [ok|failed|all]   remove logs for finished tasks (default: all = ok + failed)
set -euo pipefail

logdir="$HOME/.park"

mode=list
show_tail=0
clear_what=all
target=
case "${1:-}" in
    -l|--logs) show_tail=1 ;;
    -k|--cancel)
        mode=cancel
        target="${2:-}"
        if [ -z "$target" ]; then echo "usage: parked -k|--cancel <name>" >&2; exit 1; fi ;;
    -c|--clear)
        mode=clear
        clear_what="${2:-all}"
        case "$clear_what" in
            ok|failed|all) ;;
            *) echo "usage: parked -c|--clear [ok|failed|all]" >&2; exit 1 ;;
        esac ;;
    "")        ;;
    *) echo "usage: parked [-l|--logs] | parked -k|--cancel <name> | parked -c|--clear [ok|failed|all]" >&2; exit 1 ;;
esac

# colours only in list mode, and only when writing to a real terminal
if [ "$mode" = list ] && [ -t 1 ]; then
    c_run=$(tput setaf 4); c_ok=$(tput setaf 2); c_fail=$(tput setaf 1)
    c_die=$(tput setaf 3); c_dim=$(tput setaf 8); c_off=$(tput sgr0); c_bold=$(tput bold)
else
    c_run=; c_ok=; c_fail=; c_die=; c_dim=; c_off=; c_bold=
fi

# a task is "running" if its tmux session still exists
active=""
if command -v tmux >/dev/null 2>&1; then
    active=$(tmux ls -F '#{session_name}' 2>/dev/null || true)
fi
is_active() { printf '%s\n' "$active" | grep -qxF -- "$1"; }

# cancel mode — stop a running task by killing its tmux session; the log is kept
if [ "$mode" = cancel ]; then
    logfile="$logdir/${target}.log"
    if [ ! -f "$logfile" ]; then
        echo "no task named '$target'" >&2
        exit 1
    fi
    if is_active "$target"; then
        tmux kill-session -t "$target" 2>/dev/null || true
        printf '\n>>> cancelled: %s\n' "$(date)" >> "$logfile"
        echo "cancelled '$target'  (log kept: $logfile)"
        exit 0
    fi
    echo "task '$target' is not running (already finished)" >&2
    exit 1
fi

shopt -s nullglob
logs=("$logdir"/*.log)
if [ ${#logs[@]} -eq 0 ]; then
    echo "no park tasks found in $logdir"
    exit 0
fi

# clear mode — drop logs for finished tasks; running tasks and 'died' crashes are spared
if [ "$mode" = clear ]; then
    cleared=0
    names=""
    for log in "${logs[@]}"; do
        name=$(basename "$log" .log)
        if is_active "$name"; then continue; fi        # never clear a running task
        code=$(grep '^>>> exit code:' "$log" | tail -1 | sed 's/^>>> exit code: //' || true)
        if [ -z "$code" ]; then continue; fi           # 'died' — leave crashes visible
        if [ "$code" = "0" ]; then st=ok; else st=failed; fi
        if [ "$clear_what" = all ] || [ "$clear_what" = "$st" ]; then
            rm -f -- "$log"
            cleared=$((cleared + 1))
            names="$names $name"
        fi
    done
    if [ "$cleared" -eq 0 ]; then
        echo "no ${clear_what/all/finished} tasks to clear"
    else
        echo "cleared $cleared task(s):$names"
    fi
    exit 0
fi

printf "%s%-20s %-9s %s%s\n" "$c_bold" "SESSION" "STATUS" "DETAIL" "$c_off"

for log in "${logs[@]}"; do
    name=$(basename "$log" .log)

    if is_active "$name"; then                                  # session alive → still running
        started=$(grep -m1 '^>>> started:' "$log" | sed 's/^>>> started: //' || true)
        printf "%-20s %s%-9s%s %s%s%s\n" \
            "$name" "$c_run" "running" "$c_off" "$c_dim" "since ${started:-?}" "$c_off"
    else
        code=$(grep '^>>> exit code:' "$log" | tail -1 | sed 's/^>>> exit code: //' || true)
        fin=$(grep  '^>>> finished:'  "$log" | tail -1 | sed 's/^>>> finished: //'  || true)
        if [ -n "$code" ] && [ "$code" = "0" ]; then
            printf "%-20s %s%-9s%s %s%s%s\n" \
                "$name" "$c_ok" "ok" "$c_off" "$c_dim" "exit 0${fin:+, $fin}" "$c_off"
        elif [ -n "$code" ]; then
            printf "%-20s %s%-9s%s %s%s%s\n" \
                "$name" "$c_fail" "FAILED" "$c_off" "$c_dim" "exit $code${fin:+, $fin}" "$c_off"
        elif grep -q '^>>> cancelled:' "$log"; then              # stopped with parked -k
            can=$(grep '^>>> cancelled:' "$log" | tail -1 | sed 's/^>>> cancelled: //' || true)
            printf "%-20s %s%-9s%s %s%s%s\n" \
                "$name" "$c_die" "cancelled" "$c_off" "$c_dim" "${can:+cancelled $can}" "$c_off"
        else                                                     # no exit code → killed / OOM / reboot
            printf "%-20s %s%-9s%s %s%s%s\n" \
                "$name" "$c_die" "died" "$c_off" "$c_dim" "no exit code — killed / reboot?" "$c_off"
        fi
    fi

    if [ "$show_tail" -eq 1 ]; then
        last=$(tail -1 "$log" 2>/dev/null || true)
        printf "   %s└ %s%s\n" "$c_dim" "$last" "$c_off"
    fi
done

Drop both in ~/.local/bin, chmod +x, and go:

park dl-llama huggingface-cli download meta-llama/Llama-3.1-8B-Instruct --local-dir ./models/llama
parked               # running / ok / FAILED / cancelled / died
parked -l            # same, plus each task's last log line
parked -k dl-llama   # cancel a running task (kills its tmux session)
parked -c            # clear finished (ok + failed) logs; -c ok / -c failed to narrow
cat ~/.park/dl-llama.log

Quote any argument with a space; for pipes, &&, or redirection, put it in a script and run park job ./job.sh.