Skip to content

Instantly share code, notes, and snippets.

@yasn77
Created September 8, 2025 13:23
Show Gist options
  • Select an option

  • Save yasn77/472571ca00fab6e757f3ece62762a2c1 to your computer and use it in GitHub Desktop.

Select an option

Save yasn77/472571ca00fab6e757f3ece62762a2c1 to your computer and use it in GitHub Desktop.
Vibe Code Bash script to SSH to multiple hosts with tmux
#!/usr/bin/env bash
# ==============================================================
# tmux-sync-ssh.sh
#
# 1. Finds the real tmux binary (ignores aliases, functions, etc.).
# 2. Verifies tmux is ≥ 2.4.
# 3. Creates a tmux session with one pane per host, splits panes,
# turns on synchronized input, and attaches to the session.
#
# 4. Accepts SSH options:
# -i <identity_file> (passed to ssh)
# -o <ssh_option> (can be given multiple times)
#
# Usage:
# tmux-sync-ssh.sh [-s session] [-u user] [-i idfile] [-o ssh_opt]
# [-v] [-k] [-h hostfile] host1 host2 …
# --------------------------------------------------------------
# -s session Name of the tmux session (default: cluster)
# -u user SSH user (default: current user)
# -i idfile SSH identity file (passed as -i <file> to ssh)
# -o ssh_opt Pass arbitrary ssh option (e.g. StrictHostKeyChecking=no).
# Can be supplied more than once.
# -v Split panes vertically (default horizontal)
# -k Keep an existing session; just attach to it
# -h file One host per line in *file* (overrides the host list on the command line)
# -? Show this help
# ==============================================================
set -euo pipefail
# --------------------------------------------------------------
# 1. Locate the real tmux binary (ignore aliases, functions)
# --------------------------------------------------------------
find_tmux() {
local p
# Bash-specific: type -p returns only the pathname
p=$(type -p tmux 2>/dev/null)
[[ -n "$p" && -x "$p" ]] && echo "$p" && return
# Generic: command -v (POSIX) – might return alias definition
p=$(command -v tmux 2>/dev/null)
[[ -n "$p" && -x "$p" ]] && echo "$p" && return
# Fallback: which -a, strip alias lines
p=$(which -a tmux 2>/dev/null | grep -v '^alias' | head -n1)
[[ -n "$p" && -x "$p" ]] && echo "$p" && return
# Nothing found
echo ""
}
TMUX_BIN=$(find_tmux)
if [[ -z "$TMUX_BIN" ]]; then
echo "Error: tmux not found. Please install tmux and try again." >&2
exit 1
fi
# --------------------------------------------------------------
# 2. Verify tmux version – require ≥ 2.4
# --------------------------------------------------------------
TMUX_VER=$("$TMUX_BIN" -V 2>/dev/null | awk '{print $2}')
if [[ $TMUX_VER =~ ^([0-9]+)\.([0-9]+) ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
if ((MAJOR < 2 || (MAJOR == 2 && MINOR < 4))); then
echo "Error: tmux version 2.4 or newer is required. Detected $TMUX_VER." >&2
exit 1
fi
else
echo "Warning: could not parse tmux version from '$TMUX_VER'. Continuing anyway." >&2
fi
# --------------------------------------------------------------
# 3. Default options
# --------------------------------------------------------------
SESSION_NAME="cluster"
SSH_USER="${USER}"
SSH_IDENTITY=""
SSH_OPTIONS=()
VERTICAL_SPLIT=false
KEEP_EXISTING=false
HOSTFILE=""
HOSTS=()
# --------------------------------------------------------------
# 4. Helper functions
# --------------------------------------------------------------
usage() {
cat <<'EOF'
Usage: tmux-sync-ssh.sh [-s session] [-u user] [-i idfile] [-o ssh_opt]
[-v] [-k] [-h hostfile] host1 host2 …
-s session Name of the tmux session (default: cluster)
-u user SSH user (default: current user)
-i idfile SSH identity file (passed to ssh as -i <file>)
-o ssh_opt Pass arbitrary ssh option (e.g. StrictHostKeyChecking=no).
Can be supplied more than once.
-v Split panes vertically (default horizontal)
-k Keep an existing session; just attach to it
-h file Use this file as the host list (one host per line)
-? Show this help
EOF
exit 1
}
error() {
printf '\e[31mError:\e[0m %s\n' "$1" >&2
exit 1
}
# --------------------------------------------------------------
# 5. Parse command‑line options
# --------------------------------------------------------------
while getopts ":s:u:vkhi:o:h?" opt; do
case $opt in
s) SESSION_NAME="$OPTARG" ;;
u) SSH_USER="$OPTARG" ;;
i) SSH_IDENTITY="$OPTARG" ;;
o) SSH_OPTIONS+=("$OPTARG") ;;
v) VERTICAL_SPLIT=true ;;
k) KEEP_EXISTING=true ;;
h) HOSTFILE="$OPTARG" ;;
?) usage ;;
esac
done
shift $((OPTIND - 1))
# Build SSH command components
SSH_CMD="ssh"
if [[ -n "$SSH_IDENTITY" ]]; then
SSH_CMD+=" -i \"$SSH_IDENTITY\""
fi
# Handle empty array safely with set -u
if [[ ${#SSH_OPTIONS[@]} -gt 0 ]]; then
for opt in "${SSH_OPTIONS[@]}"; do
SSH_CMD+=" -o \"$opt\""
done
fi
# --------------------------------------------------------------
# 6. Load host list
# --------------------------------------------------------------
if [[ -n "$HOSTFILE" ]]; then
[[ ! -r "$HOSTFILE" ]] && error "Cannot read hostfile: $HOSTFILE"
mapfile -t HOSTS <"$HOSTFILE"
else
[[ $# -eq 0 ]] && usage
HOSTS=("$@")
fi
# Keep only "real" host names: strip comments/blank lines
HOSTS=($(printf '%s\n' "${HOSTS[@]}" | grep -E '^[[:alnum:].@_-]+$'))
((${#HOSTS[@]} == 0)) && error "No valid hosts supplied"
# --------------------------------------------------------------
# 7. tmux logic
# --------------------------------------------------------------
# If the session already exists, either keep it or kill it
if "$TMUX_BIN" has-session -t "$SESSION_NAME" 2>/dev/null; then
if $KEEP_EXISTING; then
echo "Session \"$SESSION_NAME\" already exists – attaching."
"$TMUX_BIN" attach-session -t "$SESSION_NAME" || exit
exit 0
else
echo "Killing existing session \"$SESSION_NAME\"."
"$TMUX_BIN" kill-session -t "$SESSION_NAME"
fi
fi
# Create the session with a shell first (not SSH directly)
FIRST_HOST="${HOSTS[0]}"
echo "Creating session '$SESSION_NAME' with window name '$FIRST_HOST'..."
"$TMUX_BIN" new-session -d -s "$SESSION_NAME" -n "$FIRST_HOST"
# Verify the session and window were created with better debugging
sleep 0.5
if ! "$TMUX_BIN" has-session -t "$SESSION_NAME" 2>/dev/null; then
error "Failed to create tmux session '$SESSION_NAME'"
fi
# Debug: Show what windows exist
echo "Checking for windows in session '$SESSION_NAME'..."
WINDOWS=$("$TMUX_BIN" list-windows -t "$SESSION_NAME" 2>/dev/null || echo "")
echo "Windows found: $WINDOWS"
# More flexible window check - look for any window, not just window 0
if [[ -z "$WINDOWS" ]]; then
error "No windows found in session '$SESSION_NAME'"
fi
# Get the actual window identifier (might not be 0)
WINDOW_ID=$("$TMUX_BIN" list-windows -t "$SESSION_NAME" -F "#{window_index}" | head -n1)
echo "Using window ID: $WINDOW_ID"
# Send SSH command to the first pane
echo "Connecting to $SSH_USER@$FIRST_HOST..."
"$TMUX_BIN" send-keys -t "$SESSION_NAME:$WINDOW_ID" "$SSH_CMD $SSH_USER@$FIRST_HOST" Enter
# Create remaining panes and send SSH commands
for i in "${!HOSTS[@]}"; do
((i == 0)) && continue
HOST="${HOSTS[$i]}"
echo "Creating pane for $SSH_USER@$HOST..."
# Split the window to create a new pane
# Note: tmux terminology is confusing:
# -h = horizontal split (creates panes side-by-side)
# -v = vertical split (creates panes top-bottom)
if $VERTICAL_SPLIT; then
# User wants vertical splits (top-bottom arrangement)
"$TMUX_BIN" split-window -t "$SESSION_NAME:$WINDOW_ID" -v
else
# Default: horizontal splits (side-by-side arrangement)
"$TMUX_BIN" split-window -t "$SESSION_NAME:$WINDOW_ID" -h
fi
# Send SSH command to the newly created pane
"$TMUX_BIN" send-keys -t "$SESSION_NAME:$WINDOW_ID" "$SSH_CMD $SSH_USER@$HOST" Enter
done
# Enable synchronized input
"$TMUX_BIN" setw -t "$SESSION_NAME:$WINDOW_ID" synchronize-panes on
# Choose a tidy layout (works on tmux ≥ 2.4)
if $VERTICAL_SPLIT; then
"$TMUX_BIN" select-layout -t "$SESSION_NAME:$WINDOW_ID" even-vertical
else
"$TMUX_BIN" select-layout -t "$SESSION_NAME:$WINDOW_ID" even-horizontal
fi
echo "Session \"$SESSION_NAME\" created – attaching now."
"$TMUX_BIN" attach-session -t "$SESSION_NAME"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment