From f50a90869a43cf9bfd3131f85a5872b6ae7f78c2 Mon Sep 17 00:00:00 2001 From: Natalie Date: Wed, 20 May 2026 18:25:37 -0700 Subject: [PATCH] =?UTF-8?q?fix(@scripts/session-tools):=20=F0=9F=90=9B=20p?= =?UTF-8?q?revent=20tmux=20session=20crashes=20on=20claude=20exits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- bin/rclaude | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/bin/rclaude b/bin/rclaude index 1b780b1..997516a 100755 --- a/bin/rclaude +++ b/bin/rclaude @@ -1569,8 +1569,16 @@ build_inner() { _mcp_esc=$(printf %s "$RCLAUDE_MCP_CONFIG" | sed "s/'/'\\\\''/g") _mcp_flag="--mcp-config '${_mcp_esc}'" fi + # Strip rclaude's one-shot directive vars from the spawned session's + # environment. Without this they leak into `claude` and everything it + # later spawns (Bash tool shells, nested `rclaude` calls): a session + # started via `rclaude resume` would carry RCLAUDE_RESUME_ID forever, + # so every `rclaude` invoked from inside it re-resumes that stale + # session instead of starting fresh. Persistent prefs (RCLAUDE_TRIAGE*, + # RCLAUDE_HOSTS, RCLAUDE_PERMS, ...) are deliberately left intact. + _unset_directives='unset RCLAUDE_RESUME_ID RCLAUDE_RESUME RCLAUDE_RESUME_NAME RCLAUDE_DETACHED RCLAUDE_MCP_CONFIG RCLAUDE_MIGRATE_FROM RCLAUDE_MIGRATE_FROM_CWD RCLAUDE_MIGRATE_SYNC; ' printf '%s' \ - "${_back_env}cd ${1} && rc_t=\$(date +%s); claude ${_resume_flag} ${_name_flag} ${_mcp_flag} ${flag}; rc_ec=\$?; " \ + "${_unset_directives}${_back_env}cd ${1} && rc_t=\$(date +%s); claude ${_resume_flag} ${_name_flag} ${_mcp_flag} ${flag}; rc_ec=\$?; " \ "rc_e=\$(date +%s); rc_d=\$((rc_e - rc_t)); " \ "if [ \$rc_ec -ne 0 ] || [ \$rc_d -lt 2 ]; then " \ "printf '\\n[rclaude] claude exited in %ds with code %d\\n' \$rc_d \$rc_ec; " \ @@ -1657,6 +1665,21 @@ fi setup_host "$host" sync_tmux_conf "$host" inner=$(build_inner "$dir") +# Transport the inner command to the remote as opaque base64. `ssh host +# ""` concatenates its args and the remote login shell RE-PARSES the +# result — so an inline-quoted "${inner}" gets a second round of expansion +# on the remote: $(date +%s), $?, $((...)) and $rc_* all evaluate at +# tmux-launch time instead of pane-runtime. That silently broke +# build_inner's safety net — rc_t==rc_e (so rc_d is always 0), and the +# guard `if [ $rc_ec -ne 0 ] || [ $rc_d -lt 2 ]` collapsed to +# `if [ -ne 0 ] || [ -lt 2 ]` (a no-op test error → false), so `read rc_` +# never ran. A fast claude exit then closed the pane with nothing to hold +# it (and, if it was the last session, took the tmux server down too). +# base64 has no shell metacharacters, so it survives the remote re-parse +# intact; the remote decodes it once inside a command substitution and +# hands the result to tmux as a single argument — tmux's own `sh -c` then +# evaluates the $(...) at pane-runtime, as intended. +inner_b64=$(printf %s "$inner" | base64 | tr -d '\n') # RCLAUDE_DETACHED=1 → spawn the tmux session on the remote in the # background and print the session name. Symmetric with the local-host # detached branch above; used by supervisor processes (e.g. clare web) @@ -1672,7 +1695,7 @@ inner=$(build_inner "$dir") # detached spawn survives; the systemd-run variant did not. # Mosh is interactive-only — always go through ssh for detached spawn. if [ -n "${RCLAUDE_DETACHED:-}" ]; then - ssh $_SSH_LIVE_OPTS "$host" "tmux new-session -d -s '${session}' \"${inner}\"" + ssh $_SSH_LIVE_OPTS "$host" "tmux new-session -d -s '${session}' \"\$(echo '${inner_b64}' | base64 -d)\"" printf '%s\n' "$session" exit 0 fi @@ -1685,4 +1708,4 @@ fi if [ "$(pick_transport "$host")" = "mosh" ]; then exec mosh "$host" -- tmux new-session -A -s "${session}" "${inner}" fi -exec ssh -t $_SSH_LIVE_OPTS "$host" "tmux new-session -A -s '${session}' \"${inner}\"" +exec ssh -t $_SSH_LIVE_OPTS "$host" "tmux new-session -A -s '${session}' \"\$(echo '${inner_b64}' | base64 -d)\""