← Back to Blog
DevOps2026-05-05Β·40 min read

How I Made Pi Run on a Remote Machine Without Touching the Remote

By Nour Mohamed Amine

How I Made Pi Run on a Remote Machine Without Touching the Remote

Current Situation Analysis

pi is fundamentally designed around local filesystem semantics. It resolves AGENTS.md, discovers skills, and executes git operations relative to the current working directory. When project code resides on a remote Linux server, this locality assumption breaks immediately, rendering the tool blind to project context.

Traditional workarounds introduce significant operational friction and reliability failures:

  • Rsync Synchronization: Requires a manual sync step before every session. This creates state drift, increases cognitive load, and fails to support real-time file watching or dynamic branch switching.
  • SSHFS Mounts: While theoretically seamless, SSHFS suffers from severe performance degradation on macOS due to FUSE overhead and latency. Git operations (status, log, diff) become 3-8x slower because git's heavy metadata scanning triggers excessive network round-trips. Additionally, SSHFS mounts frequently drop or corrupt file handles during network fluctuations.
  • Direct SSH Execution: Running pi directly on the remote machine isolates it from local internet dependencies. Many remote environments (VMs, private servers, air-gapped boxes) lack outbound access required for Claude API calls, authentication handshakes, and telemetry, making direct execution unviable.

WOW Moment: Key Findings

By intercepting Node's native module resolution and proxying filesystem/process calls through a secure SSH tunnel, we achieved near-native performance while preserving full remote isolation. The following benchmark compares workflow latency, git responsiveness, and platform stability across four approaches:

Approach File I/O Latency Git Operation Speed Workflow Friction macOS Stability
Native Local <2 ms Native Zero 10/10
Rsync Sync <2 ms (post-sync) Native High (manual sync per session) 10/10
SSHFS Mount 45-120 ms 3-8x slower Low 4/10 (flaky)
pi-bridge 8-15 ms (tunneled) Near-native (remote exec) Zero 10/10

Key Findings:

  • HTTP-over-SSH tunneling reduces latency by ~70% compared to FUSE-based mounts by batching metadata requests and avoiding synchronous network stalls.
  • Git operations remain fast because child_process spawns are proxied to the remote host, executing against local disk I/O on the server.
  • Zero manual synchronization steps eliminate state drift and enable instant branch/context switching.

Core Solution

The architecture relies on runtime interception rather than filesystem mounting or file copying. pi-bridge injects a preload script that patches Node's fs and child_process modules before the target CLI initializes. All filesystem reads are redirected to a lightweight HTTP server running on the remote host, while process spawns are forwarded over the same SSH tunnel.

Local                              Remote
─────────────────────────────      ──────────────────────
pii (bin/pii.js)
  └─ node --require preload.js pi
       β”œβ”€ upload server to remote
       β”œβ”€ start HTTP server
       β”œβ”€ SSH port forward
       β”œβ”€ fake local dir in /tmp
       └─ patch fs.* + child_process.*   remote/index.js
                                          reads real files
pi loads β€” sees a local project           runs git commands
  fs.readFileSync(...)  ──GET──→          ← returns data
  spawn('git log')      ──SSH──→          ← runs on remote

Installation & Usage:

npm install -g pi-bridge
pii --ssh user@host:/root/projects/my-app

Architecture Decisions:

  • Preload Injection: Using node --require preload.js ensures patches are applied before any module caching occurs, guaranteeing complete interception of fs and child_process.
  • HTTP over SSH Tunnel: Instead of raw SSH exec for every file read, a persistent HTTP server on the remote host handles batched requests, reducing connection overhead and enabling streaming responses.
  • Temporary Local Symlink: A fake local directory in /tmp satisfies Node's path resolution expectations while all actual I/O is transparently routed to the remote.
  • Security Model: The remote HTTP server binds strictly to 127.0.0.1. All traffic traverses an SSH tunnel. Authentication uses a 32-byte random token generated at startup (never exposed in CLI arguments or process lists). Path traversal attempts outside the target project directory are rejected with a 403.

Pitfall Guide

  1. Preload Execution Order Mismatch: If preload.js loads after the target CLI has already cached native fs bindings, patches will fail silently and fall back to local disk. Always verify node --require preload.js is the first argument in the spawn chain.
  2. SSH Tunnel Keep-Alive Drops: Long-running sessions often experience tunnel drops due to idle timeout policies. Configure ServerAliveInterval 30 and ServerAliveCountMax 3 in ~/.ssh/config to maintain tunnel stability without manual reconnection.
  3. Token Leakage in Process Lists: Passing authentication tokens via CLI flags exposes them in ps aux or system monitors. Always generate tokens in-memory and pass them via environment variables or stdin to maintain security compliance.
  4. Path Traversal Bypass: Naive string concatenation for remote paths allows ../ sequences to escape the project sandbox. Always resolve paths using path.resolve() and validate that the final path strictly starts with the configured project root before forwarding requests.
  5. Git Metadata Polling Overhead: Polling .git for branch changes every second can cause unnecessary CPU spikes on the remote host or trigger rate limits on slow networks. Implement exponential backoff or use inotify/fswatch on the remote side to push state changes instead of polling.
  6. Remote Binary Compatibility: child_process spawns execute binaries on the remote host. If the remote environment lacks the expected git version or system libraries, commands will fail with silent errors. Validate remote binary availability during the initial handshake phase.
  7. Memory Leaks in HTTP Tunnel: Streaming large files through the tunneled HTTP server without proper backpressure handling can exhaust Node's heap. Always pipe responses through stream.Transform with explicit highWaterMark limits and pause()/resume() controls.

Deliverables

πŸ“˜ Architecture Blueprint

  • Component mapping: pii CLI β†’ Node Preload Layer β†’ SSH Tunnel β†’ Remote HTTP Proxy β†’ Remote Filesystem/Git
  • Data flow diagrams for synchronous reads, async spawns, and branch polling mechanisms
  • Security boundary definitions (local internet layer vs. remote execution layer)

βœ… Pre-Flight Checklist

  • Remote host has Node.js β‰₯16 and SSH access configured
  • Target project directory is writable and contains valid .git metadata
  • SSH config includes keep-alive parameters (ServerAliveInterval)
  • Firewall allows outbound SSH but blocks direct HTTP exposure
  • pi-bridge installed globally and pii binary is in $PATH
  • Test run confirms AGENTS.md resolution and git branch display

βš™οΈ Configuration Templates

# ~/.ssh/config
Host pi-remote
    HostName <remote-ip>
    User <username>
    ServerAliveInterval 30
    ServerAliveCountMax 3
    StrictHostKeyChecking accept-new
// pi-bridge.config.json (optional overrides)
{
  "tunnelPort": 0,
  "authTokenLength": 32,
  "gitPollInterval": 1000,
  "sandboxRoot": "/root/projects/my-app",
  "httpTimeout": 5000
}

Repository & Support: github.com/zeflq/pi-bridge β€” Issues and PRs welcome.