10 KiB
pi-ssh
pi-ssh keeps Pi and its local tools on the local machine while exposing explicit remote tools over a persistent Node ssh2 connection:
ssh_connectssh_cdssh_readssh_writessh_editssh_findssh_grepssh_bash
The extension does not override Pi's local read, write, edit, find, grep, or bash tools.
Architecture
Runtime connections are pure ssh2; the extension does not spawn OpenSSH and does not require sshpass, ControlMaster, or passwordless login. Authentication can use a private-key file, a password, or an SSH agent socket such as 1Password's agent. Remote file operations use SFTP and remote shell commands use an SSH exec channel. The SSH transport persists, but commands intentionally use fresh non-interactive Bash processes rather than a hidden stateful PTY.
Hosts must be explicitly imported before use. OpenSSH remains only an import source: the configuration helper runs ssh -G <alias> once to resolve the selected alias, then stores the resulting endpoint and authentication data in the pi-ssh vault. Later changes to ~/.ssh/config require re-importing the host.
Connections are agent-controlled: when the user explicitly names an imported host as part of a concrete remote task, the model calls ssh_connect. The connection request enters the normal permission and auto-review chain before any network connection is opened. The extension does not register /ssh, --ssh, automatic session resume, or remote user-! overrides.
ProxyJump and ProxyCommand are intentionally rejected in the first ssh2 release.
Configure hosts
From the installed bundle or this repository:
./ssh_config.sh import
./ssh_config.sh import packaging-server
The helper:
- lets you select concrete aliases from
~/.ssh/config; - uses
ssh -Gto resolve HostName, User, Port, IdentityFile, and IdentityAgent; - asks whether the selected host uses an SSH agent, private key, or password;
- validates the selected agent socket or asks for a private-key passphrase when needed;
- obtains and displays the server's SHA256 host-key fingerprint;
- connects with
ssh2to verify authentication; - optionally assigns a display label and group;
- writes the encrypted vault.
Other commands:
./ssh_config.sh list
./ssh_config.sh update packaging-server
./ssh_config.sh remove packaging-server
./ssh_config.sh rotate-key
Secret prompts require an interactive terminal. Passwords and passphrases are never passed as command-line arguments.
Vault
The default paths are:
${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/pi-ssh/
├── hosts.enc
└── vault.key
On POSIX systems the directory is mode 700 and both files are mode 600. hosts.enc is encrypted and authenticated with AES-256-GCM; vault.key contains the adjacent random 256-bit key. Writes use a fresh IV and an atomic temporary-file rename. Plaintext configuration is never written to a temporary file.
This is deliberately a casual-disclosure boundary, not protection against compromise of the local account: anyone who can read both files can decrypt the vault. Encryption prevents the host configuration and passwords from being exposed by accidentally viewing or copying hosts.enc alone.
The encrypted payload contains host endpoints, pinned host-key fingerprints, groups, and one of:
- an SSH agent socket path;
- a private-key path plus optional passphrase; or
- the server password.
Private-key contents are not copied into the vault. Agent-backed private keys remain inside the agent (including 1Password); pi-ssh stores only the socket path and asks ssh2 to authenticate through it.
Usage
Ask Pi for a concrete task on an imported host, for example:
Connect to packaging-server, inspect the API logs, and identify the recent 500 errors.
The model first calls:
ssh_connect({ hostId: "packaging-server" })
It may set remotePath to an absolute path, ~, or a path beginning with ~/. The model calls sequential ssh_connect as a separate step and waits for the reviewed connection to succeed before using other ssh_* tools. ssh_cd explicitly changes the active remote workspace for subsequent shell, relative file, and relative search operations; it accepts absolute paths, paths relative to the current remote cwd, and ~/ paths relative to remote HOME. The model must also call ssh_cd as a separate step and wait for its successful result before issuing dependent remote operations. Only imported host IDs are accepted; arbitrary user@host targets are rejected. Unknown IDs report a bounded list of available imported IDs without exposing credentials. A new connection replaces the previous active connection, and session shutdown disconnects it.
Runtime behavior
ssh_connectis the only runtime connection surface; it is agent-callable, permission-reviewed, sequential, and cancellable so connection replacement cannot overlap another tool call or remain stuck after user cancellation.- One persistent
ssh2client is used for the active host; no interactive shell or PTY state is retained. ssh_cdis a sequential state transition: it validates a remote directory and updates the active workspace without reconnecting. Dependent tool calls must wait for it to succeed.- HOME/cwd probes use bounded random-marker framing, require one absolute POSIX path, and preserve the previous state on malformed output, timeout, or cancellation.
- Independent exec channels use bounded concurrency of four; shared SFTP operations remain serialized.
- Each
ssh_bashcall opens a fresh exec channel and runs underbash -lcin the active remote cwd. A command-localcdis temporary, andcd,export, alias, function, or other shell state inside one call does not persist to the next call. - SFTP provides remote reads and writes.
- Writes use a temporary remote file and prefer OpenSSH's atomic rename SFTP extension when the server supports it.
- A pinned SHA256 host-key mismatch fails closed.
- Connection loss fails closed; the extension does not silently replay a command.
- Remote
AGENTS.mdandCLAUDE.mdfiles are never discovered or injected. ssh_findandssh_grepperform capability detection inside each approved call and return at most 200 bounded result lines. Search targets are always separate from the execution cwd, sossh_grepcan target a single remote file without attempting to enter it as a directory. A 30-second timeout reports the resolved root and instructs the model to narrow it withssh_find.- RTK treats only
ssh_bashas a Bash output-compaction alias; it does not rewrite remote commands or process remote search/read results. - Permission previews display resolved absolute remote paths together with the original relative or
~/request; remote paths never enter local filesystem gates.
Adaptive remote search
ssh_find performs fixed-substring path matching with this backend order:
fd → fdfind → git ls-files → find
ssh_grep defaults to literal, case-insensitive content matching with this backend order. Hidden paths are excluded unless includeHidden is true, and include is a basename-only glob such as *.ts (globs containing / are rejected):
ripgrep → git grep → find + grep
No backend is installed or uploaded. The tools use what the server already provides, report the chosen backend and truncation state, and cap limit at 200. literal: false accepts the portable POSIX ERE subset shared by the backends; Git and fallback grep are explicitly run in ERE mode. Grep results use bounded NUL-delimited path/line/content records, so newline-containing filenames cannot corrupt match counts or truncation. Backend stderr is kept out of result rows and is reported as a warning on success or failure detail on error.
A genuine no-match result—including the find + grep fallback—succeeds with zero rows; invalid regexes, missing roots, and backend failures remain errors. Search paths accept absolute paths, relative paths, ~, and ~/...; ~user expansion is rejected. Narrow path and pattern when truncated rather than increasing the limit. Direct find, fd, grep, and rg commands remain forbidden through ssh_bash; use the structured search tools instead.
The remote host must provide bash. SFTP support is required for file tools.
Permission-system integration
All remote operations enter the bundle's existing permission chain:
ssh_connectstarts asask, so AutoReview can verify that the direct user request names the requested imported host;ssh_cd,ssh_read,ssh_write,ssh_edit,ssh_find, andssh_grepstart asask;ssh_bashuses the full deterministic Bash policy anddecisionFloor: "ask";- deterministic hard denies remain denies;
- asks enter the configured
auto-reviewauthorizer; - reviewer failures defer to the normal terminal prompt.
Permission evidence includes the configured host ID, resolved endpoint, port, remote cwd, and a bounded operation summary. It never includes passwords, passphrases, private-key contents, or the vault key. Remote paths are not normalized as local filesystem paths.
Security notes
- Import only servers you control or trust.
- Verify host-key fingerprints through an independent channel before accepting them.
- Treat both vault files as secrets even though
hosts.encis encrypted. - The extension's threat model does not protect credentials from malicious code already running as the same local user.
- Password keyboard-interactive mode reuses the configured password for the server's prompts; use it only with a trusted pinned host.
- Remote content reaches the model only through an explicit reviewed
ssh_*tool call.
Development
npm test
Important files:
index.ts— Pi extension and tool registrationsrc/ssh2-transport.ts— persistent ssh2, exec, and SFTP transportsrc/remote-bash.ts— stateless Bash adapter pinned to the active remote cwdsrc/remote-cwd.ts— explicit remote workspace path resolutionsrc/config.ts— validated configuration typessrc/vault.ts— AES-GCM vaultsrc/import.ts—ssh -Gimport helpersscripts/ssh-config.mjs— interactive configuration CLIpermission-integration.ts— permission-system bridge
Upstream and license
This maintained fork originates from pansapiens/pi-ssh; see UPSTREAM.md. The pure ssh2 design also references the transport architecture in @99percentpeople/pi-ssh-remote without adopting its local-tool override model. Licensed under MIT; see LICENSE.