@99percentpeople/pi-ssh-remote
Route Pi's file and shell tools to remote Unix or Windows workspaces through reusable OpenSSH or ssh2 transports
Package details
Install @99percentpeople/pi-ssh-remote from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@99percentpeople/pi-ssh-remote- Package
@99percentpeople/pi-ssh-remote- Version
0.3.4- Published
- Aug 3, 2026
- Downloads
- 697/mo · 697/wk
- Author
- 99percentpeople
- License
- MIT
- Types
- extension
- Size
- 503.2 KB
- Dependencies
- 3 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./dist/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@99percentpeople/pi-ssh-remote
Use the local Pi coding agent against a remote Unix or
Windows workspace. The extension routes Pi's built-in read, write, edit,
and bash tools plus the optional grep, find, and ls tools and user
!/!! commands through a selectable OpenSSH or ssh2 transport while
leaving the Pi UI, model credentials, packages, and session files on the local
machine.
The extension supports Linux, macOS, and Windows clients connected to:
- Unix hosts with POSIX
sh, including Bash, Zsh, ash, and BusyBox; - Windows OpenSSH hosts with PowerShell 7 or Windows PowerShell 5.1.
Features
- Provides
auto,openssh, andssh2transports through/99settingsor--ssh-transport - Reuses one authenticated connection by default: managed OpenSSH multiplexing
on Linux/macOS and persistent
ssh2channels on Windows - Resolves aliases and effective settings through the normal OpenSSH config;
both transports support single- and multi-hop
ProxyJump, while OpenSSH mode retains arbitraryProxyCommandand other advanced client behavior - Accepts rsync-style targets such as
host:/srv/projectanduser@host:path - Auto-detects the remote account's login shell (Zsh accounts get Zsh), or
selects a shell explicitly via
--ssh-shell; no per-target settings files are needed - Shell selection is one flag: automatic login-shell detection by default, or
explicit
--ssh-shellwith a sh/PowerShell fallback and warning when missing; probing failure keeps the deterministic Bash/PowerShell order - Keeps Pi's native tool schemas, truncation, diffs, rendering, and mutation
queue behavior;
grep,find, andlsretain Pi's default disabled state - Streams file contents over SSH stdin/stdout instead of putting them in command-line arguments
- Stores the target, platform, shell, remote home, and resolved cwd in the Pi session
- Restores and reconnects after
/resume,pi -r,pi -c, reload, fork, or clone;/newinherits the previous remote target - Fails closed after a configured connection fails instead of silently writing to the local filesystem
- Adapts
@99percentpeople/pi-background-tasksthrough itsbg:registerbackend when that extension is installed
Quick start
# 1. Install
pi install npm:@99percentpeople/pi-ssh-remote
# 2. Start a session against a remote Unix or Windows workspace.
# The default transport is auto: multiplexed OpenSSH on Linux/macOS,
# persistent ssh2 on Windows.
pi --ssh devbox:/srv/project
pi --ssh 'winuser@winbox:C:\Users\winuser\project'
# 3. Check the active connection and the effective transport
/ssh-status # target, platform, shell, transport, cwd/home
/ssh-reconnect # reconnect / apply a transport change
Common options:
--ssh-shell auto|bash|zsh|pwsh|powershell # remote shell (default auto-detect)
--ssh-transport auto|openssh|ssh2 # transport preference
--ssh-config <path> # alternate local OpenSSH config
Jump hosts work through the normal OpenSSH config with either transport:
Host devbox
HostName 192.0.2.20
User deploy
IdentityFile ~/.ssh/company
ProxyJump bastion
pi --ssh devbox:/srv/project
See SSH transport for transport details, Remote shell selection for shell detection, and Commands for the session commands.
Install
pi install npm:@99percentpeople/pi-ssh-remote
During local development:
bun run build:packages
bun run --cwd extensions/ssh-remote build
pi -e ./extensions/ssh-remote/index.ts --ssh devbox:/srv/project
Bun on Windows: installing the package with
bun add/bun installmay blockssh2's postinstall scripts (Bun printsBlocked N postinstalls). That skips the native build of ssh2's bundled AES-GCM/ChaCha20-Poly1305 binding (lib/protocol/crypto) and the persistentssh2transport can hang at connection setup. Runbun pm untrustedto allow the scripts (or install withnpm i) before usingssh2mode.
SSH transport
The default is auto:
| Local platform | Foreground transport | Connection behavior |
|---|---|---|
| Linux / macOS | OpenSSH | Extension-managed ControlMaster and ControlPersist |
| Windows | ssh2 |
One persistent TCP/authentication connection with an exec channel per operation |
Choose explicitly from SSH Remote → Transport in /99settings, or on the
command line:
pi --ssh devbox:/srv/project --ssh-transport auto
pi --ssh devbox:/srv/project --ssh-transport openssh
pi --ssh devbox:/srv/project --ssh-transport ssh2
A saved setting applies to the next remote connection; use /ssh-reconnect to
apply it to an active workspace. A command-line value overrides the saved
setting. /ssh-status reports the effective transport and whether it is reused.
In auto mode on Windows, an unsupported ssh2 configuration or connection
setup automatically falls back to single-use OpenSSH and displays the reason.
Explicit ssh2 mode fails instead, allowing configuration incompatibilities to
be diagnosed rather than hidden.
OpenSSH mode
The extension invokes the system ssh executable (ssh.exe on Windows) with
its destination alias unchanged. Without --ssh-config, OpenSSH automatically
reads its normal user and system configuration, including ~/.ssh/config.
On Linux and macOS, the extension supplies a private, short-lived ControlPath
and closes its ControlMaster during session shutdown. Each operation still
spawns a lightweight ssh process, but it opens a channel on the existing TCP
and authenticated SSH connection. This works with either Unix or Windows
remote hosts.
The native Windows OpenSSH client does not reliably support ControlMaster, so
OpenSSH mode forces ControlMaster=no and executes each foreground operation
through a separate connection. Install or enable the Windows OpenSSH Client
capability and ensure ssh.exe is available on PATH.
Host devbox
HostName 192.0.2.20
User deploy
Port 2222
IdentityFile ~/.ssh/company
ProxyJump bastion
ssh devbox
pi --ssh devbox:/srv/project
ssh2 mode and OpenSSH compatibility
ssh2 mode runs ssh -G to resolve aliases, Include, Match, host, user,
port, identity files, agent location, keepalives, effective algorithm lists,
and each ProxyJump endpoint. It verifies every jump and final server key
against direct entries in the configured OpenSSH known_hosts files and
supports unencrypted private keys plus Unix, Windows OpenSSH named-pipe,
Cygwin, or Pageant agents where ssh2 supports them.
ProxyJump jump1,jump2 is implemented entirely through ssh2 direct-tcpip
channels: every hop has its own authenticated SSH connection, and the next
connection uses the previous hop's channel as its socket. No ssh, nc, or
socat executable is required on a jump server. Each jump server must permit
TCP forwarding to the next host (for OpenSSH servers, check
AllowTcpForwarding and PermitOpen), and destination names are resolved from
the preceding jump server's network.
The following effective OpenSSH features require openssh mode and produce a
clear compatibility error: arbitrary ProxyCommand, KnownHostsCommand,
RemoteCommand, CertificateFile, @cert-authority, PKCS11Provider, and
multi-step AuthenticationMethods. Keyboard-interactive and GSSAPI login,
security-key/FIDO identities, and encrypted keys combined with
IdentitiesOnly=yes are also unsupported; direct password authentication is
available through the TUI. ControlMaster, ControlPersist,
and ControlPath are unnecessary and ignored because ssh2 owns the persistent
connection itself. Paths containing spaces in a multi-file
UserKnownHostsFile/GlobalKnownHostsFile value may not be reproduced; select
OpenSSH for those configurations.
The negotiated cipher list is filtered at runtime: Bun does not currently
implement the chacha20-poly1305 cipher in node:crypto
(oven-sh/bun#8072), so that cipher is dropped automatically and the
connection uses AES-GCM/CTR instead of failing.
Both transports are non-interactive. The extension enables BatchMode=yes for
OpenSSH and a ten-second connection timeout, so passphrase and new-host
prompts do not corrupt the Pi TUI. Load keys into an SSH agent and accept a
new host key with the system OpenSSH client before starting Pi.
Password authentication
When public-key or agent authentication fails, the TUI asks for a password
(plain-text input until Pi gains a masked input API) and retries. On Unix,
auto tries multiplexed OpenSSH first, retrying a rejected password in place
through sshpass when it is installed (cached secrets first, no re-ask), and
falls back to ssh2 only when sshpass is missing (ssh2 is the remaining
password-capable transport). If the user cancels the prompt or every password
attempt is rejected, the connection fails outright — ssh2 would reject the
same secret, so there is nothing to fall back to. ssh2 mode (and Windows
auto) prompts directly. After a password was actually tried, a rejection is
surfaced verbatim in a warning before the next prompt (for example Permission denied (publickey,password)). OpenSSH's advertised authentication-method list
is checked first, so publickey-only servers fail directly without a pointless
password prompt. Explicit openssh mode uses the system sshpass when installed (apt install sshpass on Debian/Ubuntu,
pacman -S sshpass in Git Bash on Windows, or a standalone sshpass.exe):
the password travels only in the SSHPASS environment variable, the remote
side stays PTY-free (-T) so binary file data is untouched, and a single
prompt attempt keeps the retry loop in the extension. Without sshpass,
explicit openssh reports the missing tool or falls back to ssh2 on
auto. The password is held in memory for the process, so /resume,
reconnects, and extra channels reuse it without re-asking; with
persistPasswords enabled (default) it is also saved to a 0600
ssh-remote-secrets.json next to Pi's settings so -r restarts reuse it
too. Public keys and the agent always win; passwords never enter SSH command
arguments and are passed only through ssh2 or the SSHPASS environment. A
wrong password re-prompts until cancelled or the retry safety limit is reached.
Clear all cached passwords with /ssh-forget-passwords, disable prompting in
/99settings (Password prompt), or disable persistence (Persist passwords).
Headless sessions never prompt.
Use a different local config file only when needed:
pi --ssh devbox:/srv/project --ssh-config ~/.ssh/work.conf
Accepted target forms include:
devbox
deploy@devbox:/srv/project
deploy@[2001:db8::10]:/srv/project
winbox
winuser@winbox:C:\Users\winuser\project
With no path, the remote login working directory is used. This is normally the remote home directory. Relative paths are resolved from that directory.
Remote shell selection
The default is automatic detection:
pi --ssh devbox --ssh-shell auto
On Unix hosts, auto probes the remote account's login shell (getent passwd,
falling back to the sh symlink target on systems without getent, both
inside sh -c so the remote default shell syntax does not matter). Zsh
accounts get Zsh for the bash tool, ! commands, and background jobs. A
Zsh login shell implies zsh is installed, so no separate existence check is
needed. Everything else keeps the deterministic order: Unix Bash, then
sh for ash-only hosts, PowerShell 7, then Windows PowerShell 5.1.
Control operations (HOME/cwd probe, paths, file/search tools) are POSIX sh
scripts, so every Unix host works even without Bash: OpenWrt, Alpine, and
busybox containers all get full read/write/edit/grep/find/ls support through
ash. Commands entered by the model run in the detected shell (sh on such
hosts), so they use POSIX syntax. Known POSIX trade-offs: filenames
containing newlines are not handled, and grep/find glob patterns containing
) are not supported.
Choose a shell explicitly:
pi --ssh devbox --ssh-shell bash
pi --ssh devbox --ssh-shell zsh
pi --ssh winbox --ssh-shell pwsh
pi --ssh winbox --ssh-shell powershell
An explicit shell is probed for existence first. If it is missing, the
extension warns and falls back: zsh/bash to sh on Unix, pwsh to
PowerShell 5.1 (and vice versa) on Windows.
On Windows, PowerShell control scripts and user PowerShell commands use encoded UTF-16LE payloads, so content is not exposed in the SSH process arguments. File data continues to travel as binary stdin/stdout.
Unix workspaces
Unix paths use POSIX syntax:
pi --ssh devbox:~/project
pi --ssh devbox:/srv/project
The bash tool and user ! commands execute the remote shell (Bash, Zsh,
sh, or PowerShell). Workspace control operations (paths, file probes) run
through POSIX sh on Unix and the selected PowerShell on Windows regardless
of that selection.
Windows workspaces
Windows paths support drive-qualified and UNC forms:
pi --ssh 'winbox:C:\Users\developer\project'
pi --ssh 'winbox:D:\source'
pi --ssh 'winbox:\\server\share\project'
Relative paths, ~, ~/path, and ~\path are resolved against the remote
Windows user profile and working directory. Drive-relative paths such as
C:folder and ~other paths are rejected because their meaning is ambiguous.
The tool remains named bash for Pi compatibility, but its prompt and session
context tell the model to use PowerShell, Zsh, Bash, or POSIX sh syntax as
appropriate. User !/!! commands use the same remote shell, and paths and
file operations continue through the same remote shell.
Session resume
Pi conversations remain in the local Pi session directory. The extension adds a hidden, branch-aware entry containing only:
- the SSH destination or alias;
- the resolved remote platform and shell;
- the resolved remote cwd and home;
- the optional local OpenSSH config path.
It never copies private keys, passwords, SSH config contents, or remote file contents into this state. Version 1 Unix/Bash entries are migrated in memory to the version 2 platform/shell state, so existing conversations continue to resume.
A resumed session reconnects its stored target even when Pi was started without
--ssh. Passing a different target, config, cwd, or conflicting explicit shell
while resuming is rejected to prevent an old conversation from modifying
another machine.
Pi groups sessions by its local cwd. Start each remote project from a stable
local anchor directory and name important sessions. In /resume, press Tab to
switch from Current Folder to All.
Conversation history is restored, but the extension does not snapshot remote files or revive running processes. Remote repository contents may have changed between sessions.
Commands
/ssh-status Show target, platform, shell, transport, cwd/home
/ssh-reconnect Retry the target stored in the current session
/ssh-forget-passwords Clear cached SSH passwords (memory and secrets file)
These commands are registered only when the current session requests, resumes, or inherits an SSH workspace, so ordinary local sessions do not show them.
The status line reports only connection state: SSH: is muted, while
Connecting, Connected, or Disconnected uses the matching warning,
success, or error color. It does not repeat the remote path.
When the session has no custom name, SSH Remote uses Pi's session name to show
the location in both the normal first footer line and the native /resume
list. It queries the current remote Git branch after a successful connection
and appends the first user message when it arrives, producing a line such as:
~ • SSH devbox:C:\Users\dev\Desktop\pi-extensions (master) • Fix the build
The first message is normalized to one line, matching the information Pi uses
for ordinary unnamed sessions. The SSH location stays first as a stable remote
identifier; on narrow terminals Pi may truncate the message suffix. This single
session name preserves Pi's built-in footer, token/model statistics, and other
extension statuses without installing a custom footer or widget. A
user-assigned /name remains untouched. Temporary [host:path] title names
created by the prefix experiment are migrated back to the automatic
SSH target:path (branch) • first message format when that workspace
reconnects.
If connection setup fails, overridden tools report the SSH failure and do not fall back to local operations.
Shared workspace files
SSH Remote registers its active binary file backend through
@99percentpeople/pi-workspace-files. Tools using that shared package follow
the same remote adapter path as Pi's routed read and write tools instead of
adding tool-specific SSH hooks.
@99percentpeople/pi-codex-api uses this backend automatically:
output_pathis resolved against the remote workspace. The Base64 PNG from the image API is decoded and written directly through the active SSH adapter, then reported using its native Unix or Windows path.referenced_image_pathsare read directly through the same remote adapter and converted to data URLs for the image API request.- No generated image or reference image is staged in Pi's local workspace, and
no reverse SSH or separate
scpstep is required. - The default destination remains
output/codex-images/<tool-call>.png, but it is created remotely. - Existing remote files are never overwritten. Paths outside the remote
workspace are rejected, matching
codex_image's normal workspace boundary.
For example, a Windows SSH session can use:
output_path: C:\Users\dev\Desktop\wallpaper.png
referenced_image_paths: [C:\Users\dev\Desktop\reference.jpg]
Background tasks
When @99percentpeople/pi-background-tasks is installed, SSH Remote registers a
remote shell backend for both pipe and PTY jobs. Background Tasks 1.2.7 or newer
also honors the adapter's local launch cwd, allowing bg_start.cwd to name a
remote-only Unix or Windows directory.
Background jobs require a real local process/PTY and therefore always launch
the system OpenSSH client rather than an ssh2 foreground channel. With the
OpenSSH transport on Linux/macOS they receive the same managed ControlPath and
reuse its connection. With ssh2—including Windows auto—background jobs use
separate OpenSSH connections.
Running jobs are still owned by the current Pi process. Session replacement or shutdown terminates the local SSH process and does not attempt to reattach the job after resume.
Compatibility and limitations
todo,thinking-fold,cursor-effect, andcodex_searchremain local and work normally.ssh2intentionally implements only the compatibility subset documented in ssh2 mode and OpenSSH compatibility, plus TUI password authentication for the destination andProxyJumphops. Use explicit OpenSSH mode for advanced routing, certificates, hardware keys, keyboard-interactive, or GSSAPI authentication.- On Windows clients, local sessions are left untouched so
pi-pwsh-adaptercan continue to own the localbashtool. SSH Remote registers its tool overrides only for sessions that request, resume, or inherit an SSH workspace. - A failed remote connection (unreachable host, missing remote directory, or
an unavailable remote shell) reports the error and leaves the
session in a
Disconnectedstate: tools fail closed with the initialization error, while/ssh-statusand/ssh-reconnectstay available for recovery. - Background tasks follow the same rule: while the SSH workspace is
unavailable,
bg_startis blocked with the initialization error instead of silently running on the local machine (the shared background-task backend can be claimed by other adapters such aspi-pwsh-adapter, so the block is applied at the tool level to stay independent of registration order). - On Windows,
pi-pwsh-adapterregisters thebashtool before this extension (Pi keeps the first registration per tool name). SSH Remote claims the bash tool through thebash:delegateevent protocol instead: the adapter's bash tool and!commands execute on the remote while the SSH workspace is active, fail closed while it is unavailable, and fall back to the local PowerShell backend in local sessions. Both packages must be at compatible versions for the delegation to work. - Pi's optional standalone
grep,find, andlstools remain disabled by default. If enabled through--toolsor the tool selector, they execute on the remote workspace and fail closed with the other routed tools. - Unix
finduses remotergwhen available so.gitignoreis honored; its POSIX fallback and the native Windows implementation always exclude.gitandnode_modulesbut do not parse every.gitignorerule. - Remote project discovery is not virtualized. Local
AGENTS.md,.pi, skills, and project settings still come from Pi's local anchor directory. Registered skill files and references below their skill directories are therefore read locally; all otherreadpaths remain remote. readdownloads a complete remote file before applying Pi's line and byte truncation. Avoid using it on very large files; use the remote shell to select a range.- Remote file paths are encoded into a reserved local logical namespace before
Pi's file tools run, then restored in tool output. This keeps Pi's local
mutation queue from resolving remote-only paths such as
/rootor/etc. editserializes mutations inside the current Pi process, but cannot prevent another process on the remote host from changing a file between read and write steps.- Remote command Shells inherit safe Pi model/session identifiers but never
receive
PI_SESSION_FILE, because that path exists only on the local host. - An explicitly requested shell is probed for existence; a missing Bash/Zsh
falls back to
sh, and a missingpwsh/powershellfalls back to the other PowerShell, with a warning.
Security
The package executes either the system ssh binary or the bundled ssh2
protocol client and runs remote shell commands with the same account permissions
as a manual SSH login. ssh2 refuses unknown or changed host keys and does not
implement automatic trust enrollment; establish trust with system OpenSSH
first. File paths are embedded only in encoded control scripts or shell-quoted
Unix commands, and file contents travel through SSH channels. Model-provided
shell commands are intentionally executable code. Review the package and use a
restricted remote account when appropriate.
License
MIT