Skip to content

feat: reach <target> <command>, with a session per target - #17

Merged
bojieli merged 1 commit into
mainfrom
one-shot-target-form
Aug 22, 2026
Merged

feat: reach <target> <command>, with a session per target#17
bojieli merged 1 commit into
mainfrom
one-shot-target-form

Conversation

@bojieli

@bojieli bojieli commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Stacked on #16. Review that one first; this PR's base is its branch, so the diff shown here is only this change.

What this changes

Binding a session and using it took two commands, and the first defaulted the session name to default. A second reach up with no --name therefore replaced the first session in place — and because the harness launchers resolve their target through that same name, an agent already working through it began running its commands on a different machine. reach could always hold several targets at once; they had to be named by hand, and nothing in the interface said so. The reported symptom was "I can't open multiple SSH targets".

Now the target names the session:

reach build-box claude                        # ssh_config alias; works where a login lands
reach ssh://build-box/srv/app claude          # or a directory you name
reach build-box exec -- go test ./...         # any command, not just harnesses
  • Dispatch. A first argument that is not a command is read as a target (knownCommands in main.go), and the rest is dispatched as a normal command line. A drift test parses dispatch out of the source and fails if the map and the switch disagree — a command missing from the map would be looked up as a hostname, and one listed but unhandled would exit 0 having done nothing.
  • Flag boundary. Session flags sit between the target and the command. flag.Parse stops at the first non-flag argument, which draws exactly the right line: everything from the command onwards belongs to the command.
  • Names. Derived from the target (build-box, build-box-app). A name already held by a different target gets a numbered variant rather than being overwritten, and reach up now says so on stderr when it repoints a name another session was using.
  • Reuse. A second command against a target reuses that session instead of re-probing. The connection is still re-established while the operator is present, because every connection afterwards runs in batch mode and cannot prompt for a passphrase or a hardware token — except for commands that never touch the target (log, status, env), where opening one would ask for a token touch to read a local file.
  • Pathless targets. reach build-box claude works where a login on that machine lands, which Probe asks the machine for and records, rather than reach guessing at a home directory that may not exist.
  • What counts as a host. An ssh_config Host entry (including through Include), the hosts file, an address, or a dotted name. DNS is deliberately not consulted: resolvers that answer for every name are common enough that a lookup would turn reach stauts into a connection attempt on exactly the networks where that is hardest to see. A word that is neither a command nor a host is reported as both readings.

Also fixes --mode accepting a misspelling and silently behaving as exec, since only "mirror" is ever compared against.

Checklist

  • make check and make lint pass
  • make integration passes, if this touches transports or file operations — not run; no transport or tier behaviour changed
  • Comments explain why where the code looks odd

Verified against a local:// target end to end (session bound, command run on the target, session left behind for status/down), plus unit tests for host recognition, the flag split, name derivation and collisions, and reuse rules. The ssh paths are covered by unit tests only — no live remote host was used in this branch, so first-connection authentication and multiplexed reuse against a real sshd remain unverified here.

🤖 Generated with Claude Code

https://claude.ai/code/session_018WMXbmmHmEkyWWRc72uvPW

@bojieli bojieli mentioned this pull request Aug 22, 2026
5 tasks
@bojieli
bojieli force-pushed the one-shot-target-form branch from f3e4e85 to 4ae3a2d Compare August 22, 2026 04:44
Base automatically changed from fix-duplicate-reach-session-env to main August 22, 2026 04:44
Binding a session and using it took two commands, and the first of them
defaulted the session name to "default". A second `reach up` with no
--name therefore replaced the first session in place, and because the
harness launchers resolve their target through that same name, an agent
already working through it began running its commands on a different
machine. reach could always hold several targets at once; they had to be
named by hand, and nothing in the interface said so.

Now the target names the session. `reach build-box claude` binds a
session to build-box and launches Claude Code against it; `reach
build-box exec -- go test ./...` and `reach build-box doctor` work the
same way, because dispatch reads a first argument that is not a command
as a target and hands the rest to the command. Session flags sit between
the two, and flag.Parse's stop-at-first-positional behaviour draws the
line: everything from the command onwards belongs to the command.

Two targets that derive the same name get numbered variants rather than
overwriting each other, and `reach up` now says so on stderr when it
repoints a name another session was using.

A second command against a target reuses that target's session instead
of probing again, since a probe costs a round trip and an
authentication. The connection is still re-established while the
operator is present, because every connection after that runs in batch
mode and cannot prompt for a passphrase or a hardware token — except for
commands that never touch the target (log, status, env), where opening
one would ask for a token touch to read a local file.

Targets may now be spelled without a path: the session works wherever a
login on that machine lands, which the probe asks the machine for rather
than guessing at a home directory that may not exist. A bare word is
read as a host only when the operator's own ssh configuration or hosts
file names it, or when it is an address or a dotted name. DNS is
deliberately not consulted — resolvers that answer for every name are
common enough that a lookup would turn `reach stauts` into a connection
attempt on exactly the networks where that is hardest to see.

Also fixes --mode accepting a misspelling and behaving as exec, since
only "mirror" is ever compared against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WMXbmmHmEkyWWRc72uvPW
@bojieli
bojieli force-pushed the one-shot-target-form branch from 4ae3a2d to 2f9e79f Compare August 22, 2026 04:44
@bojieli
bojieli merged commit 54ce8bb into main Aug 22, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant