90 lines
3.1 KiB
Bash
Executable file
90 lines
3.1 KiB
Bash
Executable file
#!/bin/sh
|
|
# rclaude <host> [dir]
|
|
#
|
|
# Durable Claude Code session, local or remote. Two layers of resilience:
|
|
#
|
|
# 1. tmux on <host> survives terminal/transport drops (network, lid close,
|
|
# ssh kill, terminal crash) — works even when <host> is the local box
|
|
# because the local terminal can also die independently.
|
|
# 2. `claude --continue` resumes the per-directory session from disk after
|
|
# anything kills the host itself (reboot, crash, OOM).
|
|
#
|
|
# Re-running with the same <host> + <dir> always lands you back in the same
|
|
# conversation: tmux reattaches if alive, claude --continue picks up from
|
|
# ~/.claude/projects/<encoded-cwd>/ otherwise.
|
|
#
|
|
# <host> can be:
|
|
# - any ssh-reachable target (Host alias, user@hostname, IP)
|
|
# - "local", "localhost", or the local short/long hostname → no ssh,
|
|
# just a local tmux session (still detachable with Ctrl-b d)
|
|
#
|
|
# Permission mode: --dangerously-skip-permissions is on by default — these
|
|
# are sessions on hosts you own. Override with RCLAUDE_PERMS=default (or any
|
|
# other --permission-mode value) if you want prompts back.
|
|
#
|
|
# Usage:
|
|
# rclaude # local, current pwd (shorthand)
|
|
# rclaude . # same
|
|
# rclaude apricot # remote home dir on apricot
|
|
# rclaude apricot ~/Code/@projects/foo # remote, specific dir
|
|
# rclaude local . # local, current pwd (explicit)
|
|
# rclaude local ~/Code/@projects/foo # local, specific dir
|
|
# rclaude $(hostname) ~ # also local (hostname match)
|
|
|
|
set -eu
|
|
|
|
# Argument resolution:
|
|
# `rclaude` → local, $PWD
|
|
# `rclaude .` → local, $PWD
|
|
# `rclaude <host>` → host, default dir (~ remote, $PWD local)
|
|
# `rclaude <host> <dir>` → host, dir (with `.` resolving to $PWD)
|
|
if [ $# -eq 0 ] || [ "${1:-}" = "." ]; then
|
|
host=local
|
|
dir=$PWD
|
|
else
|
|
host=$1
|
|
dir=${2:-}
|
|
fi
|
|
|
|
is_local() {
|
|
case $1 in
|
|
local|localhost|127.0.0.1|::1) return 0 ;;
|
|
esac
|
|
[ "$1" = "$(hostname)" ] && return 0
|
|
[ "$1" = "$(hostname -s 2>/dev/null)" ] && return 0
|
|
return 1
|
|
}
|
|
|
|
# Defaults + `.` expansion now that we know whether we're local or remote.
|
|
if is_local "$host"; then
|
|
case ${dir:-.} in
|
|
.|"") dir=$PWD ;;
|
|
esac
|
|
else
|
|
if [ "$dir" = "." ]; then
|
|
echo "error: '.' as dir requires a local target; pass an explicit remote path" >&2
|
|
exit 2
|
|
fi
|
|
[ -z "$dir" ] && dir=\~
|
|
fi
|
|
|
|
slug=$(printf %s "$dir" | sed -e 's|^[~/]*||' -e 's|[^A-Za-z0-9]|-|g')
|
|
[ -z "$slug" ] && slug=home
|
|
session="claude-$(whoami)-${slug}"
|
|
|
|
perms=${RCLAUDE_PERMS:-bypass}
|
|
case $perms in
|
|
bypass) flag="--dangerously-skip-permissions" ;;
|
|
*) flag="--permission-mode $perms" ;;
|
|
esac
|
|
|
|
if is_local "$host"; then
|
|
# No ssh hop — local tmux. dir is already an absolute path here.
|
|
cd "$dir"
|
|
exec tmux new-session -A -s "$session" "exec claude --continue ${flag}"
|
|
fi
|
|
|
|
# Remote: tmux on the other side of an ssh -t. exec replaces the shell so
|
|
# the tmux pane dies cleanly when claude exits.
|
|
inner="cd ${dir} && exec claude --continue ${flag}"
|
|
exec ssh -t "$host" "tmux new-session -A -s '${session}' \"${inner}\""
|