Repository navigation
feat: Add SSH remote support (beta) - #590
Merged
Merged
Conversation
Jinwoo-H
marked this pull request as ready for review
April 13, 2026 17:12
Jinwoo-H
force-pushed
the
Jinwoo-H/ssh
branch
3 times, most recently
from
April 14, 2026 01:25
c9c725f to
15cabbc
Compare
- SSH connection management (connect, disconnect, reconnect, port forwarding) - Relay daemon protocol for remote PTY, filesystem, and git operations - Provider abstraction layer dispatching local vs SSH-based operations - Renderer SSH state tracking via new zustand SshSlice - Disconnected worktree UI: dimmed cards, WifiOff icon, reconnect dialog - Grey SSH badges in AddWorktreeDialog, disabled for disconnected repos - Suppress test-connection state flicker via testingTargets guard - Preserve worktree list on SSH disconnect instead of clearing - SSH settings pane with Linux/macOS disclaimer
Security (P0): - Relay fs/git handlers now validate all paths via context.validatePath(), closing arbitrary file read/write/delete on remote hosts - Git relay restricts exec to allowlisted subcommands only - Host key verification uses ssh2 callback-form hostVerifier with real SHA256 fingerprint and blocking user confirmation - Shell injection prevented in ProxyCommand via shellEscape() helper - Relay deploy uses SFTP write instead of shell printf for version file - RelayContext rejects all paths before first root is registered Correctness (P1): - SshConnection: fixed double-reconnect race via client identity guard - SshConnectionManager: concurrent connect guard via connectingTargets Set - PTY handler: fixed revive ID collision (bumps nextId); SIGKILL fallback after 5s SIGTERM timeout - FrameDecoder: skip oversized frames instead of corrupting buffer state - Relay process exits after uncaughtException instead of running corrupted - Converted pty-shell-utils from sync execFileSync to async - Provider dispose() methods clean up notification subscriptions - cleanupConnection clears PTY ownership before unregistering providers - Relay deploy uses sort -V for proper semver ordering of nvm versions UX/Performance (P2): - SshPane reads connection states from store instead of per-render IPC - WorktreeCard uses store-cached target labels instead of per-card IPC - SshTargetCard buttons have aria-labels for accessibility - SshDisconnectedDialog shows yellow dot for reconnecting state - AddRepoDialog extracted useRemoteRepo hook to stay under 400-line limit - Added connectionId routing to git:remoteFileUrl, repo git helpers Tests: - Relay tests register workspace roots in beforeEach (security fix compat) - Subprocess tests send session.registerRoot before fs/git operations - SSH IPC test mocks updated for new provider exports
…ce limits - Add validatePathResolved() to RelayContext for symlink-aware path checks - Use validatePathResolved() for fs.readFile, writeFile, deletePath - Add validatePath to fs.watch + cap at 20 concurrent watchers - Remove fetch/worktree from git exec allowlist (destructive subcommands) - Add config read-only and branch destructive flag validation - Add PTY spawn limit (50) and signal allowlist for sendSignal - Clear killTimers in PtyHandler.dispose() to prevent SIGKILL to recycled PIDs - Reject rel === '.' in discard to prevent rm -rf of worktree root
- Main-side FrameDecoder: skip oversized frames instead of throwing to prevent permanent stream corruption (mirrors relay behavior) - Add concurrency guard to reestablishRelayStack with AbortController to prevent overlapping reconnections from leaking relay processes - Dispose old PTY/FS providers before unregistering during reconnection - Use crypto.randomUUID() for IPC response channels instead of Date.now() - Validate targetDir in git.addWorktree to prevent directory creation outside authorized workspace roots - Track and kill ProxyCommand child processes on SSH disconnect - Cap stderr buffer in waitForSentinel to 64KB to prevent OOM - Move ALLOWED_SIGNALS to module level in pty-handler
…ixes Security: - Shell-escape all remote paths in ssh-relay-deploy (prevent $HOME injection) - Validate remote $HOME against safe character pattern - Replace ProxyCommand PassThrough with Duplex (fix feedback loop) - Add global denied git flags (--output, --exec-path, --work-tree, --git-dir) - Block git config write flags (--add, --unset, --file, etc.) alongside read flags - Reject global git flags before subcommand (blocks -c injection) - Validate git addWorktree branch names don't start with "-" - Upgrade fs-handler to validatePathResolved for readDir, stat, createFile, createDir, rename, copy (prevents symlink escape) - Add symlink-aware validation for git discard on untracked files - Restrict lsof to cwd file descriptor only (-d cwd) Correctness: - Fix type-2 git status parsing: path/oldPath were swapped for renames - Restore dispose-before-kill in LocalPtyProvider (prevents SIGABRT crash) - Fix jumpClient/proxyProcess leak on connection retry - Always send fs.unwatch for specific rootPath on unsubscribe - Resolve BrowserWindow lazily in wireUpSshPtyEvents (prevents stale ref) - Clean up orphaned relay on abort in reestablishRelayStack - Snapshot notification handlers array before iteration (prevents skip) - Add timeout and window-close cleanup to IPC auth callbacks - Unref keepalive timer so relay can exit naturally - Clamp PTY resize cols/rows to 1-500, validate write data type Tests: - Update 4 pty.test.ts tests to async/await (handlers became async) - Fix local-pty-provider to store and dispose onData/onExit disposables Lint: - Fix prefer-template errors in ssh-relay-deploy.ts - Fix max-lines in pty-handler.ts (extract SerializedPtyEntry type) - Fix no-useless-spread in ssh-channel-multiplexer.ts
…ote UX Security: check known_hosts before prompting for host key verification, extract git exec argument validation to separate module with full test coverage, upgrade fs-handler search/listFiles to validatePathResolved, add baseRef flag-injection guards in git branchCompare/branchDiff. Correctness: fix connection timeout not resetting UI state (stuck on "connecting"/"verifying host key"), fix PTY ownership leaks on SSH exit and kill errors, fix port forward map mutation during iteration, fix system SSH dual onExit handler race, fix auth callback window-closed listener leak, guard testConnection from destroying active sessions, add portForwardManager cleanup on reconnection. UX: show Globe+SSH badge on remote repos in New Worktree dialog, allow switching to disconnected remote worktrees to view terminal history.
… limit - Add cpu-features to pnpm.onlyBuiltDependencies for ssh2 native module - Add build-essential to CI workflow for node-gyp compilation - Extract SSH auth IPC callbacks into ssh-auth-helpers.ts (ssh.ts 388→297 lines)
…ll assumption - subprocess.test.ts: tsx isn't a project dependency and unavailable in CI. Bundle the relay with esbuild in beforeAll and spawn with node instead. - local-pty-provider.test.ts: hasChildProcesses test assumed default shell is zsh (macOS), but CI runs bash. Pin SHELL env var in test setup. - Extract spawnRelay helper to subprocess-test-utils.ts (max-lines limit).
Instead of typing a remote path blindly, users can now click a Browse button to navigate the remote filesystem visually — breadcrumb path bar, directory listing with double-click navigation, and folder/file icons. - New ssh:browseDir IPC: uses raw SSH exec (ls -1ap) to list directories, bypassing the relay's workspace ACL which isn't registered yet during the add-repo flow. Shell-escapes the path to prevent injection. - RemoteFileBrowser component: breadcrumb nav, up/home buttons, scrollable directory listing, select/cancel actions. - Wired into RemoteStep with a folder button next to the path input.
Single-quoting ~ prevents shell expansion, causing "no such directory". Paths starting with ~ now use $HOME substitution instead.
Use isGitRepoAsync to check if the remote path is a git repo. Non-git directories are added with kind: 'folder', disabling git features — matching the local add-repo behavior. Also resolves to the git root path when the user picks a subdirectory.
The relay's git.isGitRepo was blocked by the workspace ACL which rejects all paths before any roots are registered. Since this check runs during the add-repo flow (before registration), it always failed and fell back to 'folder'. Remove the ACL check for this read-only operation. Also skip the worktree setup step for folder repos, matching the local add-repo behavior.
Fixes CI typecheck: getSshGitProvider returns IGitProvider but isGitRepoAsync was only defined on the concrete SshGitProvider class.
The worktrees:list and worktrees:listAll handlers were calling provider.listWorktrees for remote folder repos, which runs git worktree list on a non-git directory and fails silently. Check isFolderRepo first and return a synthetic root entry, matching how local folder repos work. This makes remote folders appear in the sidebar with a clickable entry.
Previously, non-git remote directories were silently added as folders
or failed to appear in the sidebar. Now the flow matches local repos:
repos:addRemote throws when the path isn't a git repo, the renderer
catches this and shows the NonGitFolderDialog, and the user confirms
to add with kind: 'folder'. Extended NonGitFolderDialog to handle
remote folders via repos.addRemote({ kind: 'folder' }).
Port the shell-ready startup command feature (PR #583) into our provider-based PTY architecture. Extract shell-ready wrappers, OSC 133 scanner, and command writer into local-pty-shell-ready.ts. Restore clean pty.ts as thin dispatch layer. Fix duplicate imports and unused imports from rebase merge artifacts.
- Resolve import conflict in filesystem-watcher.ts (keep both WSL and SSH imports) - Remove duplicate notifyWorktreesChanged local definition (already imported from worktree-remote) - Add eslint-disable max-lines for filesystem-watcher.ts (WSL + SSH watcher code)
- Change Add Repo buttons to 3-column grid layout with tighter padding - Show clear reconnecting state in SSH Disconnected dialog (spinner icon, updated title/description, animated loader replacing status dot) - Keep SshDisconnectedDialog mounted to prevent close animation flash when connection status changes
4 of 8 tasks
4 tasks
5 tasks done
9 tasks done
9 tasks done
8 tasks done
enrick-dev
pushed a commit
to enrick-dev/orca
that referenced
this pull request
Oct 15, 2026
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.
Summary
Add the ability to connect to remote machines over SSH and interact with remote terminals, files, and git repos — the full Orca experience on a remote host.
Architecture: Lightweight Relay Daemon
A small Node.js relay is deployed to the remote (
~/.orca-remote/relay-v{VERSION}) on first connect. All operations are multiplexed over a single SSH channel using JSON-RPC with 13-byte binary framing. The relay provides four capabilities: PTY management, filesystem operations, git operations, and file watching.What's included
SSH Connection Management (Phase 1)
ssh2with exponential backoff~/.ssh/configimport (host aliases, identity files, proxy commands, jump hosts)~/.ssh/known_hostswith UI prompt fallbackRemote Terminal (Phase 2)
IPtyProvider) routes PTY operations byconnectionIdpty.*JSON-RPC methodsRemote Filesystem (Phase 3)
IFilesystemProviderroutes all fs operations (read/write/stat/search/watch) through relay@parcel/watcheron the remote, streamingfs.changedeventsrgon the remote hostvalidatePathResolved) prevents traversal attacksRemote Git (Phase 4)
IGitProviderroutes git status/diff/stage/unstage/discard/branch operations through relay--flag=valuesyntaxPort Forwarding (Phase 6)
Settings UI
Security Hardening
RelayContext.registerRoot/validatePath)/tmp→/private/tmpsymlink resolution in root registrationUX Polish
Test plan
~/.ssh/config, verify hosts populatepnpm test— all 1374 tests passpnpm lintandpnpm typecheck— cleancloses #326