
Setup Opencode Remote
- 2 installs
- Updated March 18, 2026
- fwfutures/vibe-a-thon
Provisions a Google Compute Engine cloud workspace, installs the opencode server, and connects the local opencode desktop app to it over an SSH tunnel.
About
A guided setup skill that spins up a GCE cloud computer, installs everything needed, and wires the local opencode app to it for remote AI coding. A developer uses it when they want a powerful cloud dev environment reachable from their laptop.
- OS-detecting flow (mac.md / win.md) with gcloud auth, instance creation, and add-metadata safety rules
- Tracks state in devserver.txt and sets up tunnel plus auto-reconnect service
Setup Opencode Remote by the numbers
- 2 all-time installs (skills.sh)
- Ranked #917 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fwfutures/vibe-a-thon --skill setup-opencode-remoteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| Last updated | March 18, 2026 |
| Repository | fwfutures/vibe-a-thon ↗ |
What it does
Provisions a Google Compute Engine cloud workspace, installs the opencode server, and connects the local opencode desktop app to it over an SSH tunnel.
Files
Cloud Workspace for OpenCode
Create a cloud computer and connect the OpenCode desktop app to it for remote AI coding.
Tell the user: "I'll set up a cloud workspace for you — takes about 5 minutes."
Use simple language: say "cloud computer" not "GCE instance", "your files" not "persistent home disk".
Step 0: Detect OS and load the right reference
Run uname -s. If it returns Darwin → macOS. If it fails or returns nothing → Windows.
After detecting the OS, read the appropriate reference file:
- macOS/Linux: Read
references/mac.mdfrom this skill directory - Windows: Read
references/win.mdfrom this skill directory
The reference file contains ALL the exact commands for every step. Follow it precisely.
Critical rules (all platforms)
1. Never pass `--metadata=` on `gcloud compute instances create` with a template — it REPLACES all template metadata (including the startup script). Create without --metadata, then use gcloud compute instances add-metadata after.
2. Collect email/project AFTER auth (Step 6, not before). Detect from gcloud config get-value account. Don't ask during the auth flow.
3. Tunnel test expects HTTP 401 (not 200). The server requires auth — a 401 means the tunnel works.
4. Track state in `devserver.txt` in the current working directory. Update after every significant step. If it already exists, read it to recover saved values.
5. The agent runs INSIDE OpenCode desktop. To connect the app to the cloud server, write a detached reconfig script that quits the app, writes config, and relaunches. See Step 12 in the reference file.
6. 3 failures = stop and tell user. Don't keep retrying the same failing command.
Step overview
| Step | What | Key action |
|---|---|---|
| 1 | Detect OS | uname -s → load mac.md or win.md |
| 2 | Python 3.11+ | macOS only — use uv if needed |
| 3 | gcloud CLI | Check/install, find full path |
| 4 | OpenCode | Check desktop app / CLI path |
| 5 | Sign in | gcloud auth login --project=path26-489205 |
| 6 | User info | Email from gcloud, ask project name |
| 7 | Create workspace | Disk + instance + add-metadata |
| 8 | Wait for ready | Poll daemon health on port 8080 |
| 9 | Install opencode server | Install, tmux serve with password |
| 10 | Clone starter repo | Private repo with GH_TOKEN |
| 11 | Port slug | Register for web access |
| 12 | Tunnel + desktop config | Detached reconfig script |
| 13 | Convenience scripts | start/stop/status + tunnel service |
devserver.txt format
Write/update this after Step 7 and after every subsequent step:
My Cloud Workspace
==================
Last updated: 2026-03-19 10:30 AM
Name: opencode-abc12345
Project: my-app
Owner: user@example.com
Password: XDJUzZACEHue3OqbnSFNYwl5K8R7ktMT
Local Machine
-------------
Tunnel: Running (SSH on port 4096)
OpenCode URL: http://localhost:4096
Auto-reconnect: Installed (starts on login)
Remote Machine
--------------
Instance: Running
OpenCode server: Running (tmux session: oc)
Starter repo: ~/fv-rome2rio-starter (cloned)
Home directory: /home/user
GH_TOKEN: Available (baked into template)
Web Access
----------
Port slug: my-app
Dev server URL: https://my-app.path26.rome2rio.com (port 3000)
Cloud Details
-------------
GCP Project: path26-489205
Zone: europe-west1-b
External IP: None (private instance)Troubleshooting
- Startup log:
gcloud compute ssh INSTANCE --command="sudo journalctl -u google-startup-scripts --no-pager | tail -30" - OpenCode server:
gcloud compute ssh INSTANCE --command="tmux capture-pane -t oc -p" - SSH directly:
gcloud compute ssh INSTANCE - "External IP not found; defaulting to IAP tunneling": Normal — instances may not have public IPs
- tmux "no server running": No session exists yet — start one
- IAP/NumPy warnings: Add
--quietto suppress
Reference sections (in mac.md / win.md)
The OS-specific reference files also contain:
- GH_TOKEN template baking instructions
- Port proxy domain info (
path26.rome2rio.com) - Tunnel service removal
- Workspace deletion
- Error recovery rules
macOS/Linux Commands Reference
Exact commands for each step on macOS (and Linux where noted).
Step 2: Python 3.11+ (macOS only, skip on Linux)
On fresh macOS, python3/git/gcloud may trigger an Xcode Command Line Tools popup. Tell user: "Click 'Install' on the popup, wait, then tell me when done."
NEVER use `sudo`. Use uv instead (installs to home dir).
After installing via uv, python3 may not be on PATH in subsequent commands. Use uv run python3 or prefix with export PATH="$HOME/.local/bin:$PATH".
python3 --version 2>/dev/null || echo "NOT_FOUND"If NOT_FOUND or below 3.11:
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv python install 3.12
python3 --versionStep 3: gcloud CLI
which gcloud 2>/dev/null && gcloud --version 2>/dev/null | head -1 || echo "NOT_FOUND"If NOT_FOUND:
if command -v brew &>/dev/null; then
brew install --cask google-cloud-sdk
else
curl -fsSL https://sdk.cloud.google.com | bash -s -- --disable-prompts --install-dir="$HOME"
export PATH="$HOME/google-cloud-sdk/bin:$PATH"
fiStep 4: OpenCode
Check desktop app first:
if [ -x "/Applications/OpenCode.app/Contents/MacOS/opencode-cli" ]; then
echo "FOUND: /Applications/OpenCode.app/Contents/MacOS/opencode-cli"
elif command -v opencode &>/dev/null; then
echo "FOUND: $(which opencode)"
else
echo "NOT_FOUND"
fiSave found path as OPENCODE_CMD.
If NOT_FOUND:
curl -fsSL https://opencode.ai/install | bash
export PATH="$HOME/.opencode/bin:$PATH"Or tell user: "Download OpenCode from https://opencode.ai/download"
Step 5: Sign in
Run synchronously. Do NOT background with `&`. Do NOT manually construct OAuth URLs.
gcloud auth login --project=path26-489205Tell user: "A browser window will open for Google sign-in. Complete it there, then come back."
Step 6: Collect user info
Do NOT ask until auth is confirmed.
gcloud config get-value account 2>/dev/null || git config --global user.email 2>/dev/null || echo "NOT_FOUND"Confirm email with user. Ask for project name.
Step 7: Create workspace
INSTANCE_ID="opencode-$(openssl rand -hex 4)"
PROJECT_ID="${GCP_PROJECT_ID:-path26-489205}"
ZONE="${GCP_GCE_ZONE:-europe-west1-b}"
TEMPLATE="${GCP_GCE_INSTANCE_TEMPLATE:-freshvibe-gce-template}"
DISK_NAME="${INSTANCE_ID}-home"
OWNER_LABEL=$(echo "USER_EMAIL" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | cut -c1-63)
PROJECT_LABEL=$(echo "PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | cut -c1-63)
# Create disk
gcloud compute disks create "$DISK_NAME" \
--project="$PROJECT_ID" --zone="$ZONE" \
--size=10GB --type=pd-balanced \
--labels="managed-by=freshvibe,owner=$OWNER_LABEL,project=$PROJECT_LABEL" \
--quiet
# Create instance — NO --metadata (would replace template metadata!)
gcloud compute instances create "$INSTANCE_ID" \
--project="$PROJECT_ID" --zone="$ZONE" \
--source-instance-template="$TEMPLATE" \
--labels="managed-by=freshvibe,owner=$OWNER_LABEL,project=$PROJECT_LABEL" \
--disk="name=$DISK_NAME,device-name=freshvibe-home,mode=rw,boot=no,auto-delete=no" \
--quiet
# Add metadata AFTER creation (merges with template metadata)
gcloud compute instances add-metadata "$INSTANCE_ID" \
--project="$PROJECT_ID" --zone="$ZONE" \
--metadata="freshvibe-owner-email-b64=$(echo -n 'USER_EMAIL' | base64),freshvibe-project-ids-b64=$(echo -n '[\"PROJECT_NAME\"]' | base64)" \
--quietStep 8: Wait for ready
for i in $(seq 1 60); do
if gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --command="curl -sf http://localhost:8080/health" --quiet 2>/dev/null; then
echo "Ready!"; break
fi
echo " Starting... ($((i*5))s)"
sleep 5
doneStep 9: Install and start OpenCode server
OPENCODE_PASSWORD="$(openssl rand -hex 16)"
# Install
gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" \
--command='curl -fsSL https://opencode.ai/install | bash'
# Add to PATH
gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" \
--command='touch ~/.bashrc ; grep -q opencode ~/.bashrc || echo "export PATH=\$HOME/.opencode/bin:\$PATH" >> ~/.bashrc'
# Find remote home
REMOTE_HOME=$(gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --command='echo $HOME' --quiet 2>/dev/null | tr -d '\r\n')
# Start in tmux — bash -lc (login shell) so /etc/profile.d/ env vars (GH_TOKEN, Vertex AI etc.) are loaded
gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" \
--command="tmux new-session -d -s oc 'bash -lc \"OPENCODE_SERVER_PASSWORD=$OPENCODE_PASSWORD ${REMOTE_HOME}/.opencode/bin/opencode serve --port 4096 --hostname 0.0.0.0\"'"Step 10: Clone starter repo
GH_TOKEN sources (check in order): 1. Local: grep "^GH_TOKEN=" .env 2. Env: echo $GH_TOKEN 3. Remote: gcloud compute ssh ... --command='grep GH_TOKEN /etc/profile.d/freshvibe-runtime.sh'
# Public repo
gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" \
--command='cd ~ ; git clone https://github.com/fwfutures/fv-rome2rio-starter.git'
# Private repo (embed token, then strip)
GH_TOKEN="${GH_TOKEN:-$(grep "^GH_TOKEN=" .env 2>/dev/null | cut -d= -f2)}"
gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" \
--command="cd ~ ; git clone https://x-access-token:${GH_TOKEN}@github.com/fwfutures/fv-rome2rio-starter.git"
gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" \
--command='cd ~/fv-rome2rio-starter ; git remote set-url origin https://github.com/fwfutures/fv-rome2rio-starter.git'Step 11: Port slug
PORT_SLUG=$(echo "PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | cut -c1-63)
gcloud compute instances add-metadata "$INSTANCE_ID" \
--project="$PROJECT_ID" --zone="$ZONE" \
--metadata="freshvibe-port-slugs=[\"$PORT_SLUG\"]"
echo "https://${PORT_SLUG}.path26.rome2rio.com (port 3000)"Step 12: Tunnel + desktop app config
12a: Start tunnel (detached, survives app restart)
nohup gcloud compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" \
-- -L 4096:localhost:4096 -N -o ServerAliveInterval=60 \
> /tmp/opencode-tunnel.log 2>&1 &
echo "Tunnel PID: $!"Test (expect 401 = tunnel works):
sleep 10 && curl -so /dev/null -w '%{http_code}' http://localhost:4096 | grep -q '401' && echo "TUNNEL_OK" || echo "TUNNEL_FAILED"12b: Reconfig desktop app (self-destruct pattern)
The agent runs INSIDE OpenCode. Write a detached script that quits the app, writes config, relaunches.
Tell user: "I'm about to restart OpenCode to connect it to your cloud workspace. It will close and reopen in a few seconds."
cat > /tmp/opencode-reconfig.sh << 'RECONFIG'
#!/bin/bash
OPENCODE_PASSWORD="REPLACE_PASSWORD"
INSTANCE_ID="REPLACE_INSTANCE_ID"
PYTHON3="REPLACE_PYTHON3_PATH"
SETTINGS_DAT="$HOME/Library/Application Support/ai.opencode.desktop/opencode.settings.dat"
GLOBAL_DAT="$HOME/Library/Application Support/ai.opencode.desktop/opencode.global.dat"
sleep 3
osascript -e 'quit app "OpenCode"' 2>/dev/null
sleep 3
pkill -f "OpenCode" 2>/dev/null
sleep 1
echo '{"defaultServerUrl":"http://localhost:4096"}' > "$SETTINGS_DAT"
"$PYTHON3" -c "
import json, os
path = os.path.expanduser('~/Library/Application Support/ai.opencode.desktop/opencode.global.dat')
try:
with open(path) as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
data = {}
server = json.loads(data.get('server', '{}'))
if 'list' not in server:
server['list'] = []
server['list'] = [s for s in server['list'] if s.get('http', {}).get('url') != 'http://localhost:4096']
server['list'].append({
'type': 'http',
'http': {'url': 'http://localhost:4096', 'username': 'opencode', 'password': '$OPENCODE_PASSWORD'},
'displayName': 'Cloud: $INSTANCE_ID'
})
data['server'] = json.dumps(server)
with open(path, 'w') as f:
json.dump(data, f)
"
open -a OpenCode
RECONFIG
chmod +x /tmp/opencode-reconfig.sh
# Replace placeholders
sed -i '' "s|REPLACE_PASSWORD|$OPENCODE_PASSWORD|" /tmp/opencode-reconfig.sh
sed -i '' "s|REPLACE_INSTANCE_ID|$INSTANCE_ID|" /tmp/opencode-reconfig.sh
sed -i '' "s|REPLACE_PYTHON3_PATH|$(which python3 2>/dev/null || echo python3)|" /tmp/opencode-reconfig.sh
# Launch detached
nohup /tmp/opencode-reconfig.sh > /tmp/opencode-reconfig.log 2>&1 &
disownStep 13: Convenience scripts + tunnel service
Start/stop script
cat > ~/opencode-workspace.sh << 'SCRIPT'
#!/bin/bash
INSTANCE_ID="INSTANCE_ID_HERE"
PROJECT_ID="path26-489205"
ZONE="europe-west1-b"
GCLOUD="GCLOUD_PATH_HERE"
case "$1" in
start)
echo "Starting your cloud workspace..."
"$GCLOUD" compute instances start "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --quiet
echo "Waiting for it to be ready..."
for i in $(seq 1 30); do
"$GCLOUD" compute ssh "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --command='curl -sf http://localhost:8080/health' --quiet 2>/dev/null && break
sleep 5
done
echo "Workspace is ready! Open OpenCode to connect."
;;
stop)
echo "Stopping your cloud workspace (files are saved)..."
"$GCLOUD" compute instances stop "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --quiet
echo "Stopped. Start again with: opencode-workspace start"
;;
status)
STATUS=$("$GCLOUD" compute instances describe "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --format='value(status)' 2>/dev/null)
echo "Workspace: $INSTANCE_ID — $STATUS"
;;
*) echo "Usage: opencode-workspace [start|stop|status]" ;;
esac
SCRIPT
chmod +x ~/opencode-workspace.sh
SHELL_RC="$HOME/.zshrc"
[ -f "$HOME/.bashrc" ] && SHELL_RC="$HOME/.bashrc"
touch "$SHELL_RC"
grep -q opencode-workspace "$SHELL_RC" || echo 'alias opencode-workspace="$HOME/opencode-workspace.sh"' >> "$SHELL_RC"Replace INSTANCE_ID_HERE and GCLOUD_PATH_HERE with actual values.
Tunnel service (launchd)
GCLOUD_PATH="$(which gcloud)"
mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/com.freshvibe.opencode-tunnel.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.freshvibe.opencode-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>${GCLOUD_PATH}</string>
<string>compute</string>
<string>ssh</string>
<string>${INSTANCE_ID}</string>
<string>--project=${PROJECT_ID}</string>
<string>--zone=${ZONE}</string>
<string>--</string>
<string>-L</string>
<string>4096:localhost:4096</string>
<string>-N</string>
<string>-o</string>
<string>ServerAliveInterval=60</string>
</array>
<key>KeepAlive</key><true/>
<key>RunAtLoad</key><true/>
<key>ThrottleInterval</key><integer>10</integer>
<key>StandardOutPath</key><string>/tmp/opencode-tunnel.log</string>
<key>StandardErrorPath</key><string>/tmp/opencode-tunnel.log</string>
</dict>
</plist>
EOF
launchctl load ~/Library/LaunchAgents/com.freshvibe.opencode-tunnel.plistRemove: launchctl unload ~/Library/LaunchAgents/com.freshvibe.opencode-tunnel.plist && rm ~/Library/LaunchAgents/com.freshvibe.opencode-tunnel.plist
Other operations
List workspaces:
gcloud compute instances list --project="${GCP_PROJECT_ID:-path26-489205}" --zones="${GCP_GCE_ZONE:-europe-west1-b}" --filter='labels.managed-by=freshvibe' --format='table(name,status,labels.owner,labels.project)'Stop: gcloud compute instances stop "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --quiet
Delete: gcloud compute instances delete "$INSTANCE_ID" --project="$PROJECT_ID" --zone="$ZONE" --quiet
Delete saved files too: gcloud compute disks delete "${INSTANCE_ID}-home" --project="$PROJECT_ID" --zone="$ZONE" --quiet
GH_TOKEN in template
One-time setup to bake GH_TOKEN into the instance template:
./scripts/gcp-gce-setup.sh \
--project-id=path26-489205 --zone=europe-west1-b \
--instance-template=freshvibe-gce-template \
--no-external-ip --daemon-source-ranges=10.0.0.0/8 \
--agent-env="CLAUDE_CODE_USE_VERTEX=1" \
--agent-env="CLOUD_ML_REGION=global" \
--agent-env="ANTHROPIC_VERTEX_PROJECT_ID=path26-489205" \
--agent-env="GOOGLE_CLOUD_PROJECT=path26-489205" \
--agent-env="ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-6" \
--agent-env="ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6" \
--agent-env="ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5@20251001" \
--agent-env="GH_TOKEN=YOUR_GITHUB_PAT_HERE" \
--skip-managed-image-build --skip-web-bootstrap --skip-firewall --set-defaultsAd-hoc on running instance: gcloud compute ssh ... --command='echo "export GH_TOKEN=TOKEN" >> ~/.bashrc'
Port proxy
Dev servers on port 3000 are accessible at https://SLUG.path26.rome2rio.com when a port slug is registered (Step 11). The LB URL mask routes <slug>.path26.rome2rio.com to the instance's port 3000.
Windows Commands Reference
Exact commands for each step on Windows.
CRITICAL: Your shell tool runs cmd.exe, NOT PowerShell. Wrap ALL PowerShell commands:
powershell -ExecutionPolicy Bypass -Command "YOUR_COMMAND"NEVER run bare PowerShell syntax without the wrapper.
CRITICAL: gcloud compute ssh uses PuTTY (plink.exe) on Windows. SSH port forwarding (-- -L 4096:...) WILL FAIL. Use gcloud compute start-iap-tunnel instead.
CRITICAL: For remote SSH commands, NEVER use &&, ||, 2>/dev/null. Use ; or separate SSH calls.
Step 2: Python (skip on Windows)
Windows gcloud bundles its own Python. Skip this step.
Step 3: gcloud CLI
Check these locations. Save the first found path as GCLOUD_CMD — use it in ALL subsequent commands.
powershell -ExecutionPolicy Bypass -Command "
$locations = @(
\"$env:LOCALAPPDATA\Google\Cloud SDK\google-cloud-sdk\bin\gcloud.cmd\",
\"$env:ProgramFiles\Google\Cloud SDK\google-cloud-sdk\bin\gcloud.cmd\",
\"${env:ProgramFiles(x86)}\Google\Cloud SDK\google-cloud-sdk\bin\gcloud.cmd\"
)
foreach ($p in $locations) {
if (Test-Path $p) { Write-Output \"FOUND: $p\"; exit 0 }
}
$gcmd = Get-Command gcloud -ErrorAction SilentlyContinue
if ($gcmd) { Write-Output \"FOUND: $($gcmd.Source)\"; exit 0 }
Write-Output 'NOT_FOUND'
"If NOT_FOUND:
powershell -ExecutionPolicy Bypass -Command "winget install --id Google.CloudSDK -e --accept-package-agreements --accept-source-agreements"Then re-run the check to find the installed path.
Step 4: OpenCode
Check these locations. Save found path as OPENCODE_CMD.
powershell -ExecutionPolicy Bypass -Command "
$locations = @(
\"$env:LOCALAPPDATA\OpenCode\opencode-cli.exe\",
\"$env:LOCALAPPDATA\Programs\opencode\opencode.exe\",
\"$env:USERPROFILE\.opencode\bin\opencode.exe\"
)
foreach ($p in $locations) {
if (Test-Path $p) { Write-Output \"FOUND: $p\"; exit 0 }
}
$oc = Get-Command opencode -ErrorAction SilentlyContinue
if ($oc) { Write-Output \"FOUND: $($oc.Source)\"; exit 0 }
$oc2 = Get-Command opencode-cli -ErrorAction SilentlyContinue
if ($oc2) { Write-Output \"FOUND: $($oc2.Source)\"; exit 0 }
Write-Output 'NOT_FOUND'
"If NOT_FOUND, tell user: "Please download OpenCode from https://opencode.ai/download and install it." Do NOT try irm | iex — that URL doesn't exist.
Step 5: Sign in
NEVER run gcloud auth login synchronously on Windows (it will time out).
1. Launch in a separate window:
powershell -ExecutionPolicy Bypass -Command "Start-Process 'GCLOUD_CMD' -ArgumentList 'auth','login','--project=path26-489205'"2. Tell user: "A browser window should open for Google sign-in. Complete it, then tell me when done."
3. After user confirms, verify:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' auth list --format='value(account)' 2>$null"4. Set project:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' config set project path26-489205"Step 6: Collect user info
Do NOT ask until auth is confirmed.
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' config get-value account 2>$null"If empty, try: powershell -ExecutionPolicy Bypass -Command "git config --global user.email 2>$null"
Step 7: Create workspace
Generate instance ID:
powershell -ExecutionPolicy Bypass -Command "$id = 'opencode-' + -join((48..57)+(97..102)|Get-Random -Count 8|%%{[char]$_}); Write-Output $id"Save output as INSTANCE_ID.
Encode metadata:
powershell -ExecutionPolicy Bypass -Command "[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('USER_EMAIL'))"Save as EMAIL_B64.
powershell -ExecutionPolicy Bypass -Command "[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('[""PROJECT_NAME""]'))"Save as PROJECT_B64.
Create disk:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute disks create 'INSTANCE_ID-home' --project=path26-489205 --zone=europe-west1-b --size=10GB --type=pd-balanced --labels=managed-by=freshvibe,owner=OWNER_LABEL,project=PROJECT_LABEL --quiet"Create instance — NO `--metadata` (would replace template metadata!):
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute instances create 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --source-instance-template=freshvibe-gce-template --labels=managed-by=freshvibe,owner=OWNER_LABEL,project=PROJECT_LABEL --disk='name=INSTANCE_ID-home,device-name=freshvibe-home,mode=rw,boot=no,auto-delete=no' --quiet"Add metadata AFTER creation:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute instances add-metadata 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --metadata=freshvibe-owner-email-b64=EMAIL_B64,freshvibe-project-ids-b64=PROJECT_B64 --quiet"Step 8: Wait for ready
Run every 10 seconds, up to 30 times:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='curl -sf http://localhost:8080/health' --quiet 2>$null"Exit code 0 + non-empty output = ready. Otherwise wait 10 seconds and retry.
Step 9: Install and start OpenCode server
Generate password:
powershell -ExecutionPolicy Bypass -Command "$p = -join((48..57)+(65..90)+(97..122)|Get-Random -Count 32|%%{[char]$_}); Write-Output $p"Save as OPENCODE_PASSWORD.
Install:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='curl -fsSL https://opencode.ai/install | bash'"Find remote home:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='echo $HOME' --quiet"Save as REMOTE_HOME (e.g., /home/ben).
Start server (bash -lc = login shell so GH_TOKEN, Vertex AI env vars from /etc/profile.d/ are loaded):
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='tmux new-session -d -s oc ""bash -lc \""OPENCODE_SERVER_PASSWORD=OPENCODE_PASSWORD REMOTE_HOME/.opencode/bin/opencode serve --port 4096 --hostname 0.0.0.0\""""'"Replace REMOTE_HOME and OPENCODE_PASSWORD with actual values.
Verify:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='tmux has-session -t oc'"Step 10: Clone starter repo
Read GH_TOKEN from .env:
powershell -ExecutionPolicy Bypass -Command "$token = (Select-String -Path '.env' -Pattern '^GH_TOKEN=(.+)$').Matches.Groups[1].Value; Write-Output $token"Save as GH_TOKEN_VALUE.
Clone (public):
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='cd ~ ; git clone https://github.com/fwfutures/fv-rome2rio-starter.git'"Clone (private — embed token, then strip):
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='cd ~ ; git clone https://x-access-token:GH_TOKEN_VALUE@github.com/fwfutures/fv-rome2rio-starter.git'"powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute ssh 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --command='cd ~/fv-rome2rio-starter ; git remote set-url origin https://github.com/fwfutures/fv-rome2rio-starter.git'"Step 11: Port slug
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute instances add-metadata 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --metadata='freshvibe-port-slugs=[""PORT_SLUG""]'"Step 12: Tunnel + desktop app config
12a: Start IAP tunnel (detached)
powershell -ExecutionPolicy Bypass -Command "Start-Process -FilePath 'GCLOUD_CMD' -ArgumentList 'compute','start-iap-tunnel','INSTANCE_ID','4096','--local-host-port=localhost:4096','--project=path26-489205','--zone=europe-west1-b' -WindowStyle Hidden"Test (expect 401 = working):
powershell -ExecutionPolicy Bypass -Command "Start-Sleep -Seconds 10; try { $null = Invoke-WebRequest -Uri 'http://localhost:4096' -UseBasicParsing -TimeoutSec 5 } catch { if ($_.Exception.Response.StatusCode -eq 401) { Write-Output 'TUNNEL_OK' } else { Write-Output 'TUNNEL_FAILED' } }"12b: Reconfig desktop app (self-destruct pattern)
Tell user: "I'm about to restart OpenCode to connect it to your cloud workspace."
Write reconfig script:
powershell -ExecutionPolicy Bypass -Command "
@'
Start-Sleep 3
Get-Process OpenCode -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep 3
'{""defaultServerUrl"":""http://localhost:4096""}' | Set-Content ""$env:APPDATA\ai.opencode.desktop\opencode.settings.dat""
$globalPath = ""$env:APPDATA\ai.opencode.desktop\opencode.global.dat""
if (Test-Path $globalPath) { $data = Get-Content $globalPath | ConvertFrom-Json } else { $data = @{} }
$serverJson = if ($data.server) { $data.server } else { '{}' }
$serverData = $serverJson | ConvertFrom-Json
if (-not $serverData.list) { $serverData | Add-Member -NotePropertyName 'list' -NotePropertyValue @() -Force }
$serverData.list = @($serverData.list | Where-Object { $_.http.url -ne 'http://localhost:4096' })
$newServer = @{ type='http'; http=@{ url='http://localhost:4096'; username='opencode'; password='OPENCODE_PASSWORD' }; displayName='Cloud: INSTANCE_ID' }
$serverData.list += $newServer
$data.server = ($serverData | ConvertTo-Json -Depth 10 -Compress)
$data | ConvertTo-Json -Depth 10 | Set-Content $globalPath
Start-Process ""$env:LOCALAPPDATA\OpenCode\OpenCode.exe""
'@ | Set-Content ""$env:TEMP\opencode-reconfig.ps1""
"Launch detached:
powershell -ExecutionPolicy Bypass -Command "Start-Process powershell -ArgumentList '-ExecutionPolicy','Bypass','-File','$env:TEMP\opencode-reconfig.ps1' -WindowStyle Hidden"Replace OPENCODE_PASSWORD and INSTANCE_ID with actual values in the script content.
devserver.txt (Windows format)
powershell -ExecutionPolicy Bypass -Command "
@'
My Cloud Workspace
==================
Last updated: $(Get-Date -Format 'yyyy-MM-dd h:mm tt')
Name: INSTANCE_ID
Project: PROJECT_NAME
Owner: USER_EMAIL
Password: OPENCODE_PASSWORD
Local Machine
-------------
Tunnel: Running (IAP on port 4096)
OpenCode URL: http://localhost:4096
gcloud path: GCLOUD_CMD
Remote Machine
--------------
Instance: Running
OpenCode server: Running (tmux session: oc)
Cloud Details
-------------
GCP Project: path26-489205
Zone: europe-west1-b
'@ | Set-Content devserver.txt
"Step 13: Convenience scripts + tunnel service
Start/stop script
powershell -ExecutionPolicy Bypass -Command "
@'
param([string]`$Action)
`$INSTANCE_ID = 'INSTANCE_ID_HERE'
`$PROJECT_ID = 'path26-489205'
`$ZONE = 'europe-west1-b'
`$GCLOUD = 'GCLOUD_CMD'
switch (`$Action) {
'start' {
Write-Output 'Starting your cloud workspace...'
& `$GCLOUD compute instances start `$INSTANCE_ID --project=`$PROJECT_ID --zone=`$ZONE --quiet
Write-Output 'Waiting for it to be ready...'
for (`$i=1; `$i -le 30; `$i++) {
try { `$r = & `$GCLOUD compute ssh `$INSTANCE_ID --project=`$PROJECT_ID --zone=`$ZONE --command='curl -sf http://localhost:8080/health' --quiet 2>`$null; if (`$r) { break } } catch {}
Start-Sleep 5
}
Write-Output 'Workspace is ready! Open OpenCode to connect.'
}
'stop' {
Write-Output 'Stopping your cloud workspace (files are saved)...'
& `$GCLOUD compute instances stop `$INSTANCE_ID --project=`$PROJECT_ID --zone=`$ZONE --quiet
Write-Output 'Stopped.'
}
'status' {
`$s = & `$GCLOUD compute instances describe `$INSTANCE_ID --project=`$PROJECT_ID --zone=`$ZONE --format='value(status)' 2>`$null
Write-Output `"Workspace: `$INSTANCE_ID - `$s`"
}
default { Write-Output 'Usage: opencode-workspace start|stop|status' }
}
'@ | Set-Content `"`$env:USERPROFILE\opencode-workspace.ps1`"
Write-Output 'Created ~/opencode-workspace.ps1'
"Tunnel service (Scheduled Task)
powershell -ExecutionPolicy Bypass -Command "
`$action = New-ScheduledTaskAction -Execute 'GCLOUD_CMD' -Argument 'compute start-iap-tunnel INSTANCE_ID 4096 --local-host-port=localhost:4096 --project=path26-489205 --zone=europe-west1-b'
`$trigger = New-ScheduledTaskTrigger -AtLogOn
`$settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit 0
Register-ScheduledTask -TaskName 'OpenCode Cloud Tunnel' -Action `$action -Trigger `$trigger -Settings `$settings -Description 'IAP tunnel to GCE workspace'
Start-ScheduledTask -TaskName 'OpenCode Cloud Tunnel'
Write-Output 'Tunnel service installed'
"Remove:
powershell -ExecutionPolicy Bypass -Command "Stop-ScheduledTask -TaskName 'OpenCode Cloud Tunnel' -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName 'OpenCode Cloud Tunnel' -Confirm:`$false -ErrorAction SilentlyContinue"Other operations
List workspaces:
powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute instances list --project=path26-489205 --zones=europe-west1-b --filter='labels.managed-by=freshvibe' --format='table(name,status,labels.owner,labels.project)'"Stop: powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute instances stop 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --quiet"
Delete: powershell -ExecutionPolicy Bypass -Command "& 'GCLOUD_CMD' compute instances delete 'INSTANCE_ID' --project=path26-489205 --zone=europe-west1-b --quiet"
Windows-specific notes
- PowerShell aliases
curltoInvoke-WebRequest. Usecurl.exefor real curl. - "External IP not found; defaulting to IAP tunneling" is normal.
- If
gcloud compute ssh --command=fails with quoting errors: use;not&&, use separate SSH calls, use single quotes around--command=value.
param(
[string]$SessionId = 'ses_30052ecc6ffe0dSN2L9cKFxmB4',
[string]$Title = 'Greeting quick check-in',
[string]$CliPath = "$env:LOCALAPPDATA\OpenCode\opencode-cli.exe",
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $CliPath)) {
throw "OpenCode CLI not found at: $CliPath"
}
if (-not $OutputPath) {
$safeTitle = ($Title -replace '[^A-Za-z0-9._ -]', '').Trim()
if (-not $safeTitle) {
$safeTitle = $SessionId
}
$safeTitle = $safeTitle -replace '\s+', '-'
$OutputPath = Join-Path -Path (Get-Location) -ChildPath ("{0}-{1}.json" -f $safeTitle, $SessionId)
}
& $CliPath export $SessionId | Out-File -LiteralPath $OutputPath -Encoding utf8
Write-Output "Exported session to: $OutputPath"