Skip to content

Refactor remote operations to use client-daemon protobuf connection instead of direct SSH #439

@proboscis

Description

@proboscis

Problem

Remote operations (send, capture, git operations) bypass the daemon and execute SSH commands directly from the CLI. This causes:

  1. Shell escaping bugs — messages containing ?, *, !, [ etc. are interpreted as globs by the remote shell (zsh). See the recent shellQuote stopgap fix.
  2. Duplicated logic — the daemon already handles local session operations, but the CLI reimplements equivalent logic for remote targets via SSH.
  3. No unified error handling — SSH failures surface as raw exit codes rather than structured errors.
  4. Architectural inconsistency — the daemon rejects requests when TargetHost is set instead of delegating, forcing the CLI to maintain a parallel SSH-based code path.

Current state

The daemon (proto_handler.go) hard-errors on remote targets:

  • CaptureSession — rejects if TargetHost != ""
  • SendMessage — rejects if TargetHost != ""
  • Git operations (GetDiffStats, GetBranchState, GetDiff) — only work on local paths

The CLI (remote_control.go) fills the gap with 8 functions that SSH directly:

Function Purpose
captureRemoteMultiplexerFromInfo Capture tmux/zellij pane
captureRemoteOpenCodeFromInfo Capture OpenCode session
sendRemoteMultiplexerFromInfo Send to tmux/zellij
sendRemoteTmuxBracketedPaste Bracketed paste via tmux
sendRemoteZellijBracketedPaste Bracketed paste via zellij
sendRemoteOpenCodeFromInfo Send to OpenCode
runSSHScriptOutput Execute shell script over SSH
runSSHOutputArgs Base SSH execution

Proposed solution

Phase 1: Daemon handles remote targets

  • Remove the TargetHost rejection guards in proto_handler.go
  • When TargetHost is set, the daemon delegates to the remote host (SSH or daemon-to-daemon)
  • CLI always goes through the daemon — never calls SSH directly

Phase 2: Daemon-to-daemon communication (stretch)

  • Remote host's daemon handles the operation locally
  • Eliminates SSH shell interpretation entirely — messages are passed as protobuf fields
  • Client → local daemon → remote daemon → local tmux call

Phase 3: Enforce via semgrep

  • Add a semgrep rule that forbids direct exec.Command("ssh", ...) or runSSHOutputArgs / runSSHScriptOutput calls outside of the daemon package
  • This prevents future regressions where CLI code bypasses the daemon for remote operations
  • Rule should live in the repo (e.g. .semgrep/no-direct-ssh-in-cli.yml) and run in CI

Acceptance criteria

  • orch send <run> "message with ? and * chars" works without shell escaping issues
  • All remote session operations route through the daemon protobuf protocol
  • remote_control.go SSH helper functions are removed or moved into the daemon
  • Semgrep rule blocks direct SSH calls from CLI code in CI
  • No regression in local operation performance

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions