# 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. 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 ` 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: ```sh ./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, and IdentityFile; 3. asks whether the selected host uses a private key or password; 4. 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: ```sh ./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: ```text ${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 either: - a private-key path plus optional passphrase; or - the server password. Private-key contents are not copied into the vault. ## Usage Ask Pi for a concrete task on an imported host, for example: ```text Connect to packaging-server, inspect the API logs, and identify the recent 500 errors. ``` The model first calls: ```text 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. ### Adaptive remote search `ssh_find` performs fixed-substring path matching with this backend order: ```text 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): ```text 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 ```sh 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.ts` — `ssh -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](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](LICENSE).