Files

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_connect
  • ssh_cd
  • ssh_read
  • ssh_write
  • ssh_edit
  • ssh_find
  • ssh_grep
  • ssh_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:

  1. lets you select concrete aliases from ~/.ssh/config;
  2. uses ssh -G to resolve HostName, User, Port, IdentityFile, and IdentityAgent;
  3. asks whether the selected host uses an SSH agent, private key, or password;
  4. validates the selected agent socket or asks for a private-key passphrase when needed;
  5. obtains and displays the server's SHA256 host-key fingerprint;
  6. connects with ssh2 to verify authentication;
  7. optionally assigns a display label and group;
  8. 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_connect is 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 ssh2 client is used for the active host; no interactive shell or PTY state is retained.
  • ssh_cd is 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_bash call opens a fresh exec channel and runs under bash -lc in the active remote cwd. A command-local cd is temporary, and cd, 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.md and CLAUDE.md files are never discovered or injected.
  • ssh_find and ssh_grep perform capability detection inside each approved call and return at most 200 bounded result lines. Search targets are always separate from the execution cwd, so ssh_grep can 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 with ssh_find.
  • RTK treats only ssh_bash as 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.

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_connect starts as ask, so AutoReview can verify that the direct user request names the requested imported host;
  • ssh_cd, ssh_read, ssh_write, ssh_edit, ssh_find, and ssh_grep start as ask;
  • ssh_bash uses the full deterministic Bash policy and decisionFloor: "ask";
  • deterministic hard denies remain denies;
  • asks enter the configured auto-review authorizer;
  • 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.enc is 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 registration
  • src/ssh2-transport.ts — persistent ssh2, exec, and SFTP transport
  • src/remote-bash.ts — stateless Bash adapter pinned to the active remote cwd
  • src/remote-cwd.ts — explicit remote workspace path resolution
  • src/config.ts — validated configuration types
  • src/vault.ts — AES-GCM vault
  • src/import.tsssh -G import helpers
  • scripts/ssh-config.mjs — interactive configuration CLI
  • permission-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.