#!/bin/sh # crc — start a `claude rc` (Remote Control) server in a target dir on a target # host, inside a durable tmux session so transport drops don't kill it. # # `claude rc` (= `claude remote-control`) is a persistent server: you start it # in a directory, then drive sessions there from claude.ai/code or the Claude # mobile app. It must keep running after you disconnect — so crc parks it in a # named tmux session on the host (same durability trick as tssh/remote-run). # # The tmux session name is derived from the directory, so re-running crc for the # same host+dir RE-ATTACHES the existing server instead of starting a second # one. Detach with Ctrl-b d; the server keeps running. Reattach with the same # crc command. # # Usage: # crc # apricot.lan, mirror of $PWD # crc # , mirror of $PWD # crc # , explicit dir (abs path or ~/...) # crc -- # extra args passed to `claude rc` # crc -n ... # open a NEW iTerm window instead of current tab # crc -h | --help # # Options: # -s, --session override the derived tmux session name (so a boot # unit and an interactive attach share ONE session) # --ensure start the session detached if not already running, # then exit (no attach) — for systemd / scripting # --respawn run `claude rc` in a restart loop inside the session, # so a crash is recovered without systemd intervention # # host may be any ssh target (alias, user@host, IP), or local/./localhost to run # on this machine. When is omitted, $PWD is mirrored to the same path # under the remote's $HOME (like rclaude); paths outside $HOME fall back to ~. # # Env: # CRC_HOST default host when none given (default: apricot.lan) set -eu host=${CRC_HOST:-apricot.lan} new_window=0 dry_run=0 session_override='' ensure=0 respawn=0 usage() { sed -n '2,37p' "$0" | sed 's/^# \{0,1\}//'; } # --- arg parse ------------------------------------------------------------- positional='' # collected host/dir (max 2), space-free tokens unsafe so # we track count explicitly have_host=0 dir_set=0 dir='' rc_args='' # everything after `--`, verbatim while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; -n|--new-window) new_window=1; shift ;; --dry-run) dry_run=1; shift ;; -s|--session) [ $# -ge 2 ] || { echo "crc: $1 needs a value" >&2; exit 2; } session_override=$2; shift 2 ;; --ensure) ensure=1; shift ;; --respawn) respawn=1; shift ;; --) shift; rc_args=$*; break ;; -*) echo "crc: unknown option: $1" >&2; exit 2 ;; *) if [ $have_host -eq 0 ]; then host=$1; have_host=1 elif [ $dir_set -eq 0 ]; then dir=$1; dir_set=1 else echo "crc: too many arguments: $1" >&2; exit 2 fi shift ;; esac done # --- new-window mode: relaunch self (sans -n) in a fresh iTerm window ------- if [ "$new_window" -eq 1 ]; then cmd="crc" [ $have_host -eq 1 ] && cmd="$cmd $(printf %q "$host")" [ $dir_set -eq 1 ] && cmd="$cmd $(printf %q "$dir")" [ -n "$session_override" ] && cmd="$cmd --session $(printf %q "$session_override")" [ -n "$rc_args" ] && cmd="$cmd -- $rc_args" escaped=$(printf %s "$cmd" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') osascript </dev/null; then echo "crc: directory not found on host: \$DIR" >&2 exit 1 fi SH=\${SHELL:-/bin/sh} if [ "\$RESPAWN" = 1 ]; then CMD="\$SH -lc 'while true; do claude rc \$RC_ARGS; sleep 3; done'" else CMD="\$SH -lc 'exec claude rc \$RC_ARGS'" fi if [ "\$ENSURE" = 1 ]; then if tmux has-session -t "\$SESS" 2>/dev/null; then echo "crc: \$SESS already running" >&2 else tmux new-session -d -c "\$DIR" -s "\$SESS" "\$CMD" && echo "crc: started detached \$SESS" >&2 fi else exec tmux new-session -A -c "\$DIR" -s "\$SESS" "\$CMD" fi BOOT ) boot_b64=$(printf %s "$boot" | base64 | tr -d '\n') run_remote="printf %s '${boot_b64}' | base64 -d | sh" echo "crc: ${session} → claude rc in ${abs:-\$HOME/${rel}} on ${host}" >&2 [ "$ensure" -eq 0 ] && echo "crc: detach with Ctrl-b d (server keeps running); reattach by re-running this command." >&2 if [ "$dry_run" -eq 1 ]; then echo "--- remote script ($host) ---" >&2 printf %s "$boot_b64" | base64 -d echo exit 0 fi case "$host" in local|localhost|.) command -v tmux >/dev/null 2>&1 || { echo "crc: tmux not found locally" >&2; exit 127; } eval "$run_remote" ;; *) if [ "$ensure" -eq 1 ]; then exec ssh "$host" "$run_remote" else exec ssh -t "$host" "$run_remote"; fi ;; esac