#!/bin/sh # crc — launch a non-persistent, HEADLESS `claude rc` (Remote Control) server in # a target dir on a target host, and print its claude.ai/code URL. # # Headless: claude rc runs detached with output logged (no tmux, no tty) and # `--spawn` preset, so it never blocks on the interactive spawn-mode prompt. It # survives terminal/ssh drops but NOT a host reboot (non-persistent). For servers # that must survive reboot, register them with the `claude-rc` manager instead. # # Drive the session from claude.ai/code or the Claude mobile app via the printed # URL. Re-running just reports the existing server (never duplicates it). # # Usage: # crc # apricot.lan, mirror of $PWD # crc # , mirror of $PWD # crc # , explicit dir (abs path or ~/...) # crc --stop # stop that server # crc --status # status + URL only (don't start) # crc --log # tail its log # crc ... -- # extra args passed to `claude rc` # # host: any ssh target (alias, user@host, IP), or local/./localhost. When # is omitted, $PWD is mirrored to the same path under the remote's $HOME. # # Options: # --spawn worktree | same-dir | session (default: worktree) # --perm permission mode for spawned sessions: # bypassPermissions | default | acceptEdits | plan | dontAsk | auto # (default: bypassPermissions) # # Env: CRC_HOST default host when none given (default: apricot.lan) set -eu host=${CRC_HOST:-apricot.lan} action=launch # launch | stop | status | log spawn=worktree perm=bypassPermissions have_host=0 dir_set=0 dir='' rc_args='' usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; } # Account-wide environment list (the claude.ai/code picker view, every host). # crc | crc status | crc envs → list # crc envs rm | archive | --json case "${1:-}" in ''|status) exec claude-rc-envs ;; envs|ls) shift; exec claude-rc-envs "$@" ;; esac while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; --stop) action=stop; shift ;; --status) action=status; shift ;; --log) action=log; shift ;; --spawn) [ $# -ge 2 ] || { echo "crc: --spawn needs a value" >&2; exit 2; }; spawn=$2; shift 2 ;; --perm|--permission-mode) [ $# -ge 2 ] || { echo "crc: $1 needs a value" >&2; exit 2; }; perm=$2; shift 2 ;; --) 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 # --- resolve dir + a stable session name (slug) ---------------------------- rel='' abs='' slug_src='' if [ $dir_set -eq 1 ]; then abs=$dir # Normalize so ~/x, $HOME/x and /abs/$HOME/x map to the same slug (the shell # may have expanded ~ to $HOME before crc saw it). case "$dir" in "$HOME"/*) slug_src=${dir#"$HOME"/} ;; '~/'*) slug_src=${dir#\~/} ;; *) slug_src=$dir ;; esac else case "$PWD" in "$HOME") rel='' ; slug_src='home' ;; "$HOME"/*) rel=${PWD#"$HOME"/} ; slug_src=$rel ;; *) rel='' ; slug_src='home' ;; esac fi slug=$(printf %s "$slug_src" | tr '[:upper:]' '[:lower:]' | sed -e 's#[^a-z0-9]\{1,\}#-#g' -e 's#^-##' -e 's#-$##') [ -n "$slug" ] || slug='home' name="crc-${slug}" # --- remote bootstrap (base64'd to survive every quoting layer) ------------ # Runs on the target (local or via ssh). Manages a detached, logged claude rc # keyed by $name under the state dir; idempotent (won't double-launch). bootf=$(mktemp "${TMPDIR:-/tmp}/crc.XXXXXX") trap 'rm -f "$bootf"' EXIT INT TERM cat > "$bootf" </dev/null)" 2>/dev/null; } envid() { grep -aoE 'env_[A-Za-z0-9]+' "\$LOG" 2>/dev/null | tail -1; } report() { if alive; then e=\$(envid) echo "\$NAME: running (pid \$(cat "\$PIDF")) in \$DIR" if [ -n "\$e" ]; then echo " https://claude.ai/code?environment=\$e" else echo " (URL not in log yet — retry: crc ... --status)"; fi else echo "\$NAME: not running" fi } case "\$ACTION" in stop) if alive; then kill "\$(cat "\$PIDF")" 2>/dev/null && echo "stopped \$NAME"; else echo "\$NAME not running"; fi rm -f "\$PIDF" ;; status) report ;; log) [ -f "\$LOG" ] && tail -n 40 "\$LOG" || echo "no log: \$LOG" ;; launch) if alive; then echo "crc: \$NAME already running — reporting existing server" else cd "\$DIR" 2>/dev/null || { echo "crc: directory not found: \$DIR" >&2; exit 1; } : > "\$LOG" nohup claude rc --name "\$NAME" --spawn "\$SPAWN" --permission-mode "\$PERM" \$RC_ARGS >"\$LOG" 2>&1 & echo \$! > "\$PIDF" i=0; while [ \$i -lt 20 ] && [ -z "\$(envid)" ] && alive; do sleep 1; i=\$((i+1)); done fi report ;; esac BOOT boot_b64=$(base64 < "$bootf" | tr -d '\n') run_remote="printf %s '${boot_b64}' | base64 -d | sh" case "$host" in local|localhost|.) eval "$run_remote" ;; *) ssh "$host" "$run_remote" ;; esac