Provider-based remote workspace plugin and Go stub for using OpenCode against Linux hosts that cannot run OpenCode directly.
opencode-remote-ssh lets OpenCode work against remote Linux machines over SSH.
The system has two parts:
- A local OpenCode plugin that resolves configured hosts, manages SSH bootstrap, and exposes a remote workspace target.
- A small Go stub installed on the remote host that serves an OpenCode-compatible API over an SSH tunnel.
There are two ways to load the plugin in your local OpenCode config:
- Package-based plugin registration
- Path-based local development registration
The public, automated path in this repository is package-based registration.
The helper CLI scripts/opencode-remote-cli.sh only manages package-based plugin config entries.
If you are loading the plugin from a local filesystem path during development, edit your config manually.
- Provider-based host management
- Explicit host selection by configured
name, realssh.host, or friendlyaliases - SSH bootstrap for the remote stub
- Hash-checked stub installation that reuses the installed binary when it already matches
- Plugin-managed SSH tunneling with tracked local tunnel teardown
- Collision-aware local tunnel port selection
- Persisted binding recovery with stale-state cleanup after restarts
- Permission-first path access
onceand persistentalwaysapprovals per workspace- Self-contained remote stub binary
- Workspace session restore support
- Shell and command execution endpoints on the remote stub
- User-facing
remote-switch,remote-status,remote-disconnect, andremote-doctortools - Lower-level
remote-workspace-create,remote-workspace-list, andremote-workspace-removetools - Configurable local
stubBinaryPathoverride when auto-discovery is not suitable - Live remote lifecycle harness for validating a configured host before relying on interactive use
- Stub test harness covering auth, permission flow, session restore, and execution behavior
- The plugin persists local binding state and can recover stale tunnels after restart, but remote token rotation can still invalidate old bindings; reconnecting creates a fresh binding when recovery fails.
- Tunnel cleanup intentionally prefers safety over aggressive process matching; recovered bindings without a trusted tracked PID may require manual cleanup if a reachable orphan tunnel already exists.
- The live remote lifecycle harness requires a real configured host and SSH access; it is not run automatically by the unit test suite.
- Local: OpenCode, Node.js for the plugin, Go 1.21+ to build the stub
- Remote: Linux, SSH access, POSIX shell
If the plugin cannot find the stub binary automatically, set stubBinaryPath in the plugin config to the built opencode-remote-stub binary.
Recommended public configuration uses the package name:
{
"plugin": [
[
"opencode-remote-provider",
{
"providers": {
"default": {
"strategy": "first_available",
"hosts": [
{
"name": "prod-web-1",
"aliases": ["web-primary", "prod-app"],
"ssh": {
"host": "203.0.113.10",
"user": "ops",
"port": 22,
"identityFile": "~/.ssh/id_ed25519"
},
"labels": ["linux", "production"]
}
]
}
}
}
]
]
}The CLI manages package-based plugin config entries in ~/.config/opencode/opencode.json.
./scripts/opencode-remote-cli.sh add default prod-web-1 ops 22 ~/.ssh/id_ed25519
./scripts/opencode-remote-cli.sh listUse the setup script to preinstall and start the remote stub on the host:
./scripts/setup-host.sh 203.0.113.10 ops 22 ~/.ssh/id_ed25519What this does:
- Verifies SSH connectivity
- Creates remote directories under
~/.opencode-remote/ - Uploads the stub binary
- Writes a new auth token
- Starts the stub on the remote host
What it does not do by itself:
- It does not permanently attach OpenCode to the remote host.
- The runtime connection is established later by the plugin when a workspace is created.
This step is useful for manual preflight work. The normal plugin path also bootstraps or updates the stub when remote-switch or workspace creation runs.
The plugin exposes a convenience tool for the common first-launch path:
Use the remote-switch tool with provider default and host prod-web-1.
remote-switch resolves the configured host name, real ssh.host, or alias; ensures the stub is installed and running; creates or recovers the SSH tunnel; verifies remote health; creates a remote workspace/session; and persists the local binding for later recovery.
You can check or recover current bindings with:
Use the remote-status tool.
You can run a preflight without keeping a new test workspace around with:
Use the remote-doctor tool with provider default and host prod-web-1.
You can disconnect with:
Use the remote-disconnect tool.
When creating a workspace, specify the ssh-provider type and provider/host in extra:
{
"type": "ssh-provider",
"extra": {
"provider": "default",
"host": "prod-web-1"
}
}The plugin will:
- Resolve the configured host by
name,ssh.host, alias, or provider labels - Ensure the remote stub is installed and running
- Establish a local SSH tunnel
- Verify remote health
- Return the remote workspace target to OpenCode
./scripts/opencode-remote-cli.sh init
./scripts/opencode-remote-cli.sh add <provider> <host> <user> [port] [identity-file]
./scripts/opencode-remote-cli.sh remove <provider> <host>
./scripts/opencode-remote-cli.sh list
./scripts/opencode-remote-cli.sh setup <host> <user> [port] [identity-file]Notes:
- The CLI creates timestamped backups of
~/.config/opencode/opencode.jsonbefore modifying it. - The CLI only supports package-based plugin entries.
- If your config uses a local filesystem path to load the plugin, manage that config block manually.
If you are developing this repository locally, you may point OpenCode at the plugin source directory directly.
Example:
{
"plugin": [
[
"/absolute/path/to/opencode-remote/plugin",
{
"providers": {
"default": {
"hosts": []
}
}
}
]
]
}In that mode:
- You are using a path-based local dev setup.
- The CLI in this repository will not mutate that plugin config automatically.
- Edit the config manually.
Each host may now define optional aliases, and workspace creation can target either the canonical name or any configured alias. The plugin still resolves the real network destination from ssh.host at runtime.
Example:
{
"providers": {
"default": {
"hosts": [
{
"name": "prod-web-1",
"aliases": ["web-primary", "prod-app"],
"ssh": {
"host": "203.0.113.10",
"user": "ops",
"port": 22
},
"labels": ["linux", "remote"]
}
]
}
}
}Any of these workspace targets now resolve to the same configured host:
{
"type": "ssh-provider",
"extra": {
"provider": "default",
"host": "prod-web-1"
}
}{
"type": "ssh-provider",
"extra": {
"provider": "default",
"host": "web-primary"
}
}Path access is denied by default.
When a shell or command operation attempts to access an unapproved path:
- The stub creates a permission request.
- The request must be approved explicitly.
onceapprovals allow exactly one matching operation.alwaysapprovals persist for that workspace.
The plugin does not auto-approve these permissions.
On the remote host, files are installed under ~/.opencode-remote/:
~/.opencode-remote/
├── bin/
│ └── opencode-remote-stub
├── run/
│ └── stub.token
├── log/
│ └── stub.log
└── state/
├── workspaces/
├── sessions/
└── approvals/
Run the full plugin/stub lifecycle harness from the plugin directory after building the stub and configuring at least one host:
cd plugin
npm run verify:remote -- --provider default --host prod-web-1The harness uses the same provider registry and SSH manager as the plugin. It resolves the host by name, ssh.host, or alias; bootstraps or updates the stub; creates a tunnel; health-checks through the forwarded URL; creates a workspace/session; verifies the permission flow with a shell command; and cleans up the test workspace/session/tunnel.
If you want to verify the stub manually after setup:
ssh -N -L 39300:127.0.0.1:39217 user@host
TOKEN=$(ssh user@host 'cat ~/.opencode-remote/run/stub.token')
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:39300/global/health- Older hosts such as CentOS/RHEL 7 era systems may require a statically built stub binary.
- The current build/test flow for this repository uses
CGO_ENABLED=0when targeting those older hosts. - Some older hosts do not keep a backgrounded process alive reliably with a plain remote
nohup ... &launch. setup-host.shnow starts the stub in its own session from the Python launcher so older systems are less likely to kill it when the bootstrap SSH process exits.- Plugin bootstrap compares the local and remote stub hashes before upload, so a healthy matching binary is reused instead of overwritten.
- Persisted bindings are recovered through the forwarded health endpoint; bindings that cannot be resolved or reached are removed from local state so they do not block a future connection.
- Password-only SSH hosts are supported for bootstrap:
- if
sshpassis installed locally, the script can install the generated key automatically - otherwise, the script prints exact manual
authorized_keyssetup steps
- if
Build the plugin:
cd plugin
npm install
npm run buildBuild the stub:
cd stub
CGO_ENABLED=0 go build -o bin/opencode-remote-stub ./cmd