2026-04-25 22:13:25 -07:00
|
|
|
#!/bin/sh
|
|
|
|
|
# remote-run <host> <cmd...>
|
|
|
|
|
#
|
|
|
|
|
# Run a command on <host> inside a detached tmux session, stream output back,
|
|
|
|
|
# propagate exit code. If the local ssh dies mid-run, the tmux session keeps
|
|
|
|
|
# going on the remote — recover with:
|
|
|
|
|
# ssh <host> tmux ls
|
|
|
|
|
# ssh <host> tmux attach -t <session>
|
|
|
|
|
#
|
|
|
|
|
# <host> is whatever ssh accepts: a Host alias from ~/.ssh/config, a
|
|
|
|
|
# user@hostname, an IP, etc.
|
|
|
|
|
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
if [ $# -lt 2 ]; then
|
|
|
|
|
echo "usage: $0 <host> <cmd...>" >&2
|
|
|
|
|
exit 2
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
host=$1
|
|
|
|
|
shift
|
|
|
|
|
|
|
|
|
|
session="claude-$(whoami)-$$-$(date +%s)"
|
|
|
|
|
|
2026-06-02 16:30:06 -07:00
|
|
|
# Base64-encode the user command so it embeds safely no matter what quotes,
|
|
|
|
|
# pipes, semicolons, or newlines it contains. The remote side decodes it to a
|
|
|
|
|
# script file and runs THAT — user content never lands in a shell-quoted
|
|
|
|
|
# context where a stray " could break parsing (the old sed-escape only handled
|
|
|
|
|
# ' and broke on any embedded ", splitting the command at the next | or ;).
|
2026-04-25 22:13:25 -07:00
|
|
|
user_cmd=$*
|
2026-06-02 16:30:06 -07:00
|
|
|
cmd_b64=$(printf %s "$user_cmd" | base64 | tr -d '\n')
|
2026-04-25 22:13:25 -07:00
|
|
|
|
|
|
|
|
# Remote bootstrap — runs the user command in its own bash subshell so that
|
|
|
|
|
# any `exit` or `set -e` inside it does NOT short-circuit our exit-capture.
|
|
|
|
|
remote_cmd=$(cat <<REMOTE
|
|
|
|
|
session='${session}'
|
|
|
|
|
log="/tmp/\${session}.log"
|
|
|
|
|
exitf="/tmp/\${session}.exit"
|
2026-06-02 16:30:06 -07:00
|
|
|
cmdf="/tmp/\${session}.cmd"
|
|
|
|
|
printf %s '${cmd_b64}' | base64 -d > "\$cmdf"
|
2026-04-25 22:13:25 -07:00
|
|
|
: > "\$log"
|
2026-06-02 16:30:06 -07:00
|
|
|
tmux new-session -d -s "\$session" "bash \$cmdf > \$log 2>&1; echo \\\$? > \$exitf; tmux wait-for -S done-\$session" 2>/tmp/\${session}.tmuxerr
|
2026-04-25 22:13:25 -07:00
|
|
|
if [ \$? -ne 0 ]; then
|
|
|
|
|
echo "tmux failed to start session:" >&2
|
|
|
|
|
cat /tmp/\${session}.tmuxerr >&2
|
|
|
|
|
rm -f /tmp/\${session}.tmuxerr
|
|
|
|
|
exit 127
|
|
|
|
|
fi
|
|
|
|
|
rm -f /tmp/\${session}.tmuxerr
|
|
|
|
|
( tail -F "\$log" 2>/dev/null ) &
|
|
|
|
|
tail_pid=\$!
|
|
|
|
|
tmux wait-for done-\$session 2>/dev/null
|
|
|
|
|
sleep 0.2
|
|
|
|
|
kill \$tail_pid 2>/dev/null || true
|
|
|
|
|
wait \$tail_pid 2>/dev/null || true
|
|
|
|
|
code=\$(cat "\$exitf" 2>/dev/null || echo 1)
|
|
|
|
|
tmux kill-session -t "\$session" 2>/dev/null || true
|
2026-06-02 16:30:06 -07:00
|
|
|
rm -f "\$log" "\$exitf" "\$cmdf"
|
2026-04-25 22:13:25 -07:00
|
|
|
exit \$code
|
|
|
|
|
REMOTE
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ssh "$host" "$remote_cmd"
|