feat(cli): path resume takes --remote and resumes a Claude session on an ssh host - #244
Draft
ecalifornica wants to merge 3 commits into
Draft
feat(cli): path resume takes --remote and resumes a Claude session on an ssh host#244ecalifornica wants to merge 3 commits into
ecalifornica wants to merge 3 commits into
Conversation
|
🔍 Preview deployed: https://3d9b5ca1.toolpath.pages.dev |
ecalifornica
force-pushed
the
ecalifornica/ssh-transport
branch
from
August 26, 2026 19:03
516c045 to
c7fd8c9
Compare
`ssh.rs` runs commands on a remote host through an in-process SSH client (`russh`). The destination names the host and, before `@`, the user; the port is 22. The agent authenticates first, then the default identity files under `~/.ssh`; an encrypted file is skipped with a hint. `~/.ssh/known_hosts` verifies the host key: a changed key is an error, and an unknown host is learned on first contact with a notice (accept-new). `Transport::run` opens one exec channel per call: one task feeds stdin while another collects stdout, stderr, and the exit status, all under the caller's wall-clock timeout; on expiry the connection drops and the error carries the stderr received so far. Connect, handshake, and authentication share a 30s bound. Keepalives are on every session. `RemoteCommand` is the only way to build the string the exec request carries. `new` is an argv. `script` is a constant `sh` script that reads its values as positional parameters; the text is `&'static str`, so a value cannot be interpolated into shell text. Every word is quoted through shlex. `Destination` restricts the destination to `[A-Za-z0-9@._-]` with an alphanumeric first character. `parse_facts` reads `TP_<NAME>=<value>` lines and rejects any other shape, so a login banner cannot become a path component. `FakeSsh` scripts replies and records rendered commands for tests. Quoting and scripts are tested against a real `sh`; the live test (`--ignored`, `PATH_TEST_SSH_DEST`) exercises run, stdin, stderr, the exit status, and the timeout against a real host. The module has no caller yet, so `lib.rs` allows dead code on it. No version bump: the dependencies become user-visible with the command that consumes the module.
ecalifornica
force-pushed
the
ecalifornica/ssh-transport
branch
from
August 26, 2026 19:57
c7fd8c9 to
b17f788
Compare
… ssh host `path resume <input> --remote <dest> [-C <dir>] [--dry-run]` plans a resume on an ssh host without touching it. Claude only. Two read-only ssh calls through the transport module gather the facts: - Call 1: the remote home, the claude path (PATH, then ~/.local/bin, ~/.claude/local, ~/.npm-global/bin), tmux presence. The remote project directory is `-C` verbatim, else the local cwd with the local home swapped for the remote home. - Call 2: the directory is physical (`pwd -P` returns it), the tmux session for the derived ID is live, the target session file exists. The plan prints the facts and what a run does: a live tmux session is attached to, a present session file is launched, an absent file is shipped. `--dry-run` exits after the plan. Without it the command errors: ship, launch, and attach are not implemented yet. The session ID comes from `session_id_from_document_hash`, shared with `p export claude --derive-session-id`. `resolve_input` returns a `ResolvedInput` carrying the document text next to the parsed graph, so the ID hashes the input bytes rather than a type round-trip. The tmux session name is `path-<first 8 of the ID>`. The probe scripts print `TP_<NAME>=<value>` lines and the parser rejects any other output, so a login banner cannot become a path component. A captured path must be a non-empty single line starting with `/`. A non-physical directory errors with the physical path as a `-C` hint. Tests drive the command through the scripted fake transport; nothing is public for tests. path-cli 0.21.0; toolpath-cli 0.21.0 (lockstep bump of the shim). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mws8xxV7vuJs6rQSKG69ye
After the plan, the command does what the remote state asks. An absent session file is shipped: the conversation is projected in memory, renamed to the derived session ID, rooted at the remote project directory, and sent over ssh stdin (0600 via umask 077). The remote writes a temporary name, checks the byte count against the local JSONL, and renames it into place, so a present file is a complete file. The ship's timeout grows with the byte count. A fresh `claude -r <id>` starts in a detached tmux session named `path-<first 8 of the ID>` unless one is live. The command then attaches this terminal to `tmux attach-session -d -t =<name>` over a PTY channel and exits with tmux's status. The remote wins once it exists: a live tmux session is attached to as is, a present session file is launched as is, and nothing overwrites remote turns. A failed ship or launch stops the run with the remote stderr. Without `--dry-run` the command requires a TTY on stdin, because the attach is interactive. The rewrite helpers move out of `run_claude` into `rename_session` and `reroot_cwd`, shared with `p export claude`; the export behavior is unchanged. The launch script embeds the claude path in escaped double quotes for the shell tmux starts; a probed path carrying a quote, `$`, backtick, or backslash errors before any write. `Transport::attach` opens a PTY channel sized to this terminal, puts the terminal in raw mode for the duration, forwards keystrokes to the channel, channel output to stdout, and terminal resizes as window changes, and returns the command's exit status. The terminal reader is a blocking task that ends only on the next keypress, so the runtime shuts down in the background on drop instead of waiting for it. `FakeSsh` records attach calls. `scripts/resume-remote.sh` hands off to the command after build, session resolution, optional VM creation, `--setup` seeding, and the working-tree sync. Its probe shrinks to one call for the remote home and the session file's existence, which gates the sync. The plan, ship, launch, and attach blocks are gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mws8xxV7vuJs6rQSKG69ye
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
path resume <input> --remote <dest> [-C <remote-dir>] [--dry-run]resumes a Claude session on an ssh host under tmux, andscripts/resume-remote.shhands off to it after its bootstrap steps. The local host does all toolpath work. The remote runsclaudeandtmuxonly.russh); nosshbinary is involved. The destination names the host and, before@, the user; the port is 22. The agent authenticates first, then the default identity files under~/.ssh.~/.ssh/known_hostsverifies the host key: a changed key is an error, and an unknown host is learned on first contact with a notice (accept-new). Every remote call has a wall-clock timeout, and a timeout error carries the stderr received so far.RemoteCommandis the only way to build the string the exec request carries: an argv, or a constantshscript (&'static str) that reads its values as positional parameters, every word through shlex quoting. Two read-only probes (constant scripts) report the remote home, the claude path, tmux, the physical project directory, whether the tmux session for the derived ID is live, and whether the session file exists. Any other output, such as a login banner, is an error. The plan prints.--dry-runstops there.claude -r <id>and attached to. An absent file is shipped, launched, and attached to. Nothing overwrites remote turns. To reset, delete the file on the remote and re-run.p export claude --derive-session-idproduces), roots it at the remote project directory, and sends the JSONL over the channel's stdin. The remote writes a temporary name, checks the byte count, and renames it into place, so a present file is a complete file. The ship's timeout grows with the byte count.--dry-run.scripts/resume-remote.shkeeps build, session resolution,--create,--setupseeding, and the working-tree sync, thenexecs the command. Its one probe reads the remote home and whether the session file exists, which gates the sync.sh. A live test (--ignored,PATH_TEST_SSH_DEST=user@host) exercises the transport against a real host.russh0.63 (its crypto backend,aws-lc-rs, is already in the build through rustls),shlex, andcrossterm(already in the build through the embedded picker).path-cli 0.21.0; toolpath-cli 0.21.0 (lockstep bump of the shim).
Stacked on #243.