pi-local2ssh
A pi extension that lets the local agent transparently operate on remote machines over SSH — read, write, edit, bash, ls, grep, and find all routed through a persistent ControlMaster connection.
Package details
Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-local2ssh
Let the local pi agent operate on a remote machine over SSH transparently — once connected, every read, write, edit, bash, ls, grep, and find call is automatically routed to the remote host. You barely have to think about the fact that you are working remotely.
Quick Start
Inside pi, type:
/ssh # interactive host picker
/ssh ubuntu.orb.local # direct connect (reads ~/.ssh/config)
/ssh lyenbot@192.168.1.5 # direct connect (ad-hoc user@host)
/ssh ubuntu.orb.local:~/code/myapp # connect and switch to remote cwd
After that, every tool the LLM invokes runs on the remote. Use /ssh off to disconnect back to local.
Commands
| Command | Description |
|---|---|
/ssh |
Interactive host picker (from SSH config + custom JSON) |
/ssh <alias|user@host> |
Direct connect |
/ssh <alias|user@host>:/path |
Direct connect and switch remote cwd |
/ssh cd [path] |
Switch remote cwd (interactive picker when no arg) |
/ssh pwd |
Show remote cwd |
/ssh hosts |
List all available hosts |
/ssh status |
Current connection status |
/ssh off (aliases: disconnect, local) |
Disconnect back to local |
/ssh help |
Help |
Host Sources
Resolved in priority order:
1. ~/.pi/agent/ssh-remote.json (recommended)
Most readable; supports groups. Example:
{
"hosts": [
{ "name": "dev-server", "target": "deploy@dev.example.com", "path": "/srv/myapp" },
{ "name": "gpu-box", "target": "ai@10.0.0.5", "path": "/home/ai/projects" }
],
"groups": {
"staging": [
{ "name": "stage-1", "target": "ubuntu@stage-1.internal", "path": "/var/www" }
]
}
}
Each host may also set forwardX11, forwardAgent, and portForwards
([{ "remotePort": 8080, "localHost": "127.0.0.1", "localPort": 3000 }]).
2. ~/.ssh/config
Auto-reads every Host <name> entry, recognizing Hostname, User, Port,
ForwardX11, ForwardAgent. Wildcard hosts like Host * are ignored.
Status
- Status-bar widget shows the live connection:
⇄ dev-server @ /home/ai/projects, or stays empty in local mode - Run
/ssh statusfor full details - System prompt is auto-augmented so the LLM knows "we are remote" and resolves relative paths against
<remoteCwd>
Transparent Tool Forwarding
| Tool | In remote mode |
|---|---|
read, write, edit |
cat / base64 / chunked writes over SSH |
bash (including !bang user commands) |
ssh <host> 'cd <rcwd> && <cmd>' with live streaming output |
ls, find, grep |
Runs stat / ls / rg / fd on the remote; results mapped back to local-view paths |
grep |
Uses remote rg (ripgrep). Supports context lines. |
find |
Uses remote fd (preferred), falls back to find -name. |
Image detectImageMimeType |
Remote file --mime-type |
The LLM never has to be aware it is on a remote. The only signal is a line in the system prompt noting the cwd is a remote path.
Binary I/O
All binary read/write goes through base64 chunked transfer — safe for any binary content. writeFile automatically mkdir -ps parent directories.
Example Session
> /ssh ubuntu.orb.local
✓ Connected ubuntu.orb.local @ /home/shingbd
> Show me package.json in ~/myapp
(LLM invokes read tool → reads /home/shingbd/myapp/package.json)
> Change scripts.test to "vitest run"
(LLM invokes edit tool → writes the remote file)
> /ssh cd myapp/src
✓ cd → /home/shingbd/myapp/src
> Run npm test
(LLM invokes bash tool → ssh ubuntu.orb.local 'cd /home/shingbd/myapp/src && npm test')
> /ssh off
Disconnected SSH. Local cwd: /Users/shingbd/...
Path Semantics
- System prompt injects
<remoteCwd>as the cwd - LLM-relative paths → resolved against
<remoteCwd> - Local absolute paths (under the session's original local cwd) → mapped to
<remoteCwd> - Absolute paths outside both cwds → passed through (for reading
/etc/hosts, etc.)
Tip: If the LLM reports "file not found" after switching to remote mode, it is probably using an absolute local path. Use a relative path instead.
Failure Handling
- Connection failure → TUI error toast, stays in local mode
- Command timeout → error returned to the LLM so it can retry
- SSH child interrupted → upstream
AbortSignalpropagates to the child - Output too large → > 100 MB auto-kills the child to prevent OOM
Performance / Limits
- Large file read/write chunked via base64 (1024 bytes per chunk)
grepparses ripgrep JSON output, streamed (does not affect LLM-side truncation)findusesfd, falls back tofind + grep -Ebashis fully streaming (long-running outputs are fine)
Requirements
- SSH key-based auth (password auth will hang)
- Remote POSIX basics:
cat,test,stat,mkdir,printf,base64 - Recommended on remote (for search):
ripgrep (rg),fd-find (fd) - Tested on macOS / Linux / WSL
Installation
npm install pi-local2ssh
Then enable it in your pi config (e.g. ~/.pi/agent/extensions.json):
{
"extensions": [
"pi-local2ssh"
]
}
Pi will dynamically import("pi-local2ssh") and activate the /ssh command. The extension's read/write/edit/bash/ls/grep/find tools will override the built-in ones while connected to a remote host.
Try without installing
If you'd rather not install globally, you can also load it from a local clone:
{
"extensions": [
"/absolute/path/to/pi-local2ssh/index.ts"
]
}
File Locations
~/.pi/agent/extensions/pi-local2ssh/ # (when loaded from local source)
~/.pi/agent/ssh-remote.json # (optional) custom host config
FAQ
Q: After switching to remote mode the LLM keeps saying files don't exist?
A: Check /ssh status to confirm the cwd. The LLM may be using an absolute local path; switch to relative.
Q: Want to use a jump host?
A: Use ProxyJump in ~/.ssh/config, then /ssh <alias>.
Q: How do I make a host always start in a specific directory?
A: Set path for that host in ~/.pi/agent/ssh-remote.json.
Q: Do I have to reconnect every time pi restarts? A: Yes — connection state is module-level. It persists across the session but resets on pi restart.
License
MIT