Skip to content

Instantly share code, notes, and snippets.

@jeremyckahn
Last active May 28, 2026 02:28
Show Gist options
  • Select an option

  • Save jeremyckahn/9ae607bb340b4c657e2c5c017f37de38 to your computer and use it in GitHub Desktop.

Select an option

Save jeremyckahn/9ae607bb340b4c657e2c5c017f37de38 to your computer and use it in GitHub Desktop.
#!/bin/bash
# opencode-sbx.sh
#
# ==============================================================================
# Overview:
# This script automates the creation and management of persistent, secure, and
# isolated AI agent development environments using Docker Sandboxes (sbx)
# running OpenCode.
#
# What it does & What it's for:
# 1. Sandbox Isolation: Runs the OpenCode agent as a headless server inside an
# isolated, secure microVM sandbox. The sandbox mounts your project workspace
# read-write, keeping your host system files safe from autonomous agent edits.
# 2. Configuration Mapping: Mounts and symlinks your host system's global OpenCode
# configuration folder (~/.config/opencode) into the sandbox so your agent
# inherits your local settings, preferences, and custom models automatically.
# 3. LLM Network Bridging: Runs a TCP forwarder inside the sandbox to route
# local llama.cpp traffic (on port 4000) out to the host system's llama.cpp
# instance, since sandboxes are otherwise network-isolated.
# 4. Remote Exposure: Exposes the headless OpenCode server on a customizable port
# (default: 3000) binding to all interfaces (0.0.0.0), allowing you to interact
# with the agent remotely over your Tailscale Tailnet network.
# 5. Keep-Alive: Spawns a host-side background process to maintain an active CLI
# connection, preventing the sandbox daemon from auto-stopping the microVM
# due to inactivity.
#
# System Dependencies:
# - sbx CLI (Docker Sandboxes): Must be installed and authenticated on the host.
# Install on macOS via Homebrew: brew install docker/tap/sbx
# - Docker: Docker daemon must be active on the host machine.
# - llama.cpp: Required for LLM inference (expected to be listening on host port 4000).
# - Tailscale: Required on the host to expose the port (3000/custom) over your Tailnet.
# - socat: Must be available in the sandbox (installed by default in the opencode template)
# to bridge port 4000 back to the host system.
# ==============================================================================
COMMAND="${1:-help}"
# Print help/usage instructions
print_help() {
echo "Usage: ./opencode-sbx.sh <start|stop|help> [path] [port]"
echo " ./opencode-sbx.sh <start|stop|help> [port]"
echo ""
echo "Commands:"
echo " start Starts the Opencode headless server inside a Docker sandbox."
echo " By default, uses the current directory as the workspace and port 3000."
echo " stop Stops the Opencode headless server and the sandbox."
echo " By default, stops the sandbox for the current directory."
echo " help Prints this usage guide and exits."
echo ""
echo "Arguments:"
echo " [path] Optional. Absolute or relative path to the workspace directory"
echo " (defaults to the current working directory)."
echo " [port] Optional. Host port to expose the headless server on"
echo " (defaults to port 3000)."
echo ""
echo "Examples:"
echo " ./opencode-sbx.sh start"
echo " ./opencode-sbx.sh start 4002"
echo " ./opencode-sbx.sh start /path/to/workspace 4005"
echo " ./opencode-sbx.sh stop 4002"
}
if [ "$COMMAND" = "help" ] || [ "$COMMAND" = "-h" ] || [ "$COMMAND" = "--help" ]; then
print_help
exit 0
fi
if [ "$COMMAND" != "start" ] && [ "$COMMAND" != "stop" ]; then
echo "Error: Invalid command '$COMMAND'."
echo ""
print_help
exit 1
fi
# Helper function to check if a value is numeric
is_numeric() {
[[ "$1" =~ ^[0-9]+$ ]]
}
# Determine optional path and port arguments
ARG2="$2"
ARG3="$3"
WORKSPACE_DIR=""
EXPOSED_PORT="3000" # Default port
if [ -n "$ARG2" ]; then
if is_numeric "$ARG2"; then
# Example: ./opencode-sbx.sh start 4002
WORKSPACE_DIR=$(cd "." && pwd)
EXPOSED_PORT="$ARG2"
else
# Example: ./opencode-sbx.sh start /path/to/workspace [port]
WORKSPACE_DIR=$(cd "$ARG2" && pwd)
if [ -n "$ARG3" ] && is_numeric "$ARG3"; then
EXPOSED_PORT="$ARG3"
fi
fi
else
# Example: ./opencode-sbx.sh start
WORKSPACE_DIR=$(cd "." && pwd)
fi
CONFIG_DIR="$HOME/.config/opencode"
# Generate unique and valid sandbox name based on workspace path
BASENAME=$(basename "$WORKSPACE_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
if command -v md5 >/dev/null 2>&1; then
PATH_HASH=$(echo -n "$WORKSPACE_DIR" | md5 | cut -c1-8)
elif command -v md5sum >/dev/null 2>&1; then
PATH_HASH=$(echo -n "$WORKSPACE_DIR" | md5sum | cut -c1-8)
else
PATH_HASH=$(echo -n "$WORKSPACE_DIR" | cksum | awk '{print $1}')
fi
SANDBOX_NAME="opencode-${BASENAME}-${PATH_HASH}"
if [ "$COMMAND" = "start" ]; then
echo "Workspace: $WORKSPACE_DIR"
echo "Sandbox Name: $SANDBOX_NAME"
echo "Exposed Port: $EXPOSED_PORT"
# 1. Clean up existing sandbox if it exists or start it
if sbx ls | grep -q "$SANDBOX_NAME"; then
echo "Sandbox $SANDBOX_NAME already exists."
STATUS=$(sbx ls | grep "$SANDBOX_NAME" | awk '{print $3}')
if [ "$STATUS" = "stopped" ]; then
echo "Starting sandbox..."
sbx exec "$SANDBOX_NAME" echo "Starting up..."
fi
else
echo "Creating sandbox $SANDBOX_NAME..."
sbx create --name "$SANDBOX_NAME" opencode "$WORKSPACE_DIR" "$CONFIG_DIR:ro"
sleep 2
sbx exec "$SANDBOX_NAME" echo "Sandbox booted."
fi
# 2. Setup configuration and workspace symlinks inside sandbox
echo "Setting up configuration and workspace symlinks..."
sbx exec "$SANDBOX_NAME" mkdir -p /home/agent/.config
sbx exec "$SANDBOX_NAME" rm -f /home/agent/.config/opencode
sbx exec "$SANDBOX_NAME" ln -sf "$CONFIG_DIR" /home/agent/.config/opencode
sbx exec "$SANDBOX_NAME" sh -c "rm -rf /home/agent/workspace && ln -sf $WORKSPACE_DIR /home/agent/workspace"
# 3. Start socat port forwarder to bridge llama.cpp at port 4000
echo "Starting llama.cpp port forwarder (localhost:4000 -> host.docker.internal:4000)..."
sbx exec "$SANDBOX_NAME" pkill -f "socat TCP-LISTEN:4000" || true
sbx exec "$SANDBOX_NAME" sh -c "nohup socat TCP-LISTEN:4000,fork TCP:host.docker.internal:4000 >> /tmp/socat.log 2>&1 &"
# 4. Start Opencode headless server
echo "Starting Opencode headless server inside sandbox..."
sbx exec "$SANDBOX_NAME" pkill -f "opencode serve" || true
# Collect any host environment variables prefixed with OPENCODE_ to forward to the sandbox
ENV_FLAGS=()
# Always set OPENCODE_OFFLINE=1 as a sane default, unless overridden by the host environment
if [ -z "$OPENCODE_OFFLINE" ]; then
ENV_FLAGS+=("-e" "OPENCODE_OFFLINE=1")
fi
for var in $(env | grep -E '^OPENCODE_' | cut -d= -f1); do
ENV_FLAGS+=("-e" "$var=${!var}")
done
sbx exec "${ENV_FLAGS[@]}" "$SANDBOX_NAME" sh -c "cd /home/agent/workspace && nohup opencode serve --port 4096 --hostname 0.0.0.0 >> /tmp/opencode.log 2>&1 &"
# 5. Expose host port to sandbox port 4096 (all interfaces for Tailnet)
echo "Exposing port $EXPOSED_PORT over Tailnet (binding 0.0.0.0:$EXPOSED_PORT to sandbox:4096)..."
sbx ports "$SANDBOX_NAME" --unpublish "$EXPOSED_PORT:4096" 2>/dev/null || true
sbx ports "$SANDBOX_NAME" --publish "0.0.0.0:$EXPOSED_PORT:4096"
# 6. Start host-side keep-alive process to prevent auto-stop
echo "Starting keep-alive session to prevent auto-stop..."
pkill -f "sbx exec $SANDBOX_NAME sleep infinity" || true
nohup sbx exec "$SANDBOX_NAME" sleep infinity >/dev/null 2>&1 &
echo "--------------------------------------------------------"
echo "Opencode headless server is running in Sandbox!"
echo "Workspace: $WORKSPACE_DIR"
echo "Config: $CONFIG_DIR/opencode.json"
echo "Port: $EXPOSED_PORT (0.0.0.0, available over Tailnet)"
echo "LLM Bridge: localhost:4000 inside sandbox -> host:4000 (llama.cpp)"
echo "Check sandbox status: sbx ls"
echo "View Opencode server logs: sbx exec $SANDBOX_NAME cat /tmp/opencode.log"
echo "View forwarder logs: sbx exec $SANDBOX_NAME cat /tmp/socat.log"
echo "--------------------------------------------------------"
elif [ "$COMMAND" = "stop" ]; then
echo "Target Workspace: $WORKSPACE_DIR"
echo "Target Sandbox: $SANDBOX_NAME"
if ! sbx ls | grep -q "$SANDBOX_NAME"; then
echo "Sandbox $SANDBOX_NAME does not exist."
exit 0
fi
echo "Stopping Opencode serve inside sandbox..."
sbx exec "$SANDBOX_NAME" pkill -f "opencode serve" || true
echo "Stopping socat port forwarder inside sandbox..."
sbx exec "$SANDBOX_NAME" pkill -f "socat TCP-LISTEN:4000" || true
echo "Stopping host-side keep-alive session..."
pkill -f "sbx exec $SANDBOX_NAME sleep infinity" || true
# Clean up published ports
echo "Cleaning up published ports..."
sbx ports "$SANDBOX_NAME" 2>/dev/null | grep "Published" | while read -r line; do
# Extract host binding (e.g. 0.0.0.0:3000) and container port (e.g. 4096/tcp)
UNPUB_FORMAT=$(echo "$line" | awk '{print $2 ":" $4}' | sed 's/\/tcp//')
sbx ports "$SANDBOX_NAME" --unpublish "$UNPUB_FORMAT" 2>/dev/null || true
done
echo "Stopping sandbox..."
sbx stop "$SANDBOX_NAME"
echo "Sandbox $SANDBOX_NAME has been stopped."
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment