diff --git a/README.md b/README.md index 77488b6..e53aed1 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,15 @@ a detached tmux session on the remote so the work survives the SSH drop. creates) a per-user tmux session on `` named `claude-$(whoami)`. Detach with `Ctrl-b d`; transport drops don't kill the shell. -- **`bin/rclaude [dir]`** — Remote, durable Claude Code session. - Stacks two resilience layers: tmux survives transport drops, and - `claude --continue` resumes the per-directory session from +- **`bin/rclaude [dir]`** — Durable Claude Code session, local or + remote. Stacks two resilience layers: tmux survives terminal/transport + drops, and `claude --continue` resumes the per-directory session from `~/.claude/projects/` after anything kills the host itself. Re-running with the same `` + `` always lands back in the same - conversation. Defaults to `--dangerously-skip-permissions`; override with - `RCLAUDE_PERMS=default` (or any other `--permission-mode` value). + conversation. `` can be any ssh target, or `local`/`localhost`/the + local hostname to skip ssh and use a local tmux session (still detachable + for terminal/network resilience). Defaults to + `--dangerously-skip-permissions`; override with `RCLAUDE_PERMS=default`. ## Install diff --git a/bin/rclaude b/bin/rclaude index 50e04f9..edd87a9 100755 --- a/bin/rclaude +++ b/bin/rclaude @@ -1,38 +1,43 @@ #!/bin/sh # rclaude [dir] # -# Remote, durable Claude Code session. Two layers of resilience stacked: +# Durable Claude Code session, local or remote. Two layers of resilience: # -# 1. tmux on survives transport drops (network, lid close, ssh kill) +# 1. tmux on survives terminal/transport drops (network, lid close, +# ssh kill, terminal crash) — works even when 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) +# anything kills the host itself (reboot, crash, OOM). # -# Re-running with the same + always lands you back where you -# were: tmux reattaches if alive, claude --continue picks up the conversation -# from ~/.claude/projects// otherwise. +# Re-running with the same + always lands you back in the same +# conversation: tmux reattaches if alive, claude --continue picks up from +# ~/.claude/projects// otherwise. +# +# 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 remote sessions on hosts you own. Override with RCLAUDE_PERMS=default -# (or any other --permission-mode value) if you want prompts back. +# are sessions on hosts you own. Override with RCLAUDE_PERMS=default (or any +# other --permission-mode value) if you want prompts back. # # Usage: -# rclaude apricot # remote home dir -# rclaude apricot ~/Code/@projects/foo # specific dir -# rclaude apricot @proj/lilith # alias from project-paths.md -# # works if remote shell expands it +# rclaude apricot # remote home dir on apricot +# rclaude apricot ~/Code/@projects/foo # remote, specific dir +# rclaude local ~/Code/@projects/foo # local tmux-wrapped session +# rclaude $(hostname) ~ # same — detected as local set -eu if [ $# -lt 1 ]; then - echo "usage: $0 [dir] (dir defaults to remote \$HOME)" >&2 + echo "usage: $0 [dir] (dir defaults to remote/local \$HOME)" >&2 exit 2 fi host=$1 dir=${2:-\~} -# tmux session name unique per (user, dir) so multiple Claude sessions on the -# same host don't collide. Slug is the path with non-alnum collapsed. slug=$(printf %s "$dir" | sed -e 's|^[~/]*||' -e 's|[^A-Za-z0-9]|-|g') [ -z "$slug" ] && slug=home session="claude-$(whoami)-${slug}" @@ -43,8 +48,22 @@ case $perms in *) flag="--permission-mode $perms" ;; esac -# cd then exec claude. exec replaces the shell so the tmux pane dies cleanly -# when claude exits (instead of leaving a stray shell behind). -inner="cd ${dir} && exec claude --continue ${flag}" +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 +} +if is_local "$host"; then + # No ssh hop — just local tmux. eval expands ~ and env vars in dir. + eval "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}\""