Problem
Remote operations (send, capture, git operations) bypass the daemon and execute SSH commands directly from the CLI. This causes:
- Shell escaping bugs — messages containing
?, *, !, [ etc. are interpreted as globs by the remote shell (zsh). See the recent shellQuote stopgap fix.
- Duplicated logic — the daemon already handles local session operations, but the CLI reimplements equivalent logic for remote targets via SSH.
- No unified error handling — SSH failures surface as raw exit codes rather than structured errors.
- 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
🤖 Generated with Claude Code
Problem
Remote operations (
send,capture, git operations) bypass the daemon and execute SSH commands directly from the CLI. This causes:?,*,!,[etc. are interpreted as globs by the remote shell (zsh). See the recentshellQuotestopgap fix.TargetHostis 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 ifTargetHost != ""SendMessage— rejects ifTargetHost != ""GetDiffStats,GetBranchState,GetDiff) — only work on local pathsThe CLI (
remote_control.go) fills the gap with 8 functions that SSH directly:captureRemoteMultiplexerFromInfocaptureRemoteOpenCodeFromInfosendRemoteMultiplexerFromInfosendRemoteTmuxBracketedPastesendRemoteZellijBracketedPastesendRemoteOpenCodeFromInforunSSHScriptOutputrunSSHOutputArgsProposed solution
Phase 1: Daemon handles remote targets
TargetHostrejection guards inproto_handler.goTargetHostis set, the daemon delegates to the remote host (SSH or daemon-to-daemon)Phase 2: Daemon-to-daemon communication (stretch)
Phase 3: Enforce via semgrep
exec.Command("ssh", ...)orrunSSHOutputArgs/runSSHScriptOutputcalls outside of the daemon package.semgrep/no-direct-ssh-in-cli.yml) and run in CIAcceptance criteria
orch send <run> "message with ? and * chars"works without shell escaping issuesremote_control.goSSH helper functions are removed or moved into the daemon🤖 Generated with Claude Code